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 `