diff --git a/SEQUENCE.md b/SEQUENCE.md index 235ef0e..c4bc28d 100644 --- a/SEQUENCE.md +++ b/SEQUENCE.md @@ -1,4 +1,4 @@ -# 🛡️ Trusted Server — First-Party Proxying Flow +# 🛡️ Trusted Server — Proxying Flow ## 🔄 System Flow Diagram @@ -80,7 +80,7 @@ sequenceDiagram activate TS activate PBS activate DSP - JS->>TS: GET /first-party/ad
(with signals) + JS->>TS: GET /ad/render
(with signals) TS->>PBS: POST /openrtb2/auction
(OpenRTB 2.x) PBS->>DSP: POST bid request DSP-->>PBS: 200 bid response diff --git a/crates/common/README.md b/crates/common/README.md index 4fa5fad..50ad8bc 100644 --- a/crates/common/README.md +++ b/crates/common/README.md @@ -1,6 +1,6 @@ # trusted-server-common -Utilities shared by Trusted Server components. This crate contains HTML/CSS rewriting helpers used to normalize ad creative assets to first‑party proxy endpoints. +Utilities shared by Trusted Server components. This crate contains HTML/CSS rewriting helpers used to normalize ad creative assets to proxy endpoints. ## Creative Rewriting diff --git a/crates/common/src/html_processor.rs b/crates/common/src/html_processor.rs index 1803436..ca9ade0 100644 --- a/crates/common/src/html_processor.rs +++ b/crates/common/src/html_processor.rs @@ -1,4 +1,4 @@ -//! Simplified HTML processor that combines URL replacement and Prebid injection +//! Simplified HTML processor that combines URL replacement and integration injection //! //! This module provides a StreamProcessor implementation for HTML content. use std::cell::Cell; @@ -189,10 +189,23 @@ pub fn create_html_processor(config: HtmlProcessorConfig) -> impl StreamProcesso // Inject unified tsjs bundle once at the start of element!("head", { let injected_tsjs = injected_tsjs.clone(); + let integrations = integration_registry.clone(); + let patterns = patterns.clone(); + let document_state = document_state.clone(); move |el| { if !injected_tsjs.get() { - let loader = tsjs::unified_script_tag(); - el.prepend(&loader, ContentType::Html); + let mut snippet = String::new(); + let ctx = IntegrationHtmlContext { + request_host: &patterns.request_host, + request_scheme: &patterns.request_scheme, + origin_host: &patterns.origin_host, + document_state: &document_state, + }; + for insert in integrations.head_inserts(&ctx) { + snippet.push_str(&insert); + } + snippet.push_str(&tsjs::unified_script_tag()); + el.prepend(&snippet, ContentType::Html); injected_tsjs.set(true); } Ok(()) @@ -454,8 +467,10 @@ pub fn create_html_processor(config: HtmlProcessorConfig) -> impl StreamProcesso #[cfg(test)] mod tests { use super::*; + use crate::integrations::prebid::{config_script_tag, Mode as PrebidMode}; use crate::integrations::{ AttributeRewriteAction, IntegrationAttributeContext, IntegrationAttributeRewriter, + IntegrationHeadInjector, }; use crate::streaming_processor::{Compression, PipelineConfig, StreamingPipeline}; use crate::test_support::tests::create_test_settings; @@ -582,6 +597,58 @@ mod tests { assert_eq!(config.request_scheme, "https"); } + #[test] + fn injects_tsjs_config_when_mode_set() { + struct ModeInjector; + + impl IntegrationHeadInjector for ModeInjector { + fn integration_id(&self) -> &'static str { + "mode-injector" + } + + fn head_inserts(&self, _ctx: &IntegrationHtmlContext<'_>) -> Vec { + vec![config_script_tag(PrebidMode::Auction)] + } + } + + let mut config = create_test_config(); + config.integrations = IntegrationRegistry::from_rewriters_with_head_injectors( + Vec::new(), + Vec::new(), + vec![Arc::new(ModeInjector)], + ); + let processor = create_html_processor(config); + + let pipeline_config = PipelineConfig { + input_compression: Compression::None, + output_compression: Compression::None, + chunk_size: 8192, + }; + let mut pipeline = StreamingPipeline::new(pipeline_config, processor); + + let html = ""; + let mut output = Vec::new(); + pipeline + .process(Cursor::new(html.as_bytes()), &mut output) + .unwrap(); + let result = String::from_utf8(output).unwrap(); + + let config_pos = result.find("setConfig({mode:\"auction\"})"); + let script_pos = result.find("trustedserver-js"); + assert!( + config_pos.is_some(), + "should inject tsjs mode config when configured" + ); + assert!( + script_pos.is_some(), + "should inject unified tsjs script when processing HTML" + ); + assert!( + config_pos.unwrap() < script_pos.unwrap(), + "should place tsjs config before the unified bundle" + ); + } + #[test] fn test_real_publisher_html() { // Test with publisher HTML from test_publisher.html diff --git a/crates/common/src/integrations/mod.rs b/crates/common/src/integrations/mod.rs index 076d5cb..b758e8e 100644 --- a/crates/common/src/integrations/mod.rs +++ b/crates/common/src/integrations/mod.rs @@ -13,9 +13,9 @@ pub mod testlight; pub use registry::{ AttributeRewriteAction, AttributeRewriteOutcome, IntegrationAttributeContext, IntegrationAttributeRewriter, IntegrationDocumentState, IntegrationEndpoint, - IntegrationHtmlContext, IntegrationHtmlPostProcessor, IntegrationMetadata, IntegrationProxy, - IntegrationRegistration, IntegrationRegistrationBuilder, IntegrationRegistry, - IntegrationScriptContext, IntegrationScriptRewriter, ScriptRewriteAction, + IntegrationHeadInjector, IntegrationHtmlContext, IntegrationHtmlPostProcessor, + IntegrationMetadata, IntegrationProxy, IntegrationRegistration, IntegrationRegistrationBuilder, + IntegrationRegistry, IntegrationScriptContext, IntegrationScriptRewriter, ScriptRewriteAction, }; type IntegrationBuilder = fn(&Settings) -> Option; diff --git a/crates/common/src/integrations/prebid.rs b/crates/common/src/integrations/prebid.rs index 822990c..c2380fe 100644 --- a/crates/common/src/integrations/prebid.rs +++ b/crates/common/src/integrations/prebid.rs @@ -19,7 +19,8 @@ use crate::geo::GeoInfo; use crate::http_util::RequestInfo; use crate::integrations::{ AttributeRewriteAction, IntegrationAttributeContext, IntegrationAttributeRewriter, - IntegrationEndpoint, IntegrationProxy, IntegrationRegistration, + IntegrationEndpoint, IntegrationHeadInjector, IntegrationHtmlContext, IntegrationProxy, + IntegrationRegistration, }; use crate::openrtb::{Banner, Format, Imp, ImpExt, OpenRtbRequest, PrebidImpExt, Site}; use crate::request_signing::RequestSigner; @@ -27,8 +28,26 @@ use crate::settings::{IntegrationConfig, Settings}; use crate::synthetic::{generate_synthetic_id, get_or_generate_synthetic_id}; const PREBID_INTEGRATION_ID: &str = "prebid"; -const ROUTE_FIRST_PARTY_AD: &str = "/first-party/ad"; -const ROUTE_THIRD_PARTY_AD: &str = "/third-party/ad"; +const ROUTE_RENDER: &str = "/ad/render"; +const ROUTE_AUCTION: &str = "/ad/auction"; + +#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum Mode { + Render, + Auction, +} + +pub fn config_script_tag(mode: Mode) -> String { + let mode_value = match mode { + Mode::Render => "render", + Mode::Auction => "auction", + }; + format!( + r#""#, + mode_value + ) +} #[derive(Debug, Clone, Deserialize, Serialize, Validate)] pub struct PrebidIntegrationConfig { @@ -44,6 +63,9 @@ pub struct PrebidIntegrationConfig { pub bidders: Vec, #[serde(default)] pub debug: bool, + /// Optional default mode to enqueue when injecting the unified bundle. + #[serde(default)] + pub mode: Option, /// Patterns to match Prebid script URLs for serving empty JS. /// Supports suffix matching (e.g., "/prebid.min.js" matches any path ending with that) /// and wildcard patterns (e.g., "/static/prebid/*" matches paths under that prefix). @@ -213,7 +235,7 @@ impl PrebidIntegration { } } - async fn handle_third_party_ad( + async fn handle_auction( &self, settings: &Settings, mut req: Request, @@ -224,7 +246,7 @@ impl PrebidIntegration { }, )?; - log::info!("/third-party/ad: received {} adUnits", body.ad_units.len()); + log::info!("/auction: received {} adUnits", body.ad_units.len()); for unit in &body.ad_units { if let Some(mt) = &unit.media_types { if let Some(banner) = &mt.banner { @@ -261,14 +283,14 @@ impl PrebidIntegration { .with_body(body)) } - async fn handle_first_party_ad( + async fn handle_render( &self, settings: &Settings, mut req: Request, ) -> Result> { let url = req.get_url_str(); let parsed = Url::parse(url).change_context(TrustedServerError::Prebid { - message: "Invalid first-party serve-ad URL".to_string(), + message: "Invalid render URL".to_string(), })?; let qp = parsed .query_pairs() @@ -345,7 +367,8 @@ pub fn register(settings: &Settings) -> Option { Some( IntegrationRegistration::builder(PREBID_INTEGRATION_ID) .with_proxy(integration.clone()) - .with_attribute_rewriter(integration) + .with_attribute_rewriter(integration.clone()) + .with_head_injector(integration) .build(), ) } @@ -358,8 +381,8 @@ impl IntegrationProxy for PrebidIntegration { fn routes(&self) -> Vec { let mut routes = vec![ - IntegrationEndpoint::get(ROUTE_FIRST_PARTY_AD), - IntegrationEndpoint::post(ROUTE_THIRD_PARTY_AD), + IntegrationEndpoint::get(ROUTE_RENDER), + IntegrationEndpoint::post(ROUTE_AUCTION), ]; // Register routes for script removal patterns @@ -382,12 +405,8 @@ impl IntegrationProxy for PrebidIntegration { let method = req.get_method().clone(); match method { - Method::GET if path == ROUTE_FIRST_PARTY_AD => { - self.handle_first_party_ad(settings, req).await - } - Method::POST if path == ROUTE_THIRD_PARTY_AD => { - self.handle_third_party_ad(settings, req).await - } + Method::GET if path == ROUTE_RENDER => self.handle_render(settings, req).await, + Method::POST if path == ROUTE_AUCTION => self.handle_auction(settings, req).await, // Serve empty JS for matching script patterns Method::GET if self.matches_script_pattern(&path) => self.handle_script_handler(), _ => Err(Report::new(Self::error(format!( @@ -420,6 +439,19 @@ impl IntegrationAttributeRewriter for PrebidIntegration { } } +impl IntegrationHeadInjector for PrebidIntegration { + fn integration_id(&self) -> &'static str { + PREBID_INTEGRATION_ID + } + + fn head_inserts(&self, _ctx: &IntegrationHtmlContext<'_>) -> Vec { + self.config + .mode + .map(|mode| vec![config_script_tag(mode)]) + .unwrap_or_default() + } +} + fn build_openrtb_from_ts( req: &AdRequest, settings: &Settings, @@ -781,6 +813,7 @@ mod tests { timeout_ms: 1000, bidders: vec!["exampleBidder".to_string()], debug: false, + mode: None, script_patterns: default_script_patterns(), } } @@ -931,8 +964,8 @@ mod tests { let routes = integration.routes(); // Should include the default ad routes - assert!(routes.iter().any(|r| r.path == "/first-party/ad")); - assert!(routes.iter().any(|r| r.path == "/third-party/ad")); + assert!(routes.iter().any(|r| r.path == "/ad/render")); + assert!(routes.iter().any(|r| r.path == "/ad/auction")); // Should include default script removal patterns assert!(routes.iter().any(|r| r.path == "/prebid.js")); @@ -950,7 +983,7 @@ mod tests { let synthetic_id = "synthetic-123"; let fresh_id = "fresh-456"; - let mut req = Request::new(Method::POST, "https://edge.example/third-party/ad"); + let mut req = Request::new(Method::POST, "https://edge.example/auction"); req.set_header("Sec-GPC", "1"); enhance_openrtb_request(&mut request_json, synthetic_id, fresh_id, &settings, &req) @@ -1143,8 +1176,8 @@ server_url = "https://prebid.example" assert_eq!(routes.len(), 6); // Verify ad routes - assert!(routes.iter().any(|r| r.path == "/first-party/ad")); - assert!(routes.iter().any(|r| r.path == "/third-party/ad")); + assert!(routes.iter().any(|r| r.path == "/ad/render")); + assert!(routes.iter().any(|r| r.path == "/ad/auction")); // Verify script pattern routes assert!(routes.iter().any(|r| r.path == "/prebid.js")); @@ -1152,4 +1185,55 @@ server_url = "https://prebid.example" assert!(routes.iter().any(|r| r.path == "/prebidjs.js")); assert!(routes.iter().any(|r| r.path == "/prebidjs.min.js")); } + + #[test] + fn config_script_tag_generates_render_mode() { + let tag = config_script_tag(Mode::Render); + assert!(tag.starts_with("")); + assert!(tag.contains(r#"mode:"render""#)); + assert!(tag.contains("tsjs.setConfig")); + assert!(tag.contains("tsjs.que.push")); + } + + #[test] + fn config_script_tag_generates_auction_mode() { + let tag = config_script_tag(Mode::Auction); + assert!(tag.starts_with("")); + assert!(tag.contains(r#"mode:"auction""#)); + assert!(tag.contains("tsjs.setConfig")); + } + + #[test] + fn head_injector_returns_empty_when_mode_not_set() { + let integration = PrebidIntegration::new(base_config()); + let ctx = IntegrationHtmlContext { + request_host: "pub.example", + request_scheme: "https", + origin_host: "origin.example", + document_state: &Default::default(), + }; + let inserts = integration.head_inserts(&ctx); + assert!( + inserts.is_empty(), + "should not inject config when mode is None" + ); + } + + #[test] + fn head_injector_returns_config_script_when_mode_set() { + let mut config = base_config(); + config.mode = Some(Mode::Auction); + let integration = PrebidIntegration::new(config); + let ctx = IntegrationHtmlContext { + request_host: "pub.example", + request_scheme: "https", + origin_host: "origin.example", + document_state: &Default::default(), + }; + let inserts = integration.head_inserts(&ctx); + assert_eq!(inserts.len(), 1); + assert!(inserts[0].contains(r#"mode:"auction""#)); + } } diff --git a/crates/common/src/integrations/registry.rs b/crates/common/src/integrations/registry.rs index 819890d..734586a 100644 --- a/crates/common/src/integrations/registry.rs +++ b/crates/common/src/integrations/registry.rs @@ -357,6 +357,14 @@ pub trait IntegrationHtmlPostProcessor: Send + Sync { fn post_process(&self, html: &mut String, ctx: &IntegrationHtmlContext<'_>) -> bool; } +/// Trait for integration-provided HTML head injections. +pub trait IntegrationHeadInjector: Send + Sync { + /// Identifier for logging/diagnostics. + fn integration_id(&self) -> &'static str; + /// Return HTML snippets to insert at the start of ``. + fn head_inserts(&self, ctx: &IntegrationHtmlContext<'_>) -> Vec; +} + /// Registration payload returned by integration builders. pub struct IntegrationRegistration { pub integration_id: &'static str, @@ -364,6 +372,7 @@ pub struct IntegrationRegistration { pub attribute_rewriters: Vec>, pub script_rewriters: Vec>, pub html_post_processors: Vec>, + pub head_injectors: Vec>, } impl IntegrationRegistration { @@ -386,6 +395,7 @@ impl IntegrationRegistrationBuilder { attribute_rewriters: Vec::new(), script_rewriters: Vec::new(), html_post_processors: Vec::new(), + head_injectors: Vec::new(), }, } } @@ -420,6 +430,12 @@ impl IntegrationRegistrationBuilder { self } + #[must_use] + pub fn with_head_injector(mut self, injector: Arc) -> Self { + self.registration.head_injectors.push(injector); + self + } + #[must_use] pub fn build(self) -> IntegrationRegistration { self.registration @@ -441,6 +457,7 @@ struct IntegrationRegistryInner { html_rewriters: Vec>, script_rewriters: Vec>, html_post_processors: Vec>, + head_injectors: Vec>, } impl Default for IntegrationRegistryInner { @@ -455,6 +472,7 @@ impl Default for IntegrationRegistryInner { html_rewriters: Vec::new(), script_rewriters: Vec::new(), html_post_processors: Vec::new(), + head_injectors: Vec::new(), } } } @@ -539,6 +557,9 @@ impl IntegrationRegistry { inner .html_post_processors .extend(registration.html_post_processors.into_iter()); + inner + .head_injectors + .extend(registration.head_injectors.into_iter()); } } @@ -622,6 +643,18 @@ impl IntegrationRegistry { self.inner.html_post_processors.clone() } + /// Collect HTML snippets for insertion at the start of ``. + pub fn head_inserts(&self, ctx: &IntegrationHtmlContext<'_>) -> Vec { + let mut inserts = Vec::new(); + for injector in &self.inner.head_injectors { + let mut next = injector.head_inserts(ctx); + if !next.is_empty() { + inserts.append(&mut next); + } + } + inserts + } + /// Provide a snapshot of registered integrations and their hooks. pub fn registered_integrations(&self) -> Vec { let mut map: BTreeMap<&'static str, IntegrationMetadata> = BTreeMap::new(); @@ -668,6 +701,29 @@ impl IntegrationRegistry { html_rewriters: attribute_rewriters, script_rewriters, html_post_processors: Vec::new(), + head_injectors: Vec::new(), + }), + } + } + + #[cfg(test)] + pub fn from_rewriters_with_head_injectors( + attribute_rewriters: Vec>, + script_rewriters: Vec>, + head_injectors: Vec>, + ) -> Self { + Self { + inner: Arc::new(IntegrationRegistryInner { + get_router: Router::new(), + post_router: Router::new(), + put_router: Router::new(), + delete_router: Router::new(), + patch_router: Router::new(), + routes: Vec::new(), + html_rewriters: attribute_rewriters, + script_rewriters, + html_post_processors: Vec::new(), + head_injectors, }), } } @@ -711,6 +767,7 @@ impl IntegrationRegistry { html_rewriters: Vec::new(), script_rewriters: Vec::new(), html_post_processors: Vec::new(), + head_injectors: Vec::new(), }), } } diff --git a/crates/js/lib/src/core/config.ts b/crates/js/lib/src/core/config.ts index c06f081..918af65 100644 --- a/crates/js/lib/src/core/config.ts +++ b/crates/js/lib/src/core/config.ts @@ -3,7 +3,7 @@ import { log, LogLevel } from './log'; import type { Config } from './types'; import { RequestMode } from './types'; -let CONFIG: Config = { mode: RequestMode.FirstParty }; +let CONFIG: Config = { mode: RequestMode.Render }; // Merge publisher-provided config and adjust the log level accordingly. export function setConfig(cfg: Config): void { diff --git a/crates/js/lib/src/core/request.ts b/crates/js/lib/src/core/request.ts index 98e04a3..03799af 100644 --- a/crates/js/lib/src/core/request.ts +++ b/crates/js/lib/src/core/request.ts @@ -1,4 +1,4 @@ -// Request orchestration for tsjs: fires first-party iframe loads or third-party fetches. +// Request orchestration for tsjs: fires render (iframe) or auction (JSON) requests. import { delay } from '../shared/async'; import { log } from './log'; @@ -10,7 +10,7 @@ import type { RequestAdsCallback, RequestAdsOptions } from './types'; // getHighestCpmBids is provided by the Prebid extension (shim) to mirror Prebid's API -// Entry point matching Prebid's requestBids signature; decides first/third-party mode. +// Entry point matching Prebid's requestBids signature; decides render/auction mode. export function requestAds( callbackOrOpts?: RequestAdsCallback | RequestAdsOptions, maybeOpts?: RequestAdsOptions @@ -25,28 +25,38 @@ export function requestAds( callback = opts?.bidsBackHandler; } - const mode: RequestMode = (getConfig().mode as RequestMode | undefined) ?? RequestMode.FirstParty; + const mode = resolveRequestMode(getConfig().mode); log.info('requestAds: called', { hasCallback: typeof callback === 'function', mode }); try { const adUnits = getAllUnits(); const payload = { adUnits, config: {} }; log.debug('requestAds: payload', { units: adUnits.length }); - if (mode === RequestMode.FirstParty) void requestAdsFirstParty(adUnits); - else requestAdsThirdParty(payload); + if (mode === RequestMode.Render) void requestAdsRender(adUnits); + else requestAdsAuction(payload); // Synchronously invoke callback to match test expectations try { if (callback) callback(); } catch { /* ignore callback errors */ } - // network handled in requestAdsThirdParty; no-op here + // network handled in requestAdsAuction; no-op here } catch { log.warn('requestAds: failed to initiate'); } } -// Create per-slot first-party iframe requests served directly from the edge. -async function requestAdsFirstParty(adUnits: ReadonlyArray<{ code: string }>) { +function resolveRequestMode(mode: unknown): RequestMode { + if (mode === RequestMode.Render || mode === RequestMode.Auction) { + return mode; + } + if (mode !== undefined) { + log.warn('requestAds: invalid mode; defaulting to render', { mode }); + } + return RequestMode.Render; +} + +// Create per-slot iframe requests served directly from the edge via /ad/render. +async function requestAdsRender(adUnits: ReadonlyArray<{ code: string }>) { for (const unit of adUnits) { const size = (firstSize(unit) ?? [300, 250]) as readonly [number, number]; const slotId = unit.code; @@ -60,12 +70,12 @@ async function requestAdsFirstParty(adUnits: ReadonlyArray<{ code: string }>) { width: size[0], height: size[1], }); - iframe.src = `/first-party/ad?slot=${encodeURIComponent(slotId)}&w=${encodeURIComponent(String(size[0]))}&h=${encodeURIComponent(String(size[1]))}`; + iframe.src = `/ad/render?slot=${encodeURIComponent(slotId)}&w=${encodeURIComponent(String(size[0]))}&h=${encodeURIComponent(String(size[1]))}`; return; } if (attemptsRemaining <= 0) { - log.warn('requestAds(firstParty): slot not found; skipping iframe', { slotId }); + log.warn('requestAds(render): slot not found; skipping iframe', { slotId }); return; } @@ -88,18 +98,18 @@ async function requestAdsFirstParty(adUnits: ReadonlyArray<{ code: string }>) { } } -// Fire a JSON POST to the third-party ad endpoint and render returned creatives. -function requestAdsThirdParty(payload: { adUnits: unknown[]; config: unknown }) { +// Fire a JSON POST to /ad/auction and render returned creatives. +function requestAdsAuction(payload: { adUnits: unknown[]; config: unknown }) { // Render simple placeholders immediately so pages have content renderAllAdUnits(); if (typeof fetch !== 'function') { log.warn('requestAds: fetch not available; nothing to render'); return; } - log.info('requestAds: sending request to /third-party/ad', { + log.info('requestAds: sending request to /ad/auction', { units: (payload.adUnits || []).length, }); - void fetch('/third-party/ad', { + void fetch('/ad/auction', { method: 'POST', headers: { 'content-type': 'application/json' }, credentials: 'same-origin', diff --git a/crates/js/lib/src/core/types.ts b/crates/js/lib/src/core/types.ts index 3bf1b49..e65641a 100644 --- a/crates/js/lib/src/core/types.ts +++ b/crates/js/lib/src/core/types.ts @@ -37,14 +37,14 @@ export interface TsjsApi { } export enum RequestMode { - FirstParty = 'firstParty', - ThirdParty = 'thirdParty', + Render = 'render', + Auction = 'auction', } export interface Config { debug?: boolean; logLevel?: 'silent' | 'error' | 'warn' | 'info' | 'debug'; - /** Select ad serving mode. Default is RequestMode.FirstParty. */ + /** Select ad serving mode. Default is RequestMode.Render. */ mode?: RequestMode; // Extendable for future fields [key: string]: unknown; diff --git a/crates/js/lib/test/core/request.test.ts b/crates/js/lib/test/core/request.test.ts index 54b6264..79eeeb5 100644 --- a/crates/js/lib/test/core/request.test.ts +++ b/crates/js/lib/test/core/request.test.ts @@ -34,7 +34,7 @@ describe('request.requestAds', () => { document.body.innerHTML = '
'; addAdUnits({ code: 'slot1', mediaTypes: { banner: { sizes: [[300, 250]] } } } as any); - setConfig({ mode: 'thirdParty' } as any); + setConfig({ mode: 'auction' } as any); requestAds(); // wait microtasks @@ -66,7 +66,7 @@ describe('request.requestAds', () => { const { requestAds } = await import('../../src/core/request'); addAdUnits({ code: 'slot1', mediaTypes: { banner: { sizes: [[300, 250]] } } } as any); - setConfig({ mode: 'thirdParty' } as any); + setConfig({ mode: 'auction' } as any); requestAds(); await Promise.resolve(); @@ -92,7 +92,7 @@ describe('request.requestAds', () => { const { requestAds } = await import('../../src/core/request'); addAdUnits({ code: 'slot1', mediaTypes: { banner: { sizes: [[300, 250]] } } } as any); - setConfig({ mode: 'thirdParty' } as any); + setConfig({ mode: 'auction' } as any); requestAds(); await Promise.resolve(); @@ -102,7 +102,7 @@ describe('request.requestAds', () => { expect(renderMock).not.toHaveBeenCalled(); }); - it('inserts an iframe per ad unit with correct src (firstParty)', async () => { + it('inserts an iframe per ad unit with correct src (render mode)', async () => { const { addAdUnits } = await import('../../src/core/registry'); const { setConfig } = await import('../../src/core/config'); const { requestAds } = await import('../../src/core/request'); @@ -112,8 +112,8 @@ describe('request.requestAds', () => { div.id = 'slot1'; document.body.appendChild(div); - // Configure first-party mode explicitly - setConfig({ mode: 'firstParty' } as any); + // Configure render mode explicitly + setConfig({ mode: 'render' } as any); // Add an ad unit and request addAdUnits({ code: 'slot1', mediaTypes: { banner: { sizes: [[300, 250]] } } } as any); @@ -122,18 +122,18 @@ describe('request.requestAds', () => { // Verify iframe was inserted with expected src const iframe = document.querySelector('#slot1 iframe') as HTMLIFrameElement | null; expect(iframe).toBeTruthy(); - expect(iframe!.getAttribute('src')).toContain('/first-party/ad?'); + expect(iframe!.getAttribute('src')).toContain('/ad/render?'); expect(iframe!.getAttribute('src')).toContain('slot=slot1'); expect(iframe!.getAttribute('src')).toContain('w=300'); expect(iframe!.getAttribute('src')).toContain('h=250'); }); - it('skips iframe insertion when slot is missing (firstParty)', async () => { + it('skips iframe insertion when slot is missing (render mode)', async () => { const { addAdUnits } = await import('../../src/core/registry'); const { setConfig } = await import('../../src/core/config'); const { requestAds } = await import('../../src/core/request'); - setConfig({ mode: 'firstParty' } as any); + setConfig({ mode: 'render' } as any); addAdUnits({ code: 'missing-slot', mediaTypes: { banner: { sizes: [[300, 250]] } } } as any); requestAds(); diff --git a/docs/guide/api-reference.md b/docs/guide/api-reference.md index eabd1fa..535ab49 100644 --- a/docs/guide/api-reference.md +++ b/docs/guide/api-reference.md @@ -11,9 +11,9 @@ Quick reference for all Trusted Server HTTP endpoints. --- -## First-Party Endpoints +## Ad and Proxy Endpoints -### GET /first-party/ad +### GET /ad/render Server-side ad rendering endpoint. Returns complete HTML for a single ad slot. @@ -26,11 +26,11 @@ Server-side ad rendering endpoint. Returns complete HTML for a single ad slot. **Response:** - **Content-Type:** `text/html; charset=utf-8` -- **Body:** Complete HTML creative with first-party proxying applied +- **Body:** Complete HTML creative with proxy rewrites applied **Example:** ```bash -curl "https://edge.example.com/first-party/ad?slot=header-banner&w=728&h=90" +curl "https://edge.example.com/ad/render?slot=header-banner&w=728&h=90" ``` **Response Headers:** @@ -44,7 +44,7 @@ curl "https://edge.example.com/first-party/ad?slot=header-banner&w=728&h=90" --- -### POST /third-party/ad +### POST /ad/auction Client-side auction endpoint for TSJS library. @@ -88,7 +88,7 @@ Client-side auction endpoint for TSJS library. **Example:** ```bash -curl -X POST https://edge.example.com/third-party/ad \ +curl -X POST https://edge.example.com/ad/auction \ -H "Content-Type: application/json" \ -d '{"adUnits":[{"code":"banner","mediaTypes":{"banner":{"sizes":[[300,250]]}}}]}' ``` @@ -164,7 +164,7 @@ curl -I "https://edge.example.com/first-party/click?tsurl=https://advertiser.com ### GET/POST /first-party/sign -URL signing endpoint. Returns signed first-party proxy URL for a given target URL. +URL signing endpoint. Returns signed proxy URL for a given target URL. **Request Methods:** GET or POST @@ -415,11 +415,11 @@ See [Configuration](./configuration.md) for TSJS build options. ### Prebid Integration -#### GET /first-party/ad -See [First-Party Endpoints](#get-first-party-ad) above. +#### GET /ad/render +See [Ad and Proxy Endpoints](#get-ad-render) above. -#### POST /third-party/ad -See [First-Party Endpoints](#post-third-party-ad) above. +#### POST /ad/auction +See [Ad and Proxy Endpoints](#post-ad-auction) above. #### GET /prebid.js, /prebid.min.js, etc. (Script Override) Returns empty JavaScript to override Prebid.js scripts when the Prebid integration is enabled. By default, exact requests to `/prebid.js`, `/prebid.min.js`, `/prebidjs.js`, or `/prebidjs.min.js` will be intercepted and served an empty script. diff --git a/docs/guide/configuration-reference.md b/docs/guide/configuration-reference.md index db4d7ec..3366973 100644 --- a/docs/guide/configuration-reference.md +++ b/docs/guide/configuration-reference.md @@ -598,6 +598,7 @@ All integrations support: | `timeout_ms` | Integer | `1000` | Request timeout in milliseconds | | `bidders` | Array[String] | `[]` | List of enabled bidders | | `debug` | Boolean | `false` | Enable debug logging | +| `mode` | String | None | Default TSJS request mode when Prebid is enabled (`render` or `auction`) | | `script_patterns` | Array[String] | See below | Patterns for removing Prebid script tags and intercepting requests | **Default `script_patterns`**: @@ -615,6 +616,7 @@ server_url = "https://prebid-server.example/openrtb2/auction" timeout_ms = 1200 bidders = ["kargo", "rubicon", "appnexus", "openx"] debug = false +mode = "auction" # script_patterns = ["/static/prebid/*"] # Optional: restrict to specific path ``` @@ -625,6 +627,7 @@ TRUSTED_SERVER__INTEGRATIONS__PREBID__SERVER_URL=https://prebid.example/auction TRUSTED_SERVER__INTEGRATIONS__PREBID__TIMEOUT_MS=1200 TRUSTED_SERVER__INTEGRATIONS__PREBID__BIDDERS=kargo,rubicon,appnexus TRUSTED_SERVER__INTEGRATIONS__PREBID__DEBUG=false +TRUSTED_SERVER__INTEGRATIONS__PREBID__MODE=auction TRUSTED_SERVER__INTEGRATIONS__PREBID__SCRIPT_PATTERNS__0=/prebid.js TRUSTED_SERVER__INTEGRATIONS__PREBID__SCRIPT_PATTERNS__1=/prebid.min.js ``` diff --git a/docs/guide/environment-variables.md b/docs/guide/environment-variables.md index ce4b5a6..7162d04 100644 --- a/docs/guide/environment-variables.md +++ b/docs/guide/environment-variables.md @@ -204,6 +204,9 @@ TRUSTED_SERVER__INTEGRATIONS__PREBID__BIDDERS="appnexus,rubicon,openx" # Enable debug logging TRUSTED_SERVER__INTEGRATIONS__PREBID__DEBUG=false +# Default tsjs mode when Prebid integration is enabled (optional) +TRUSTED_SERVER__INTEGRATIONS__PREBID__MODE="auction" # or "render" + # Script patterns to remove Prebid tags and serve empty JS (indexed format) # Default patterns match common Prebid filenames at exact paths TRUSTED_SERVER__INTEGRATIONS__PREBID__SCRIPT_PATTERNS__0="/prebid.js" @@ -220,6 +223,7 @@ server_url = "https://prebid-server.example.com" timeout_ms = 1000 bidders = ["appnexus", "rubicon", "openx"] debug = false +mode = "auction" script_patterns = ["/prebid.js", "/prebid.min.js", "/prebidjs.js", "/prebidjs.min.js"] ``` diff --git a/docs/guide/error-reference.md b/docs/guide/error-reference.md index a7c9c43..18f9297 100644 --- a/docs/guide/error-reference.md +++ b/docs/guide/error-reference.md @@ -595,7 +595,7 @@ fastly log-tail fastly compute serve # Test endpoint -curl http://localhost:7676/first-party/ad?slot=test&w=300&h=250 +curl http://localhost:7676/ad/render?slot=test&w=300&h=250 ``` --- diff --git a/docs/guide/integration-guide.md b/docs/guide/integration-guide.md index f96a9a3..d5fc921 100644 --- a/docs/guide/integration-guide.md +++ b/docs/guide/integration-guide.md @@ -210,6 +210,8 @@ impl IntegrationScriptRewriter for MyIntegration { `html_processor.rs` calls these hooks after applying the standard origin→first-party rewrite, so you can simply swap URLs, append query parameters, or mutate inline JSON. Use this to point `