Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Trusted Server

:information_source: Trusted Server is an open-source, cloud based orchestration framework and runtime for publishers. It moves code execution and operations that traditionally occurs in browsers (via 3rd party JS) to secure, zero-cold-start [WASM](https://webassembly.org) binaries running in [WASI](https://github.com/WebAssembly/WASI) supported environments. It importantly gives publishers benefits such as: dramatically increasing control over how and who they share their data with (while maintaining user-privacy compliance), increasing revenue from inventory inside cookie restricted or non-JS environments, ability to serve all assets under 1st party context, and provides secure cryptographic functions to ensure trust across the programmatic ad ecosystem.
:information_source: Trusted Server is an open-source, cloud based orchestration framework and runtime for publishers. It moves code execution and operations that traditionally occurs in browsers (via external JS) to secure, zero-cold-start [WASM](https://webassembly.org) binaries running in [WASI](https://github.com/WebAssembly/WASI) supported environments. It importantly gives publishers benefits such as: dramatically increasing control over how and who they share their data with (while maintaining user-privacy compliance), increasing revenue from inventory inside cookie restricted or non-JS environments, ability to serve all assets under publisher context, and provides secure cryptographic functions to ensure trust across the programmatic ad ecosystem.

Trusted Server is the new execution layer for the open-web, returning control of 1st party data, security, and overall user-experience back to publishers.

Expand Down Expand Up @@ -210,11 +210,11 @@ Once configured, the following endpoints are available:

:warning: Key rotation keeps both the new and previous key active to allow for graceful transitions. Deactivate old keys manually when no longer needed.

## First-Party Endpoints
## Ad and Proxy Endpoints

- `/first-party/ad` (GET): returns HTML for a single slot (`slot`, `w`, `h` query params). The server inspects returned creative HTML and rewrites:
- `/ad/render` (GET): returns HTML for a single slot (`slot`, `w`, `h` query params). The server inspects returned creative HTML and rewrites:
- All absolute images and iframes to `/first-party/proxy?tsurl=<base-url>&<original-query-params>&tstoken=<sig>` (1×1 pixels are detected server‑side heuristically for logging). The `tstoken` is derived from encrypting the full target URL and hashing it.
- `/third-party/ad` (POST): accepts tsjs ad units and proxies to Prebid Server.
- `/ad/auction` (POST): accepts tsjs ad units and proxies to Prebid Server.
- `/first-party/proxy` (GET): unified proxy for resources referenced by creatives.
- Query params:
- `tsurl`: Target URL without query (base URL) — required
Expand All @@ -226,10 +226,10 @@ Once configured, the following endpoints are available:
- Image responses: proxied; if content‑type is missing, sets `image/*`; logs likely 1×1 pixels via size/URL heuristics
- Follows HTTP redirects (301/302/303/307/308) up to four hops, reapplying the forwarded synthetic ID and switching to `GET` after a 303; logs when the redirect limit is reached.
- When forwarding to the target URL, no `tstoken` is included (it is not part of the target URL).
- Synthetic ID propagation: reads the trusted ID from the incoming cookie/header and appends `synthetic_id=<value>` to the target URL sent to the third-party origin while preserving existing query strings.
- Synthetic ID propagation: reads the trusted ID from the incoming cookie/header and appends `synthetic_id=<value>` to the target URL sent to the origin while preserving existing query strings.
- Redirect following re-applies the identifier on each hop so downstream origins see a consistent ID even when assets bounce through intermediate trackers.

- `/first-party/click` (GET): first‑party click redirect handler for anchors and clickable areas.
- `/first-party/click` (GET): click redirect handler for anchors and clickable areas.
- Query params: same as `/first-party/proxy` (uses `tsurl`, original params, `tstoken`).
- Behavior:
- Validates `tstoken` against the reconstructed full URL (same enc+SHA256 scheme).
Expand All @@ -243,7 +243,7 @@ Notes

- Rewriting uses `lol_html`. Only absolute and protocol‑relative URLs are rewritten; relative URLs are left unchanged.
- For the proxy endpoint, the base URL is carried in `tsurl`, the original query parameters are preserved individually, and `tstoken` authenticates the reconstructed full URL.
- Synthetic identifiers are generated by `crates/common/src/synthetic.rs` and are surfaced in three places: publisher responses (headers + cookie), creative proxy target URLs (`synthetic_id` query param), and click redirect URLs. This ensures downstream integrations can correlate impressions and clicks without direct third-party cookies.
- Synthetic identifiers are generated by `crates/common/src/synthetic.rs` and are surfaced in three places: publisher responses (headers + cookie), creative proxy target URLs (`synthetic_id` query param), and click redirect URLs. This ensures downstream integrations can correlate impressions and clicks without direct cross-site cookies.

## Integration Modules

Expand Down
4 changes: 2 additions & 2 deletions SEQUENCE.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# 🛡️ Trusted Server — First-Party Proxying Flow
# 🛡️ Trusted Server — Proxying Flow

## 🔄 System Flow Diagram

Expand Down Expand Up @@ -80,7 +80,7 @@ sequenceDiagram
activate TS
activate PBS
activate DSP
JS->>TS: GET /first-party/ad<br/>(with signals)
JS->>TS: GET /ad/render<br/>(with signals)
TS->>PBS: POST /openrtb2/auction<br/>(OpenRTB 2.x)
PBS->>DSP: POST bid request
DSP-->>PBS: 200 bid response
Expand Down
2 changes: 1 addition & 1 deletion crates/common/README.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
34 changes: 17 additions & 17 deletions crates/common/src/integrations/prebid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@
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, Deserialize, Serialize, Validate)]
pub struct PrebidIntegrationConfig {
Expand Down Expand Up @@ -212,7 +212,7 @@
}
}

async fn handle_third_party_ad(
async fn handle_auction(
&self,
settings: &Settings,
mut req: Request,
Expand All @@ -223,7 +223,7 @@
},
)?;

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 {
Expand Down Expand Up @@ -260,14 +260,14 @@
.with_body(body))
}

async fn handle_first_party_ad(
async fn handle_render(
&self,
settings: &Settings,
mut req: Request,
) -> Result<Response, Report<TrustedServerError>> {
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()
Expand Down Expand Up @@ -357,8 +357,8 @@

fn routes(&self) -> Vec<IntegrationEndpoint> {
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
Expand All @@ -378,14 +378,14 @@
req: Request,
) -> Result<Response, Report<TrustedServerError>> {
let path = req.get_path().to_string();
let method = req.get_method().clone();

Check warning on line 381 in crates/common/src/integrations/prebid.rs

View workflow job for this annotation

GitHub Actions / cargo fmt

Diff in /home/runner/work/trusted-server/trusted-server/crates/common/src/integrations/prebid.rs

match method {
Method::GET if path == ROUTE_FIRST_PARTY_AD => {
self.handle_first_party_ad(settings, req).await
Method::GET if path == ROUTE_RENDER => {
self.handle_render(settings, req).await
}
Method::POST if path == ROUTE_THIRD_PARTY_AD => {
self.handle_third_party_ad(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(),
Expand Down Expand Up @@ -946,8 +946,8 @@
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"));
Expand All @@ -965,7 +965,7 @@

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)
Expand Down Expand Up @@ -1158,8 +1158,8 @@
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"));
Expand Down
2 changes: 1 addition & 1 deletion crates/js/lib/src/core/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
38 changes: 24 additions & 14 deletions crates/js/lib/src/core/request.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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
Expand All @@ -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;
Expand All @@ -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;
}

Expand All @@ -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',
Expand Down
6 changes: 3 additions & 3 deletions crates/js/lib/src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
18 changes: 9 additions & 9 deletions crates/js/lib/test/core/request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe('request.requestAds', () => {

document.body.innerHTML = '<div id="slot1"></div>';
addAdUnits({ code: 'slot1', mediaTypes: { banner: { sizes: [[300, 250]] } } } as any);
setConfig({ mode: 'thirdParty' } as any);
setConfig({ mode: 'auction' } as any);

requestAds();
// wait microtasks
Expand Down Expand Up @@ -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();
Expand All @@ -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();
Expand All @@ -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');
Expand All @@ -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);
Expand All @@ -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();

Expand Down
Loading
Loading