Skip to content
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Validate synthetic ID format on inbound values from the `x-synthetic-id` header and `synthetic_id` cookie; values that do not match the expected format (`64-hex-hmac.6-alphanumeric-suffix`) are discarded and a fresh ID is generated rather than forwarded to response headers, cookies, or third-party APIs

### Fixed

- Protocol-relative creative URLs now honor `rewrite.exclude_domains`, so excluded creative assets stay direct and excluded absolute or protocol-relative URLs submitted to `/first-party/sign` are rejected.

### Added

- Added the default-true `[auction].rewrite_creatives` option. Setting it to `false` preserves mandatory creative sanitization across `POST /auction` and publisher SSAT/page-bids delivery while skipping first-party resource/click URL rewriting; it also skips creative TSJS injection on `POST /auction`.
- Added Osano consent mirror integration docs and public enablement guidance.
- Implemented basic authentication for configurable endpoint paths (#73)
- Added integrations guide with example `testlight` integration
Expand Down
194 changes: 194 additions & 0 deletions crates/trusted-server-cli/tests/config_env_overlay.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
//! Regression coverage for typed app-config environment overlays.

use std::fs;
use std::process::{Command, Output};

use tempfile::TempDir;
use toml_edit::{DocumentMut, value};

const LEGACY_CONFIG: &str = include_str!(
"../../trusted-server-integration-tests/fixtures/configs/trusted-server.integration.toml"
);
const MANIFEST: &str = r#"
[app]
name = "trusted-server"

[adapters.axum.adapter]
crate = "crates/trusted-server-adapter-axum"

[adapters.axum.commands]
build = "echo"
deploy = "echo"
serve = "echo"

[stores.config]
ids = ["trusted_server_config"]

[stores.secrets]
ids = ["trusted_server_secrets"]
"#;
const REWRITE_ENV: &str = "TRUSTED_SERVER__AUCTION__REWRITE_CREATIVES";

struct MigratedProject {
directory: TempDir,
config_path: std::path::PathBuf,
manifest_path: std::path::PathBuf,
}

fn migrated_legacy_project() -> MigratedProject {
let directory = tempfile::tempdir().expect("should create temporary config directory");
let config_path = directory.path().join("trusted-server.toml");
let manifest_path = directory.path().join("edgezero.toml");
let mut document = LEGACY_CONFIG
.parse::<DocumentMut>()
.expect("should parse legacy integration config");
document["auction"]["rewrite_creatives"] = value(true);
fs::write(&config_path, document.to_string()).expect("should write migrated config");
fs::write(&manifest_path, MANIFEST).expect("should write test manifest");
MigratedProject {
directory,
config_path,
manifest_path,
}
}

fn validate_with_overlay(project: &MigratedProject, raw_value: &str) -> Output {
Command::new(env!("CARGO_BIN_EXE_ts"))
.args(["config", "validate", "--manifest"])
.arg(&project.manifest_path)
.arg("--app-config")
.arg(&project.config_path)
.current_dir(project.directory.path())
.env(REWRITE_ENV, raw_value)
.output()
.expect("should run ts config validate")
Comment thread
ChristianPavilonis marked this conversation as resolved.
}

#[test]
fn migrated_legacy_config_applies_rewrite_creatives_environment_override() {
let project = migrated_legacy_project();
let output = Command::new(env!("CARGO_BIN_EXE_ts"))
.args(["config", "push", "--adapter", "axum", "--manifest"])
.arg(&project.manifest_path)
.arg("--app-config")
.arg(&project.config_path)
.args(["--yes", "--no-diff"])
.current_dir(project.directory.path())
.env(REWRITE_ENV, "false")
.output()
.expect("should run ts config push");

assert!(
output.status.success(),
"valid boolean overlay should push successfully: {}",
String::from_utf8_lossy(&output.stderr)
);

let local_store_path = project
.directory
.path()
.join(".edgezero/local-config-trusted_server_config.json");
let local_store: serde_json::Value = serde_json::from_str(
&fs::read_to_string(local_store_path).expect("should read pushed local config"),
)
.expect("should parse local config store");
let envelope_json = local_store
.as_object()
.and_then(|entries| entries.values().next())
.and_then(serde_json::Value::as_str)
.expect("should contain a blob envelope");
let envelope: serde_json::Value =
serde_json::from_str(envelope_json).expect("should parse blob envelope");

assert_eq!(
envelope["data"]["auction"]["rewrite_creatives"],
serde_json::Value::Bool(false),
"pushed config should contain the environment override"
);
}

#[test]
fn migrated_legacy_config_default_rewrite_creatives_has_no_local_diff() {
let project = migrated_legacy_project();
let push = Command::new(env!("CARGO_BIN_EXE_ts"))
.args(["config", "push", "--adapter", "axum", "--manifest"])
.arg(&project.manifest_path)
.arg("--app-config")
.arg(&project.config_path)
.args(["--yes", "--no-diff"])
.current_dir(project.directory.path())
.output()
.expect("should run ts config push");

assert!(
push.status.success(),
"default rewrite setting should push successfully: {}",
String::from_utf8_lossy(&push.stderr)
);

let local_store_path = project
.directory
.path()
.join(".edgezero/local-config-trusted_server_config.json");
let local_store: serde_json::Value = serde_json::from_str(
&fs::read_to_string(local_store_path).expect("should read pushed local config"),
)
.expect("should parse local config store");
let envelope_json = local_store
.as_object()
.and_then(|entries| entries.values().next())
.and_then(serde_json::Value::as_str)
.expect("should contain a blob envelope");
let envelope: serde_json::Value =
serde_json::from_str(envelope_json).expect("should parse blob envelope");

assert!(
envelope["data"]["auction"]
.get("rewrite_creatives")
.is_none(),
"should omit the default rewrite setting from the pushed blob"
);

let diff = Command::new(env!("CARGO_BIN_EXE_ts"))
.args([
"config",
"diff",
"--adapter",
"axum",
"--local",
"--manifest",
])
.arg(&project.manifest_path)
.arg("--app-config")
.arg(&project.config_path)
.current_dir(project.directory.path())
.output()
.expect("should run ts config diff");

assert!(
diff.status.success(),
"default rewrite setting should have no local diff: {}",
String::from_utf8_lossy(&diff.stderr)
);
assert!(
String::from_utf8_lossy(&diff.stderr).contains("# no changes"),
"diff should report no changes: {}",
String::from_utf8_lossy(&diff.stderr)
);
}

#[test]
fn migrated_legacy_config_rejects_invalid_rewrite_creatives_environment_override() {
let project = migrated_legacy_project();
let output = validate_with_overlay(&project, "not-a-boolean");
let stderr = String::from_utf8_lossy(&output.stderr);

assert!(
!output.status.success(),
"invalid boolean overlay should fail validation"
);
assert!(
stderr.contains(REWRITE_ENV) && stderr.contains("boolean"),
"error should identify the invalid boolean overlay: {stderr}"
);
}
13 changes: 10 additions & 3 deletions crates/trusted-server-core/src/auction/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,8 @@ When a request arrives at the `/auction` endpoint, it goes through the following
┌──────────────────────────────────────────────────────────────────────┐
│ 11. Transform to OpenRTB Response (mod.rs:274-322) │
│ - Build seatbid array (one per winning bid) │
│ - Rewrite creative HTML for first-party proxy │
│ - Always sanitize creative HTML │
│ - Rewrite creative HTML when enabled (default) │
│ - Add orchestrator metadata (timing, strategy, bid count) │
└──────────────────────────────────────────────────────────────────────┘
Expand Down Expand Up @@ -248,7 +249,12 @@ The orchestrator collects all bids and creates an OpenRTB response:
}
```

Note that creative HTML is rewritten to use the first-party proxy (`/first-party/proxy`) for privacy and security.
Creative HTML is always sanitized. By default, each auction delivery path then
rewrites eligible URLs through the first-party proxy (`/first-party/proxy`). The
`POST /auction` response also injects the creative runtime; the publisher SSAT
inline path uses absolute first-party URLs without injecting that bundle. Setting
`[auction].rewrite_creatives = false` skips rewriting in both paths and runtime
injection on `POST /auction`.

## Route Registration & Endpoints

Expand Down Expand Up @@ -379,7 +385,8 @@ The `/auction` endpoint is the primary entry point for auctions:
**Key Transformations:**
- `adUnits[].code` → `seatbid[].bid[].impid` (slot identifier)
- `mediaTypes.banner.sizes` → evaluated by providers, winning size in `bid.w` and `bid.h`
- Creative HTML is rewritten to use `/first-party/proxy` URLs
- Creative HTML is always sanitized, then rewritten to use `/first-party/proxy` URLs by default
- `[auction].rewrite_creatives = false` skips rewriting in both delivery paths and `POST /auction` creative runtime injection, not sanitization
- Multiple bids per slot become separate `seatbid` entries
- Orchestrator metadata added in `ext.orchestrator`

Expand Down
7 changes: 4 additions & 3 deletions crates/trusted-server-core/src/auction/endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,10 @@ const MAX_AUCTION_BODY_SIZE: usize = 256 * 1024;
/// ## Response
///
/// Returns an `OpenRTB 2.x` response. Creative HTML is inlined in each bid's
/// `adm` field after sanitisation and first-party URL rewriting. Response
/// headers include `X-TS-EC` (the caller's Edge Cookie ID) and
/// `X-TS-EC-Fresh` (a freshly generated ID for cookie renewal).
/// `adm` field after mandatory server-side sanitization. First-party resource
/// and click URL rewriting plus creative TSJS injection are enabled by default;
/// setting [`auction.rewrite_creatives`][`crate::auction_config_types::AuctionConfig::rewrite_creatives`]
/// to `false` skips only that rewrite pass.
///
/// ## Scroll, refresh, and SPA navigation
///
Expand Down
Loading
Loading