diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bc49c80..fddc8009 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Added the default-true `[auction].rewrite_creatives` option. Setting it to `false` preserves mandatory `/auction` creative sanitization while skipping first-party resource/click URL rewriting and creative TSJS injection. - 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 diff --git a/crates/trusted-server-cli/tests/config_env_overlay.rs b/crates/trusted-server-cli/tests/config_env_overlay.rs new file mode 100644 index 00000000..380ccb6b --- /dev/null +++ b/crates/trusted-server-cli/tests/config_env_overlay.rs @@ -0,0 +1,122 @@ +//! 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::() + .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) + .env(REWRITE_ENV, raw_value) + .output() + .expect("should run ts config validate") +} + +#[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"]) + .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_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}" + ); +} diff --git a/crates/trusted-server-core/src/auction/README.md b/crates/trusted-server-core/src/auction/README.md index dcc9e150..b7b8141d 100644 --- a/crates/trusted-server-core/src/auction/README.md +++ b/crates/trusted-server-core/src/auction/README.md @@ -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) │ └──────────────────────────────────────────────────────────────────────┘ │ @@ -248,7 +249,10 @@ 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, it is then rewritten to use the +first-party proxy (`/first-party/proxy`) and the creative runtime is injected. +Setting `[auction].rewrite_creatives = false` skips only that rewrite and +injection pass. ## Route Registration & Endpoints @@ -379,7 +383,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 and creative runtime injection, not sanitization - Multiple bids per slot become separate `seatbid` entries - Orchestrator metadata added in `ext.orchestrator` diff --git a/crates/trusted-server-core/src/auction/endpoints.rs b/crates/trusted-server-core/src/auction/endpoints.rs index 1b0ced7a..e5796323 100644 --- a/crates/trusted-server-core/src/auction/endpoints.rs +++ b/crates/trusted-server-core/src/auction/endpoints.rs @@ -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 /// diff --git a/crates/trusted-server-core/src/auction/formats.rs b/crates/trusted-server-core/src/auction/formats.rs index 441828a1..71f9a290 100644 --- a/crates/trusted-server-core/src/auction/formats.rs +++ b/crates/trusted-server-core/src/auction/formats.rs @@ -217,7 +217,8 @@ pub fn convert_tsjs_to_auction_request( /// Convert `OrchestrationResult` to `OpenRTB` response format. /// -/// Returns rewritten creative HTML directly in the `adm` field for inline delivery. +/// Always sanitizes creative HTML in the `adm` field and optionally rewrites it +/// according to the auction configuration. /// /// # Errors /// @@ -250,21 +251,34 @@ pub fn convert_to_openrtb_response( let width = to_openrtb_i32(bid.width, "width", &bid_context); let height = to_openrtb_i32(bid.height, "height", &bid_context); - // Process creative HTML if present - — sanitize dangerous markup first, then rewrite URLs. + // Process creative HTML if present — always sanitize dangerous markup first. let creative_html = if let Some(ref raw_creative) = bid.creative { let sanitized = creative::sanitize_creative_html(raw_creative); - let rewritten = creative::rewrite_creative_html(settings, &sanitized); + let sanitized_len = sanitized.len(); + let rewrite_creatives = settings.auction.rewrite_creatives; + let processed = if rewrite_creatives { + creative::rewrite_creative_html(settings, &sanitized) + } else { + sanitized + }; + let rewrite_mode = if rewrite_creatives { + "enabled" + } else { + "disabled" + }; log::debug!( - "Processed creative for auction {} slot {} ({} → {} → {} bytes)", + "Processed creative for auction {} slot {} bidder {} (rewrite {}, raw {} bytes, sanitized {} bytes, output {} bytes)", auction_request.id, slot_id, + bid.bidder, + rewrite_mode, raw_creative.len(), - sanitized.len(), - rewritten.len() + sanitized_len, + processed.len() ); - rewritten + processed } else { // No creative provided (e.g., from mediation layer that returns iframe URLs) log::warn!( @@ -445,6 +459,15 @@ mod tests { } } + fn make_complete_creative_bid() -> Bid { + let mut bid = make_bid("div-gpt-top", "appnexus", Some(2.75)); + bid.creative = Some( + r#""# + .to_string(), + ); + bid + } + fn make_result(bid: Bid) -> OrchestrationResult { OrchestrationResult { provider_responses: vec![AuctionResponse { @@ -466,6 +489,13 @@ mod tests { .expect("should parse JSON response") } + fn response_adm(response: Response) -> String { + response_json(response)["seatbid"][0]["bid"][0]["adm"] + .as_str() + .expect("should serialize adm as a string") + .to_string() + } + fn make_banner_body(config: Option) -> AdRequest { AdRequest { ad_units: vec![AdUnit { @@ -932,6 +962,103 @@ mod tests { ); } + #[test] + fn convert_to_openrtb_response_rewrites_sanitized_creative_by_default() { + let settings = make_settings(); + let auction_request = make_auction_request(); + let result = make_result(make_complete_creative_bid()); + + let response = convert_to_openrtb_response(&result, &settings, &auction_request, false) + .expect("should convert creative with rewriting enabled"); + let adm = response_adm(response); + + assert!( + adm.matches("/first-party/proxy?tsurl=").count() >= 2, + "should rewrite image and inline CSS URLs through the proxy: {adm}" + ); + assert!( + adm.contains("/first-party/click?tsurl="), + "should rewrite click URLs: {adm}" + ); + assert!( + adm.contains("data-tsclick"), + "should add the click guard attribute: {adm}" + ); + assert!( + adm.contains("tsjs-unified.min.js"), + "should inject the unified creative runtime: {adm}" + ); + assert!( + !adm.contains(r#"src="https://cdn.example.com/ad.png""#), + "should not retain the image URL as a direct attribute: {adm}" + ); + assert!( + !adm.contains(r#"href="https://advertiser.example.com/landing""#), + "should not retain the click URL as a direct attribute: {adm}" + ); + assert!( + !adm.contains("url(https://styles.example.com/bg.png)"), + "should not retain the CSS URL as a direct value: {adm}" + ); + assert!( + !adm.contains("auction-script-marker"), + "should remove malicious script content before rewriting: {adm}" + ); + assert!( + !adm.contains("auction-handler-marker") && !adm.contains("onerror"), + "should remove event handlers before rewriting: {adm}" + ); + } + + #[test] + fn convert_to_openrtb_response_can_skip_rewriting_but_not_sanitization() { + let mut settings = make_settings(); + settings.auction.rewrite_creatives = false; + let auction_request = make_auction_request(); + let result = make_result(make_complete_creative_bid()); + + let response = convert_to_openrtb_response(&result, &settings, &auction_request, false) + .expect("should convert creative with rewriting disabled"); + let adm = response_adm(response); + + assert!( + adm.contains(r#"src="https://cdn.example.com/ad.png""#), + "should retain the sanitizer-accepted image URL: {adm}" + ); + assert!( + adm.contains(r#"href="https://advertiser.example.com/landing""#), + "should retain the sanitizer-accepted click URL: {adm}" + ); + assert!( + adm.contains("url(https://styles.example.com/bg.png)"), + "should retain the sanitizer-accepted CSS URL: {adm}" + ); + assert!( + !adm.contains("/first-party/proxy"), + "should not rewrite resource URLs: {adm}" + ); + assert!( + !adm.contains("/first-party/click"), + "should not rewrite click URLs: {adm}" + ); + assert!( + !adm.contains("data-tsclick"), + "should not add the click guard attribute: {adm}" + ); + assert!( + !adm.contains("tsjs-unified.min.js"), + "should not inject the unified creative runtime: {adm}" + ); + assert!( + !adm.contains("auction-script-marker"), + "should still remove malicious script content: {adm}" + ); + assert!( + !adm.contains("auction-handler-marker") && !adm.contains("onerror"), + "should still remove event handlers: {adm}" + ); + } + #[test] fn convert_to_openrtb_response_serializes_missing_creative_as_empty_adm() { let settings = make_settings(); diff --git a/crates/trusted-server-core/src/auction/orchestrator.rs b/crates/trusted-server-core/src/auction/orchestrator.rs index bee63856..b018518d 100644 --- a/crates/trusted-server-core/src/auction/orchestrator.rs +++ b/crates/trusted-server-core/src/auction/orchestrator.rs @@ -1823,6 +1823,7 @@ mod tests { futures::executor::block_on(async { let config = AuctionConfig { enabled: true, + rewrite_creatives: true, providers: vec![], mediator: None, timeout_ms: 2000, diff --git a/crates/trusted-server-core/src/auction_config_types.rs b/crates/trusted-server-core/src/auction_config_types.rs index 3bd747f6..eb93adbd 100644 --- a/crates/trusted-server-core/src/auction_config_types.rs +++ b/crates/trusted-server-core/src/auction_config_types.rs @@ -11,6 +11,13 @@ pub struct AuctionConfig { #[serde(default)] pub enabled: bool, + /// Rewrite sanitized winning-bid creative HTML to first-party endpoints. + #[serde( + default = "default_rewrite_creatives", + skip_serializing_if = "is_default_rewrite_creatives" + )] + pub rewrite_creatives: bool, + /// Provider names that participate in bidding /// Simply list the provider names (e.g., ["prebid", "aps"]) #[serde(default, deserialize_with = "crate::settings::vec_from_seq_or_map")] @@ -41,6 +48,7 @@ impl Default for AuctionConfig { fn default() -> Self { Self { enabled: false, + rewrite_creatives: default_rewrite_creatives(), providers: Vec::new(), mediator: None, timeout_ms: default_timeout(), @@ -54,6 +62,14 @@ fn default_timeout() -> u32 { 2000 } +fn default_rewrite_creatives() -> bool { + true +} + +fn is_default_rewrite_creatives(value: &bool) -> bool { + *value == default_rewrite_creatives() +} + fn default_creative_store() -> String { "creative_store".to_owned() } @@ -79,3 +95,45 @@ impl AuctionConfig { self.mediator.is_some() } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn rewrite_creatives_defaults_to_true() { + let config: AuctionConfig = + serde_json::from_value(serde_json::json!({})).expect("should deserialize defaults"); + + assert!( + config.rewrite_creatives, + "should enable creative rewriting by default" + ); + } + + #[test] + fn default_rewrite_creatives_is_not_serialized() { + let serialized = + serde_json::to_value(AuctionConfig::default()).expect("should serialize defaults"); + + assert!( + serialized.get("rewrite_creatives").is_none(), + "should omit the default rewrite setting" + ); + } + + #[test] + fn disabled_rewrite_creatives_is_serialized() { + let config = AuctionConfig { + rewrite_creatives: false, + ..AuctionConfig::default() + }; + let serialized = serde_json::to_value(config).expect("should serialize disabled rewriting"); + + assert_eq!( + serialized.get("rewrite_creatives"), + Some(&serde_json::Value::Bool(false)), + "should preserve an explicit rewrite opt-out" + ); + } +} diff --git a/crates/trusted-server-core/src/config_payload.rs b/crates/trusted-server-core/src/config_payload.rs index dd0b3533..f7ae531e 100644 --- a/crates/trusted-server-core/src/config_payload.rs +++ b/crates/trusted-server-core/src/config_payload.rs @@ -46,6 +46,24 @@ mod tests { use super::*; use crate::redacted::Redacted; use crate::test_support::tests::crate_test_settings_str; + use serde::Deserialize; + + #[derive(Deserialize)] + #[serde(deny_unknown_fields)] + struct LegacyAuctionConfig { + #[serde(rename = "enabled")] + _enabled: bool, + #[serde(rename = "providers")] + _providers: Vec, + #[serde(rename = "mediator")] + _mediator: Option, + #[serde(rename = "timeout_ms")] + _timeout_ms: u32, + #[serde(rename = "creative_store")] + _creative_store: String, + #[serde(rename = "allowed_context_keys")] + _allowed_context_keys: std::collections::HashSet, + } fn test_settings() -> Settings { Settings::from_toml(&crate_test_settings_str()).expect("should parse test settings") @@ -78,6 +96,57 @@ mod tests { ); } + #[test] + fn legacy_blob_without_rewrite_creatives_preserves_rewriting() { + let data = + serde_json::to_value(test_settings()).expect("should serialize settings to JSON"); + let auction = data + .get("auction") + .and_then(serde_json::Value::as_object) + .expect("should serialize auction settings as an object"); + assert!( + !auction.contains_key("rewrite_creatives"), + "should omit the default rewrite setting from the payload" + ); + let envelope = BlobEnvelope::new(data, "2026-01-01T00:00:00Z".to_string()); + let envelope_json = serde_json::to_string(&envelope).expect("should serialize envelope"); + + let reconstructed = + settings_from_config_blob(&envelope_json).expect("should reconstruct legacy settings"); + + assert!( + reconstructed.auction.rewrite_creatives, + "should enable creative rewriting for legacy blobs" + ); + } + + #[test] + fn default_auction_payload_is_accepted_by_legacy_schema() { + let data = + serde_json::to_value(test_settings()).expect("should serialize settings to JSON"); + let auction = data + .get("auction") + .cloned() + .expect("should serialize auction settings"); + + serde_json::from_value::(auction) + .expect("should deserialize the default payload with the legacy schema"); + } + + #[test] + fn disabled_rewrite_creatives_survives_blob_round_trip() { + let mut original = test_settings(); + original.auction.rewrite_creatives = false; + + let reconstructed = settings_from_config_blob(&envelope_json(&original)) + .expect("should reconstruct disabled rewriting"); + + assert!( + !reconstructed.auction.rewrite_creatives, + "should preserve the explicit rewrite opt-out" + ); + } + #[test] fn strings_that_look_like_json_scalars_round_trip_as_strings() { let mut original = test_settings(); diff --git a/crates/trusted-server-core/src/creative.rs b/crates/trusted-server-core/src/creative.rs index 63a6d93c..584e53f8 100644 --- a/crates/trusted-server-core/src/creative.rs +++ b/crates/trusted-server-core/src/creative.rs @@ -56,30 +56,21 @@ pub(super) fn to_abs(settings: &Settings, u: &str) -> Option { return None; } - // Skip if excluded from rewrites in settings - if settings.rewrite.is_excluded(t) { + let lower = t.to_ascii_lowercase(); + let absolute = if t.starts_with("//") { + format!("https:{t}") + } else if lower.starts_with("http://") || lower.starts_with("https://") { + t.to_owned() + } else { return None; - } + }; - // Skip non-network schemes commonly found in creatives - let lower = t.to_ascii_lowercase(); - if lower.starts_with("data:") - || lower.starts_with("javascript:") - || lower.starts_with("mailto:") - || lower.starts_with("tel:") - || lower.starts_with("blob:") - || lower.starts_with("about:") - { + // Match exclusions against the same absolute URL used for rewriting. + if settings.rewrite.is_excluded(&absolute) { return None; } - if t.starts_with("//") { - Some(format!("https:{t}")) - } else if lower.starts_with("http://") || lower.starts_with("https://") { - Some(t.to_owned()) - } else { - None - } + Some(absolute) } // Helper: rewrite url(...) occurrences inside a CSS style string to first-party proxy. @@ -1323,11 +1314,22 @@ mod tests { None ); + assert_eq!( + to_abs(&settings, "//trusted-cdn.example.com/lib.js"), + None, + "should exclude a protocol-relative URL by exact domain" + ); + // Non-excluded domain should return Some assert_eq!( to_abs(&settings, "https://other-cdn.example.com/lib.js"), Some("https://other-cdn.example.com/lib.js".to_owned()) ); + assert_eq!( + to_abs(&settings, "//other-cdn.example.com/lib.js"), + Some("https://other-cdn.example.com/lib.js".to_owned()), + "should normalize a non-excluded protocol-relative URL" + ); } #[test] @@ -1343,6 +1345,16 @@ mod tests { to_abs(&settings, "https://cdnjs.cloudflare.com/lib.js"), None ); + assert_eq!( + to_abs(&settings, "//cloudflare.com/cdn.js"), + None, + "should exclude a protocol-relative wildcard base domain" + ); + assert_eq!( + to_abs(&settings, "//cdnjs.cloudflare.com/lib.js"), + None, + "should exclude a protocol-relative wildcard subdomain" + ); // Should not exclude different domain assert_eq!( @@ -1358,6 +1370,7 @@ mod tests { let html = r#" + "#; @@ -1366,6 +1379,11 @@ mod tests { // Excluded domain should NOT be rewritten assert!(out.contains(r#"src="https://trusted-cdn.example.com/logo.png"#)); + assert!( + out.contains(r#"src="//trusted-cdn.example.com/protocol-relative.png""#), + "excluded protocol-relative URL should remain direct: {out}" + ); + // Non-excluded domain SHOULD be rewritten assert!(out.contains("/first-party/proxy?tsurl=")); assert!(out.contains("other-cdn.example.com")); diff --git a/crates/trusted-server-core/src/proxy.rs b/crates/trusted-server-core/src/proxy.rs index 9e03f4db..9bc71fb2 100644 --- a/crates/trusted-server-core/src/proxy.rs +++ b/crates/trusted-server-core/src/proxy.rs @@ -2882,6 +2882,51 @@ mod tests { assert_eq!(ct, "text/css; charset=utf-8"); } + #[test] + fn auction_rewrite_setting_does_not_change_proxied_html_or_css_rewriting() { + let mut settings = create_test_settings(); + settings.auction.rewrite_creatives = false; + let req = build_http_request(Method::GET, "https://edge.example/first-party/proxy"); + + let html = r#""#; + let mut html_response = build_http_response(StatusCode::OK, EdgeBody::from(html)); + html_response.headers_mut().insert( + header::CONTENT_TYPE, + HeaderValue::from_static("text/html; charset=utf-8"), + ); + let html_output = finalize( + &settings, + &req, + "https://cdn.example/creative.html", + html_response, + ) + .expect("should finalize proxied HTML"); + let html_body = response_body_string(html_output); + + let css = "body{background:url(https://cdn.example/bg.png)}"; + let mut css_response = build_http_response(StatusCode::OK, EdgeBody::from(css)); + css_response + .headers_mut() + .insert(header::CONTENT_TYPE, HeaderValue::from_static("text/css")); + let css_output = finalize( + &settings, + &req, + "https://cdn.example/creative.css", + css_response, + ) + .expect("should finalize proxied CSS"); + let css_body = response_body_string(css_output); + + assert!( + html_body.contains("/first-party/proxy?tsurl="), + "should keep rewriting proxied HTML when auction rewriting is disabled: {html_body}" + ); + assert!( + css_body.contains("/first-party/proxy?tsurl="), + "should keep rewriting proxied CSS when auction rewriting is disabled: {css_body}" + ); + } + #[test] fn html_response_rewrite_preserves_non_standard_port() { // Verify that HTML rewriting preserves non-standard ports in sub-resource URLs. diff --git a/crates/trusted-server-core/src/settings.rs b/crates/trusted-server-core/src/settings.rs index c49e9968..bdba093b 100644 --- a/crates/trusted-server-core/src/settings.rs +++ b/crates/trusted-server-core/src/settings.rs @@ -4379,6 +4379,41 @@ origin_host_header_overide = "www.example.com""#, assert!(!rewrite.is_excluded("")); } + #[test] + fn test_auction_rewrite_creatives_defaults_to_true_when_omitted() { + let toml_str = crate_test_settings_str() + + r#" + [auction] + enabled = true + providers = [] + "#; + + let settings = Settings::from_toml(&toml_str).expect("should parse valid TOML"); + + assert!( + settings.auction.rewrite_creatives, + "should preserve creative rewriting when the setting is omitted" + ); + } + + #[test] + fn test_auction_rewrite_creatives_accepts_explicit_false() { + let toml_str = crate_test_settings_str() + + r#" + [auction] + enabled = true + providers = [] + rewrite_creatives = false + "#; + + let settings = Settings::from_toml(&toml_str).expect("should parse valid TOML"); + + assert!( + !settings.auction.rewrite_creatives, + "should disable creative rewriting when explicitly configured" + ); + } + #[test] fn test_auction_allowed_context_keys_defaults_to_empty() { let settings = create_test_settings(); diff --git a/docs/guide/auction-orchestration.md b/docs/guide/auction-orchestration.md index d7595881..4b9a7434 100644 --- a/docs/guide/auction-orchestration.md +++ b/docs/guide/auction-orchestration.md @@ -12,7 +12,7 @@ Key capabilities: - **Strategy-based winner selection** — Automatic strategy detection based on configuration - **Mediator support** — Optional external mediator for decoding encoded prices (e.g., APS) and applying unified floor pricing - **Provider abstraction** — Pluggable provider interface for adding new demand sources -- **Creative rewriting** — Winning creatives automatically rewritten with first-party proxy URLs +- **Creative rewriting** — Winning creatives are sanitized and rewritten with first-party proxy URLs by default ## System Flow (Prebid + APS) @@ -147,7 +147,7 @@ sequenceDiagram Note over Client,Mock: Response Assembly activate TS activate Client - Orch->>Orch: Transform to OpenRTB response
Generate iframe creatives
Rewrite creative URLs
Add orchestrator metadata + Orch->>Orch: Transform to OpenRTB response
Sanitize creative HTML
Optionally rewrite creative URLs
Add orchestrator metadata Orch-->>TS: OpenRTB BidResponse Note right of Orch: { "id": "auction-response",
"seatbid": [{ "seat": "amazon-aps",
"bid": [{ "price": 2.50,
"adm": "