diff --git a/crates/common/README.md b/crates/common/README.md index 2dd2423..4fa5fad 100644 --- a/crates/common/README.md +++ b/crates/common/README.md @@ -51,8 +51,8 @@ Behavior is covered by an extensive test suite in `crates/common/src/creative.rs - `synthetic.rs` generates a deterministic synthetic identifier per user request and exposes helpers: - `generate_synthetic_id` — creates a fresh HMAC-based ID using request signals. - - `get_synthetic_id` — extracts an existing ID from the `X-Synthetic-Trusted-Server` header or `synthetic_id` cookie. + - `get_synthetic_id` — extracts an existing ID from the `x-psid-ts` header or `synthetic_id` cookie. - `get_or_generate_synthetic_id` — reuses the existing ID when present, otherwise creates one. -- `publisher.rs::handle_publisher_request` stamps proxied origin responses with `X-Synthetic-Fresh`, `X-Synthetic-Trusted-Server`, and (when absent) issues the `synthetic_id` cookie so the browser keeps the identifier on subsequent requests. +- `publisher.rs::handle_publisher_request` stamps proxied origin responses with `X-Synthetic-Fresh`, `x-psid-ts`, and (when absent) issues the `synthetic_id` cookie so the browser keeps the identifier on subsequent requests. - `proxy.rs::handle_first_party_proxy` replays the identifier to third-party creative origins by appending `synthetic_id=` to the reconstructed target URL, follows redirects (301/302/303/307/308) up to four hops, and keeps downstream fetches linked to the same user scope. - `proxy.rs::handle_first_party_click` adds `synthetic_id=` to outbound click redirect URLs so analytics endpoints can associate clicks with impressions without third-party cookies. diff --git a/crates/common/build.rs b/crates/common/build.rs index d7b020b..492364b 100644 --- a/crates/common/build.rs +++ b/crates/common/build.rs @@ -14,6 +14,7 @@ const TRUSTED_SERVER_OUTPUT_CONFIG_PATH: &str = "../../target/trusted-server-out fn main() { merge_toml(); + generate_header_constants(); rerun_if_changed(); } @@ -57,6 +58,37 @@ fn merge_toml() { fs::write(dest_path, merged_toml).unwrap_or_else(|_| panic!("Failed to write {:?}", dest_path)); } +fn generate_header_constants() { + // Read and parse the config + let init_config_path = Path::new(TRUSTED_SERVER_INIT_CONFIG_PATH); + let toml_content = fs::read_to_string(init_config_path) + .unwrap_or_else(|_| panic!("Failed to read {:?}", init_config_path)); + + let settings = settings::Settings::from_toml(&toml_content) + .expect("Failed to parse settings at build time"); + + // Generate the header name based on psid + let psid = &settings.publisher.psid; + let header_name = format!("x-{}-ts", psid); + + // Generate Rust code for the constant + let generated_code = format!( + r#"// This file is auto-generated by build.rs - DO NOT EDIT + +pub const HEADER_SYNTHETIC_TRUSTED_SERVER: HeaderName = HeaderName::from_static("{}"); +"#, + header_name + ); + + // Write to OUT_DIR + let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR not set"); + let dest_path = Path::new(&out_dir).join("generated_header_constants.rs"); + fs::write(&dest_path, generated_code) + .unwrap_or_else(|_| panic!("Failed to write {:?}", dest_path)); + + println!("cargo:warning=Generated header constant: {}", header_name); +} + fn collect_env_vars(value: &Value, env_vars: &mut HashSet, path: Vec) { if let Value::Object(map) = value { for (key, val) in map { diff --git a/crates/common/src/constants.rs b/crates/common/src/constants.rs index 9715728..14db7d1 100644 --- a/crates/common/src/constants.rs +++ b/crates/common/src/constants.rs @@ -3,8 +3,9 @@ use http::header::HeaderName; pub const HEADER_SYNTHETIC_FRESH: HeaderName = HeaderName::from_static("x-synthetic-fresh"); pub const HEADER_SYNTHETIC_PUB_USER_ID: HeaderName = HeaderName::from_static("x-pub-user-id"); pub const HEADER_X_PUB_USER_ID: HeaderName = HeaderName::from_static("x-pub-user-id"); -pub const HEADER_SYNTHETIC_TRUSTED_SERVER: HeaderName = - HeaderName::from_static("x-synthetic-trusted-server"); + +// Generated at build time from publisher.id_prefix in trusted-server.toml +include!(concat!(env!("OUT_DIR"), "/generated_header_constants.rs")); pub const HEADER_X_CONSENT_ADVERTISING: HeaderName = HeaderName::from_static("x-consent-advertising"); pub const HEADER_X_FORWARDED_FOR: HeaderName = HeaderName::from_static("x-forwarded-for"); diff --git a/crates/common/src/gam.rs b/crates/common/src/gam.rs index 04066fe..2680cd2 100644 --- a/crates/common/src/gam.rs +++ b/crates/common/src/gam.rs @@ -6,6 +6,7 @@ use std::collections::HashMap; use std::io::Read; use uuid::Uuid; +use crate::constants::HEADER_SYNTHETIC_TRUSTED_SERVER; use crate::error::TrustedServerError; use crate::gdpr::get_consent_from_request; use crate::settings::Settings; @@ -35,7 +36,7 @@ impl GamRequest { // Get synthetic ID from request headers let synthetic_id = req - .get_header("X-Synthetic-Trusted-Server") + .get_header(HEADER_SYNTHETIC_TRUSTED_SERVER) .and_then(|h| h.to_str().ok()) .unwrap_or("unknown") .to_string(); @@ -1077,12 +1078,14 @@ mod tests { use super::*; use serde_json::json; - use crate::test_support::tests::create_test_settings; + use crate::{ + constants::HEADER_SYNTHETIC_TRUSTED_SERVER, test_support::tests::create_test_settings, + }; fn create_test_request() -> Request { let mut req = Request::new(Method::GET, "https://example.com/test"); req.set_header(header::USER_AGENT, "Mozilla/5.0 Test Browser"); - req.set_header("X-Synthetic-Trusted-Server", "test-synthetic-id-123"); + req.set_header(HEADER_SYNTHETIC_TRUSTED_SERVER, "test-synthetic-id-123"); req } @@ -1171,7 +1174,7 @@ mod tests { Method::GET, "https://example.com/test?param=value&special=test%20space", ); - req.set_header("X-Synthetic-Trusted-Server", "test-id"); + req.set_header(HEADER_SYNTHETIC_TRUSTED_SERVER, "test-id"); let gam_req = GamRequest::new(&settings, &req).unwrap(); let url = gam_req.build_golden_url(); @@ -1215,7 +1218,7 @@ mod tests { let settings = create_test_settings(); let mut req = Request::new(Method::GET, "https://example.com/gam-test"); req.set_header("X-Consent-Advertising", "true"); - req.set_header("X-Synthetic-Trusted-Server", "test-synthetic-id"); + req.set_header(HEADER_SYNTHETIC_TRUSTED_SERVER, "test-synthetic-id"); // Note: This test will fail when actually sending to GAM backend // In a real test environment, we'd mock the backend response diff --git a/crates/common/src/publisher.rs b/crates/common/src/publisher.rs index 6fda0a3..d54e6b0 100644 --- a/crates/common/src/publisher.rs +++ b/crates/common/src/publisher.rs @@ -126,7 +126,7 @@ pub fn handle_main_page( let fresh_id = generate_synthetic_id(settings, &req)?; // Check for existing Trusted Server ID in this specific order: - // 1. X-Synthetic-Trusted-Server header + // 1. x-psid-ts header // 2. Cookie // 3. Fall back to fresh ID let synthetic_id = get_or_generate_synthetic_id(settings, &req)?; diff --git a/crates/common/src/settings.rs b/crates/common/src/settings.rs index 3b59a1f..5d54f41 100644 --- a/crates/common/src/settings.rs +++ b/crates/common/src/settings.rs @@ -25,6 +25,14 @@ pub struct Publisher { /// Secret used to encrypt/decrypt proxied URLs in `/first-party/proxy`. /// Keep this secret stable to allow existing links to decode. pub proxy_secret: String, + /// Prefix for the publisher-specific ID header (x-{psid}-ts). + /// Defaults to "psid" if not specified. + #[serde(default = "default_psid")] + pub psid: String, +} + +fn default_psid() -> String { + "psid".to_string() } impl Publisher { @@ -530,6 +538,7 @@ mod tests { cookie_domain: ".example.com".to_string(), origin_url: "https://origin.example.com:8080".to_string(), proxy_secret: "test-secret".to_string(), + psid: "psid".to_string(), }; assert_eq!(publisher.origin_host(), "origin.example.com:8080"); @@ -539,6 +548,7 @@ mod tests { cookie_domain: ".example.com".to_string(), origin_url: "https://origin.example.com".to_string(), proxy_secret: "test-secret".to_string(), + psid: "psid".to_string(), }; assert_eq!(publisher.origin_host(), "origin.example.com"); @@ -548,6 +558,7 @@ mod tests { cookie_domain: ".example.com".to_string(), origin_url: "http://localhost:9090".to_string(), proxy_secret: "test-secret".to_string(), + psid: "psid".to_string(), }; assert_eq!(publisher.origin_host(), "localhost:9090"); @@ -557,6 +568,7 @@ mod tests { cookie_domain: ".example.com".to_string(), origin_url: "localhost:9090".to_string(), proxy_secret: "test-secret".to_string(), + psid: "psid".to_string(), }; assert_eq!(publisher.origin_host(), "localhost:9090"); @@ -566,6 +578,7 @@ mod tests { cookie_domain: ".example.com".to_string(), origin_url: "http://192.168.1.1:8080".to_string(), proxy_secret: "test-secret".to_string(), + psid: "psid".to_string(), }; assert_eq!(publisher.origin_host(), "192.168.1.1:8080"); @@ -575,6 +588,7 @@ mod tests { cookie_domain: ".example.com".to_string(), origin_url: "http://[::1]:8080".to_string(), proxy_secret: "test-secret".to_string(), + psid: "psid".to_string(), }; assert_eq!(publisher.origin_host(), "[::1]:8080"); } diff --git a/crates/common/src/synthetic.rs b/crates/common/src/synthetic.rs index b62ec8c..d84a95e 100644 --- a/crates/common/src/synthetic.rs +++ b/crates/common/src/synthetic.rs @@ -83,7 +83,7 @@ pub fn generate_synthetic_id( /// Gets or creates a synthetic ID from the request. /// /// Attempts to retrieve an existing synthetic ID from: -/// 1. The `X-Synthetic-Trusted-Server` header +/// 1. The `x-psid-ts` header /// 2. The `synthetic_id` cookie /// /// If neither exists, generates a new synthetic ID. diff --git a/crates/common/src/templates.rs b/crates/common/src/templates.rs index f416ffc..e5d167f 100644 --- a/crates/common/src/templates.rs +++ b/crates/common/src/templates.rs @@ -1208,7 +1208,7 @@ pub const GAM_TEST_TEMPLATE: &str = r#" try { const response = await fetch('/'); const freshId = response.headers.get('X-Synthetic-Fresh'); - const trustedServerId = response.headers.get('X-Synthetic-Trusted-Server'); + const trustedServerId = response.headers.get('x-psid-ts'); const statusDiv = document.getElementById('syntheticStatus'); statusDiv.className = 'status success'; @@ -1280,14 +1280,14 @@ pub const GAM_TEST_TEMPLATE: &str = r#" // First get the main page to ensure we have synthetic IDs const mainResponse = await fetch('/'); const freshId = mainResponse.headers.get('X-Synthetic-Fresh'); - const trustedServerId = mainResponse.headers.get('X-Synthetic-Trusted-Server'); + const trustedServerId = mainResponse.headers.get('x-psid-ts'); // Now test the GAM request const response = await fetch('/gam-test', { headers: { 'X-Consent-Advertising': 'true', 'X-Synthetic-Fresh': freshId || '', - 'X-Synthetic-Trusted-Server': trustedServerId || '' + 'x-psid-ts': trustedServerId || '' } }); diff --git a/crates/js/README.md b/crates/js/README.md index 86b4c47..cde9ff6 100644 --- a/crates/js/README.md +++ b/crates/js/README.md @@ -111,7 +111,7 @@ The Rust services (`trusted-server-common`) expose several proxy entry points th - Endpoint: `handle_publisher_request` (`crates/common/src/publisher.rs`). - Retrieves or generates the trusted synthetic identifier before Fastly consumes the request body. -- Always stamps the proxied response with `X-Synthetic-Fresh` and `X-Synthetic-Trusted-Server` headers and, when the browser does not already present one, sets the `synthetic_id=` cookie (Secure + SameSite=Lax) bound to the configured publisher domain. +- Always stamps the proxied response with `X-Synthetic-Fresh` and `x-psid-ts` headers and, when the browser does not already present one, sets the `synthetic_id=` cookie (Secure + SameSite=Lax) bound to the configured publisher domain. - Result: downstream assets fetched through the same first-party origin automatically include the synthetic ID header/cookie so subsequent proxy layers can read it. ### Creative Asset Proxy diff --git a/trusted-server.toml b/trusted-server.toml index 73455f8..45f6542 100644 --- a/trusted-server.toml +++ b/trusted-server.toml @@ -7,6 +7,7 @@ domain = "test-publisher.com" cookie_domain = ".test-publisher.com" origin_url = "https://origin.test-publisher.com" proxy_secret = "change-me-proxy-secret" +psid = "psid" [prebid] server_url = "http://68.183.113.79:8000"