Skip to content

Add first-class APS OpenRTB integration#918

Open
ChristianPavilonis wants to merge 7 commits into
mainfrom
issue-764-aps-openrtb
Open

Add first-class APS OpenRTB integration#918
ChristianPavilonis wants to merge 7 commits into
mainfrom
issue-764-aps-openrtb

Conversation

@ChristianPavilonis

Copy link
Copy Markdown
Collaborator

Summary

  • Replace the legacy APS contextual/TAM request path with a first-class APS OpenRTB provider using /e/pb/bid.
  • Carry minimized, typed APS renderer state through direct auctions, navigation/page-bids, mediation, and GPT/Universal Creative rendering.
  • Add default-off script-creative gating, opaque-origin rendering, nonce-bound messaging, PBS coexistence safeguards, migration docs, and regression coverage.

Changes

File Change
crates/trusted-server-core/src/integrations/aps.rs Builds independent APS OpenRTB requests, validates and reduces responses, creates minimized renderer envelopes, and serves the sandboxed static renderer.
crates/trusted-server-core/src/auction/* Adds typed APS renderer metadata, preserves bid identity through floors and orchestration, and exposes APS winners without adm.
crates/trusted-server-core/src/integrations/prebid.rs Prevents APS from also being sent to Prebid Server for Trusted Server APS cohorts.
crates/trusted-server-core/src/{publisher.rs,creative_opportunities.rs} Carries APS renderer data through navigation/page-bids while retaining legacy slot configuration only for compatibility.
crates/trusted-server-js/lib/src/integrations/aps/render.ts Validates exact renderer descriptors and envelopes, creates opaque sandboxed frames, and enforces fragment-bound nonces and readiness acknowledgements.
crates/trusted-server-js/lib/src/integrations/gpt/index.ts Routes APS winners through the Universal Creative dynamic-renderer bridge without cache fetches, beacons, or native APS display calls.
crates/trusted-server-integration-tests/browser/tests/shared/aps-renderer.spec.ts Verifies CSP sandboxing, nonce and replay protections, same-origin rejection, restrictive-CSP compatibility, and parent-DOM isolation.
trusted-server.example.toml and integration fixtures Adds canonical account_id, the APS OpenRTB endpoint, and default-off allow_script_creatives.
docs/**, CHANGELOG.md, TESTING.md Documents migration, security boundaries, orchestration behavior, rollout gates, and validation coverage.

Closes

Closes #764

Test plan

  • cargo test-fastly && cargo test-axum
  • cargo clippy-fastly && cargo clippy-axum
  • cargo fmt --all -- --check
  • JS tests: cd crates/trusted-server-js/lib && npx vitest run
  • JS format: cd crates/trusted-server-js/lib && npm run format
  • Docs format: cd docs && npm run format
  • WASM build: cargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1
  • Manual testing via fastly compute serve
  • Other: cargo test-cloudflare, cargo test-spin, all Cloudflare/Spin clippy aliases, 13 cross-adapter parity tests, browser TypeScript compilation, full Next.js/WordPress Playwright suite, TSJS bundle build, and VitePress build

Checklist

  • Changes follow CLAUDE.md conventions
  • No unwrap() in production code — use expect("should ...")
  • Uses project log macros (not println!)
  • New code has tests
  • No secrets or credentials committed

Rollout note

Broad production rollout and production tagtype=script enablement remain gated on controlled APS-account validation and APS account-team confirmation. Script creatives remain disabled by default.

Comment thread crates/trusted-server-js/lib/test/integrations/aps/render.test.ts Fixed
Comment thread crates/trusted-server-js/lib/test/integrations/aps/render.test.ts Fixed
@ChristianPavilonis
ChristianPavilonis marked this pull request as draft July 16, 2026 15:17
@ChristianPavilonis
ChristianPavilonis marked this pull request as ready for review July 17, 2026 02:07

@aram356 aram356 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add APS metadata to result of the auction?

@aram356 aram356 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Replaces the legacy APS TAM/contextual path with a first-class OpenRTB provider, adds a sandboxed opaque-origin creative renderer, a Prebid-adapter rendering route, PBS coexistence safeguards, and an inventory-identity override. The security core is well-hardened and I verified rather than assumed it — allow-same-origin is never combined with allow-scripts (pinned by a regression test), the nonce/envelope contract matches byte-for-byte across Rust and JS, and the two CodeQL alerts were genuinely resolved. CI is green.

That said, I found 7 blocking defects — an auction-wide failure from one malformed bid, two ways APS demand can still reach Prebid Server, two blank-slot / consumed-impression bugs in the new rendering routes, a privacy leak in the identity override, and a silent config break on upgrade. Requesting changes.

A process note: the PR body's change table documents only the first commit. Two later commits ("Add APS inventory identity overrides", "Render APS bids from the trustedServer Prebid adapter") added whole subsystems it omits — worth refreshing before merge.

Most findings are filed as inline comments. The cross-cutting ones are below.

Blocking

🔧 wrench

  • APS exclusion is case-sensitive and leaks via the head-insert path (prebid.rs:1427/1434, prebid.rs:909): Both PBS guards compare name == "aps" exactly, so "APS"/"Aps" in the free-form config.bidders slips through to PBS and is sold twice. Separately, head_inserts serializes config.bidders/client_side_bidders verbatim into window.__tsjs_prebid, so aps still ships to the browser — the safeguard is server-side only, and client_side_bidders is never filtered at all. The durable fix for this and the stored-request finding is to reject aps in [prebid].bidders/client_side_bidders at config-validation time.

Non-blocking

🤔 thinking

  • Full client-controlled Referer path+query now enters the bidstream (formats.rs:93-116): site.page changed from a bare https://{domain} to the full same-origin Referer including query string, so publisher URLs carrying PII (?email=, session tokens, gclid) are forwarded to every SSP — up to 8 KiB of attacker-chosen data (the host check constrains only the authority). This mirrors client-side Prebid so it may be intentional, but for a privacy-preserving edge server it deserves an explicit decision: strip query/fragment by default, or gate behind config.
  • adserver_mock mediation is last-write-wins and the new test enshrines it (adserver_mock.rs:107, :899): build_bid_index keys on (provider, slot, bidder); if APS ever stops reducing to one bid per slot, the mediator restores the losing candidate's renderer against the winning bid's price — wrong creative at wrong price, silently. bid_id now exists and would disambiguate; the mock only echoes crid. At least raise the collision log from debug! to warn!.
  • providers.aps is now write-only and fails silently (creative_opportunities.rs:366,378): the field is a genuine deny_unknown_fields deser-compat shim (removing it hard-fails existing TOML), but to_ad_slot no longer reads it, so an operator who sets aps.slot_id expecting routing gets a no-op with zero diagnostics. Add a load-time warning.

♻️ refactor

  • BidRenderer::aps() is an infallible accessor on a single-variant enum (types.rs:217-225, used at publisher.rs:1957): the moment a second variant lands the natural "fix" is a panic in the fallback arm. Prefer as_aps(&self) -> Option<&ApsRendererV1>. Note the generic bid.bid_id field this PR adds already carries the same value, so publisher.rs could derive hb_adid renderer-agnostically.

📝 note

  • Undocumented /auction wire-contract change: for renderer bids, crid goes from always-present {bidder}-creative to bid.creative_id (absent when the bidder omits crid), id from {bidder}-{slot} to the upstream bid id, and adm is omitted entirely. tsjs absorbs all three, but POST /auction is a documented OpenRTB endpoint; any non-tsjs consumer reading those fields breaks. The CHANGELOG's Breaking section doesn't mention the response shape.
  • Test quality across the new suites: the browser same-origin rejection test (aps-renderer.spec.ts:731-805) is effectively vacuous — a fixed waitForTimeout(100) with no positive control, driven through a hand-rolled harness instead of renderApsCreative, so it would pass if the guard broke. Several sandbox assertions (:464,:555) assert the harness's own input rather than product behavior. And there is no JS-side boundary coverage for the size caps (envelope 256 KiB / base64 349528, account-id 1024, creative-url 4096) nor a regression test for the two blocking rendering-route bugs. (I did confirm the same-origin guard at aps.rs:74 is live, not dead code, via a browser test — so that check is real; the test just doesn't exercise it.)

⛏ nitpick

  • creative_id is the only renderer field without a length cap (aps.rs:682) — bounded by the 2 MiB body cap but inconsistent with account_id/creative_url.
  • dropped_bid_count and drop_reasons don't reconcile (aps.rs:776-799) — price-losers increment the count with no reason entry; the test asserts dropped=1, drop_reasons={}.
  • auction/README.md:120 ASCII box right border is off by one column.
  • trusted-server.example.toml regressed a fictional aps.example.com/e/dtb/bid to a real vendor host; several docs/tests use real *.amazon-adsystem.com endpoints against the project's "example.com only" rule. These are functionally-required vendor endpoints (not customer data or credentials) and amazon-adsystem predates this PR, so noting rather than blocking.

CI Status

  • fmt: PASS
  • clippy (all six adapter targets): PASS
  • rust tests (fastly/axum/cloudflare/spin + parity): PASS
  • js tests (vitest): PASS
  • browser integration + CodeQL: PASS

Comment thread crates/trusted-server-core/src/auction/formats.rs Outdated
Comment thread crates/trusted-server-core/src/auction/formats.rs Outdated
Comment thread crates/trusted-server-core/src/auction/formats.rs
Comment thread crates/trusted-server-core/src/integrations/prebid.rs Outdated
Comment thread crates/trusted-server-core/src/integrations/prebid.rs Outdated
Comment thread crates/trusted-server-js/lib/src/integrations/prebid/index.ts Outdated
Comment thread crates/trusted-server-js/lib/src/integrations/aps/render.ts
Comment thread crates/trusted-server-js/lib/src/integrations/aps/render.ts Outdated
Comment thread crates/trusted-server-js/lib/src/integrations/aps/render.ts
Comment thread crates/trusted-server-integration-tests/browser/tests/shared/aps-renderer.spec.ts Outdated

@prk-Jr prk-Jr left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

First-class APS OpenRTB integration replacing the legacy contextual/TAM path, with a sandboxed (opaque-origin, nonce-gated, CSP-enforced) renderer that deliberately withholds adm from the client outside that sandbox. The core security design (bid parsing never reads adm, envelope byte-bounds, iframe sandbox without allow-same-origin, identity-checked postMessage, server-side CSP header) is solid and well-tested, including a genuinely adversarial browser test suite (nonce replay, CSP-only isolation with the sandbox attribute omitted, live same-origin attack attempts). The main blocker is that this branch does not currently merge cleanly with main — see Blocking #1, which is also the root cause of the two failing CI checks.

Blocking

🔧 wrench

  • PR does not merge cleanly with main: main's commit 8bb5450dc (#890) added a make_test_bid_with_creative() helper in publisher.rs that builds a Bid{} literal without this PR's new bid_id/creative_id/renderer fields. Rebasing/merging onto current main fails with error[E0063]: missing fields — this is exactly what's failing the cargo fmt and cargo test CI checks shown on this PR (GitHub builds the synthetic merge commit, not the raw branch tip). See inline comment on auction/types.rs.

❓ question

  • Debug mode can leak adm to the browser (crates/trusted-server-core/src/integrations/aps.rs:628): undermines the PR's core claim that adm never reaches the client outside the sandbox. See inline comment.
  • One bad bid can abort the entire /auction response (crates/trusted-server-core/src/auction/formats.rs:313): no multi-slot blast-radius test exists. See inline comment.

Non-blocking

♻️ refactor

  • Bid (types.rs:253) has no #[derive(Default)] despite every field type supporting it — deriving it directly prevents recurrence of Blocking #1 the next time a field is added.
  • aps.rs:1169 — no test exercises the fail-closed startup path (enabled+invalid config → register_providers() returning Err), only ApsConfig::validate() directly.
  • gpt/index.ts:999renderingAdIds grows unbounded on the success path, inconsistent with sibling bounded caches added elsewhere in this PR.

🤔 thinking

  • aps.rs:1047 — context-free parse_response silently discards a valid response if ever called without context; safe today only because all real call sites use parse_response_with_context.
  • aps.rs:380device.language has no explicit byte cap, unlike other forwarded fields in this file.
  • orchestrator.rs:28backend_to_provider's bare 4-tuple is duplicated across 3 sites; same fragility class as Blocking #1.
  • formats.rs:302 — when both bid.creative and bid.renderer are Some, creative silently wins with no log/assert.
  • prebid.rs:1427 — the "aps" bidder exclusion (preventing APS double-serving through Prebid Server) is case-sensitive; pre-existing pattern, worth eq_ignore_ascii_case given the stated goal.
  • publisher.rs:2207 — full client query string is now forwarded into site.page for every provider; unlike aps.rs (8192-byte cap), prebid.rs's equivalent has no bound.
  • render.ts:325 — rapid re-render of the same slot orphans the prior frame's message listener/timeout instead of routing through its own cleanup (self-heals within 10s).
  • TESTING.md:49 — still describes APS as "mocked" / lists "Implement real APS" as a future step, contradicting this PR and the log excerpt this PR itself just updated a few lines above.
  • docs/guide/auction-orchestration.md:623 — config reference table omits inventory_domain/inventory_page_origin, real validated fields documented elsewhere in this PR.

⛏ nitpick

  • aps.rs:851 — a seatbid entry with no bid array is skipped without incrementing drop_reasons, making a no-bid response harder to diagnose.
  • render.test.ts:216 — no test for "correct nonce, wrong event.source" rejection specifically (the guard is implemented correctly at render.ts:351, just not locked in by that exact case).
  • render.ts:388 — the embedded renderer-document string hand-duplicates constants (including the 10000ms timeout) instead of interpolating them.

🌱 seedling

  • Byte-limit constants (MAX_ACCOUNT_ID_BYTES, etc.) exist as 3 independently hardcoded copies across Rust, TS, and the embedded HTML string — worth a single source of truth eventually.
  • creative_opportunities.rs:366 — legacy providers.aps.slot_id is now a silent no-op with no operator-facing warning if left configured.
  • aps-renderer.spec.ts:169 — no browser-level test for cross-ad-unit capability theft (the messageSourceBelongsToAdUnit guard exists and works, just isn't e2e-tested against a mismatched ad unit).

📝 note

  • auction/README.md:364 — this example is stale relative to the new renderer flow (pre-existing drift, not touched by this PR).

👍 praise

  • aps.rs: parse_bid never reads adm; envelope is byte-bounded and fixture-verified byte-for-byte between Rust and TS; CSP header (APS_RENDERER_CSP) is correctly sent server-side with no allow-same-origin.
  • orchestrator.rs: provider_request_context/effective_timeout threading fix, pinned by dispatched_collection_reuses_provider_launch_context.
  • render.ts: validateApsRenderer's exact-key allowlist cross-checking the outer descriptor vs. the decoded OpenRTB payload; the event.source === iframe.contentWindow identity check (the right check for an opaque-origin sandbox).
  • prebid.rs: the trustedServer bidder-expansion path correctly re-strips "aps" even when reintroduced via config.bidders, with a dedicated regression test.
  • aps-renderer.spec.ts: nonce-replay, CSP-only isolation (with the sandbox attribute omitted), and same-origin attack tests are genuinely adversarial — real attempts against a live server, not tautological checks of config strings.

CI Status

  • fmt: FAIL (caused by the merge-conflict issue above, not this branch's own code)
  • rust tests: FAIL (same root cause)
  • clippy (all adapters): PASS
  • cross-adapter parity: PASS
  • browser/integration tests: PASS
  • vitest: PASS
  • CodeQL: PASS

Comment thread crates/trusted-server-core/src/auction/types.rs
Comment thread crates/trusted-server-core/src/integrations/aps.rs
Comment thread crates/trusted-server-core/src/auction/formats.rs Outdated
Comment thread crates/trusted-server-core/src/integrations/aps.rs
Comment thread crates/trusted-server-js/lib/src/integrations/gpt/index.ts Outdated
Comment thread crates/trusted-server-core/src/integrations/aps.rs
Comment thread crates/trusted-server-js/lib/test/integrations/aps/render.test.ts
Comment thread crates/trusted-server-js/lib/src/integrations/aps/render.ts Outdated
Comment thread crates/trusted-server-core/src/creative_opportunities.rs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add the Amazon APS Prebid Adapter as first class integration

4 participants