Skip to content

Add ts CLI ad-template config diagnostics and browser audit#823

Open
prk-Jr wants to merge 158 commits into
mainfrom
feature/ts-cli-ad-templates
Open

Add ts CLI ad-template config diagnostics and browser audit#823
prk-Jr wants to merge 158 commits into
mainfrom
feature/ts-cli-ad-templates

Conversation

@prk-Jr

@prk-Jr prk-Jr commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Adds operator tooling to verify server-side ad-template ([creative_opportunities]) configuration: static path/slot diagnostics via ts config ad-templates …, and browser-backed live verification via ts audit … (local Chrome/Chromium over CDP).
  • Adds ts audit ad-templates generate <url> to bootstrap [creative_opportunities] slots from a live page's GPT state — reconstructed from the GPT registry (googletag.pubads().getSlots()) and captured gampad/ads requests — and write them into an existing trusted-server.toml in place, preserving every other section. Slots accumulate across pages (--page-pattern unions coverage into re-seen slots), so multi-page pattern sets can be built run-by-run. A --cookie flag carries a valid session past an origin challenge (this tool still does not evade bot detection — it forwards a clearance cookie a human already earned).
  • Lets publishers catch slot/config drift before deploy — match a path to configured slots, assert expected slots in CI, explain runtime ad-stack gating, and confirm configured slots actually render (DOM/GPT/APS evidence) on live pages.
  • Keeps the CLI host-only: browser deps (chromiumoxide) are excluded from the wasm32-wasip1 build, and the runtime ad-stack gate is shared with publisher.rs so the CLI cannot drift from server behavior.

Stacks on the EdgeZero CLI base (server-side-ad-templates-impl). After merging the base back in, the diff against it is feature-only.

Behavior change: bare ts audit <url> is now a hidden alias for ts audit page <url>; the base branch's draft-artifact generation moved to the explicit ts audit generate <url>.

Changes

Area Change
trusted-server-core/src/creative_opportunities.rs [creative_opportunities] config types, match_slots, and a shared evaluate_ad_stack_gate runtime gate
trusted-server-core/src/publisher.rs Route should_run_server_side_ad_stack through the shared gate (behavior-preserving)
trusted-server-cli/src/commands/config/ad_templates.rs ts config ad-templates {lint,match,check,explain} static diagnostics
trusted-server-cli/src/app_config.rs Shared effective app-config loader for the config + audit commands
trusted-server-cli/src/ad_templates/{expected,compare,output}.rs Pure expected-slot projection, DOM/GPT/APS evidence comparison, and the stable JSON model
trusted-server-cli/src/commands/audit/{mod,page,collector,browser,ad_templates}.rs, commands/audit/ad_template_collector.js ts audit page + ts audit ad-templates verify: chromiumoxide collector, read-only GPT/APS/DOM init script, verifier orchestration
trusted-server-cli/src/commands/audit/generate/{mod,gpt_slots}.rs ts audit ad-templates generate: reconstruct slots from live GPT (registry + gampad/ads), normalize ephemeral div-id noise (React _R_ hashes, -container, hex UUIDs) to stable prefixes, merge into the existing config in place
trusted-server-cli/src/commands/audit/generate/{browser_collector,collector,analyzer}.rs Existing draft-config generator moved under generate/; collector scrapes the live GPT registry and supports operator cookies
trusted-server-cli/src/run.rs, src/lib.rs Command wiring and parser tests for the audit namespace
trusted-server-cli/Cargo.toml Host-only edgezero-core + serde_json deps (cfg-gated off wasm, like the existing browser deps)
docs/superpowers/{specs,plans}/2026-06-26-server-side-ad-template-cli* Design spec and implementation plan

Closes

Not yet linked to an issue.

Test plan

  • cargo test --workspace
  • cargo clippy --workspace --all-targets --all-features -- -D warnings
  • cargo fmt --all -- --check
  • JS tests: cd crates/trusted-server-js/lib && npx vitest run (393 passed)
  • 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 (plus cargo build -p trusted-server-cli --target wasm32-wasip1 — browser deps stay out of wasm)
  • Other: host CLI tests cargo test -p trusted-server-cli --target <host-triple> (164, incl. slot reconstruction, div-normalization, merge/union, TOML-escaping/CRLF tests + Chrome-gated browser fixtures for evidence collection and scroll-phase attribution); manual ts audit ad-templates verify against a local fixture — confirmed (exit 0) and --strict on a missing slot (exit 2)

How to use

Static ts config ad-templates … commands only read local effective config (no browser). Browser-backed ts audit … commands launch a local Chrome/Chromium.

Configure slots

In your (gitignored) trusted-server.toml — fictional values shown:

[creative_opportunities]
gam_network_id = "123"

[[creative_opportunities.slot]]
id = "atf"
gam_unit_path = "/123/news/atf"
div_id = "ad-atf-"            # treated as a prefix; matches e.g. ad-atf-0
page_patterns = ["/news/*"]
formats = [{ width = 300, height = 250 }]

# optional provider hints
[creative_opportunities.slot.providers.aps]
slot_id = "atf"

Generate slots from a live page (needs local Chrome/Chromium)

# Bootstrap slots → writes [creative_opportunities] into trusted-server.toml in place
ts audit ad-templates generate https://www.example.com/ --page-pattern '/'

# Add another page's coverage — unions the pattern into shared slots
ts audit ad-templates generate https://www.example.com/news/story --page-pattern '/news/*'

ts audit ad-templates generate https://www.example.com/ --dry-run    # preview, no write
ts audit ad-templates generate https://www.example.com/ --replace    # overwrite instead of merge
ts audit ad-templates generate https://www.example.com/ --cookie 'name=value'  # carry a session past a challenge

Static diagnostics (no browser)

# Summarize config + deploy-time implications
ts config ad-templates lint

# Which configured slots match a path (or full URL)
ts config ad-templates match /news/story --details

# CI assertion: exact expected slot set
ts config ad-templates check /news/story --expected-slot atf
ts config ad-templates check /weather --expect-no-slots
ts config ad-templates check /news/story --expected-slot atf --allow-extra-slots

# Explain the runtime ad-stack gates for a modeled request
ts config ad-templates explain /news/story --bot --consent-denied --prefetch

Browser-backed audit (needs local Chrome/Chromium)

# Generic read-only page summary
ts audit page https://www.example.com/

# Verify configured slots render on live pages (DOM/GPT/APS evidence)
ts audit ad-templates verify https://www.example.com/news/story
ts audit ad-templates verify https://www.example.com/news/story --json
ts audit ad-templates verify https://www.example.com/ --strict --scroll

# Carry a session past an origin challenge (repeatable)
ts audit ad-templates verify https://www.example.com/ --cookie 'name=value'

# Point at a specific browser / tune settle timing
ts audit ad-templates verify https://www.example.com/ \
  --chrome /path/to/chrome --settle-quiet-ms 1000 --settle-max-ms 15000

Shared config flags (all of the above)

--app-config <path>     # default: <app.name>.toml beside edgezero.toml
--manifest <path>       # default: edgezero.toml
--no-env                # skip the app-config environment overlay

Exit behavior

  • Default verify is auditor-assist: exits 0 even with missing/partial evidence.
  • --strict exits non-zero when a matched slot is missing or only partially confirmed (CI gate). A page-level navigation failure also exits non-zero.
  • Runtime gates (e.g. [auction].enabled = false) mark a page "skipped" so --strict does not fail it.

Local live test (deterministic, no external site)

Many large ad publishers block headless/non-evasive browsers, so verify against them sees a challenge page, not the article (this tool does not evade bot detection). To exercise the full pipeline reliably, serve a local fixture:

# 1. Fixture page that stubs just enough GPT (no network), served over HTTP
mkdir -p demo/news
cat > demo/news/story.html <<'HTML'
<!doctype html><html><head><meta charset="utf-8">
<div id="ad-atf-0"></div>
<script>
(function(){var s=[];var gt={cmd:[]};
gt.defineSlot=function(p,z,d){var o={getAdUnitPath:function(){return p},getSlotElementId:function(){return d},getSizes:function(){return z.map(function(x){return {getWidth:function(){return x[0]},getHeight:function(){return x[1]}}})}};s.push(o);return o};
gt.pubads=function(){return {getSlots:function(){return s}}};var op=gt.cmd.push.bind(gt.cmd);gt.cmd.push=function(c){op(c);c()};
window.googletag=gt;googletag.cmd.push(function(){googletag.defineSlot('/123/news/atf',[[300,250]],'ad-atf-0')});})();
</script></head><body></body></html>
HTML
(cd demo && python3 -m http.server 8771 &)

# 2. Verify against it (config from the example above, with page_patterns ["/news/*"])
ts audit ad-templates verify http://127.0.0.1:8771/news/story.html --json
#   => slots[0].status == "confirmed"

Checklist

  • Changes follow CLAUDE.md conventions
  • No unwrap() in production code — use expect("should ...")
  • No println!/eprintln! in library code (CLI output uses writeln!; errors use log)
  • New code has tests
  • No secrets or credentials committed

jevansnyc and others added 30 commits April 15, 2026 20:47
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Incorporate all review feedback (aram356 + jevansnyc): cache contract,
  consent/GDPR gating, async restructuring detail, CreativeOpportunityFormat
  schema, glob pattern fix, XSS escaping, win notifications, APS params,
  timeout config key, defineSlot fix, gpt.rs ownership, KV migration path,
  Phase 2 sketch
- Fix Prettier formatting (format-docs CI)
- Add implementation plan (12 tasks, TDD, ordered by dependency)
- Incorporate all review feedback (aram356 + jevansnyc): cache contract,
  consent/GDPR gating, async restructuring detail, CreativeOpportunityFormat
  schema, glob pattern fix, XSS escaping, win notifications, APS params,
  timeout config key, defineSlot fix, gpt.rs ownership, KV migration path,
  Phase 2 sketch
- Fix Prettier formatting (format-docs CI)
- Add implementation plan (12 tasks, TDD, ordered by dependency)
Replace the head-injected __ts_bids design with a server-cached bid
delivery model fetched by the client via a new /ts-bids endpoint. The
auction never blocks page rendering — </head> flushes immediately, body
parses without waiting for bids, and the client fetches bids in parallel
with content paint.

Key changes:
- §2 Goal: bid delivery decoupled from page rendering; FCP unchanged from
  no-TS baseline
- §4.3 Auction Trigger: drop buffered/streaming dichotomy; single mode
  forces chunked encoding on all origins (WordPress, NextJS, etc.)
- §4.4 Head Injection: only __ts_ad_slots and __ts_request_id injected at
  <head> open; bid results moved to /ts-bids endpoint
- §4.6 Client Residual: __tsAdInit defines slots immediately, fetches
  bids via /ts-bids, applies targeting and fires refresh() after resolve
- §4.7 (new) Caching Behavior: explicit cacheability table for HTML, JS,
  CSS, tsjs bundle, bid results; Fastly edge HTTP cache leveraged for
  origin HTML
- §5 Request-Time Sequence: full mermaid diagram covering content +
  creative + burl flow with cache-hit and cache-miss branches; separate
  text sequences for cache-hit (~80ms FCP, ~900ms ad-visible) and
  cache-miss (~250ms FCP, ~1,050ms ad-visible)
- §6 Performance Summary: cache-hit and cache-miss columns; FCP added
  as a tracked metric
- §7 Implementation Scope: add bid_cache.rs, /ts-bids endpoint, force
  chunked encoding step
- §8 Edge Cases: origin-agnostic entries; new entries for /ts-bids 404
  and client-never-fetches-/ts-bids

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pivot from the /ts-bids fetch endpoint + in-process bid_cache design to
inline __ts_bids injection before </body>. The earlier design relied on
shared state that doesn't reliably survive Fastly Compute's per-request Wasm
isolate model — body injection achieves the same FCP property in a single
response with no shared-state requirement.

Key changes:

- §4.3: replace /ts-bids long-poll with bounded </body> hold tied to
  A_deadline. Body content above </body> paints first; close-tag held
  until auction completes or A_deadline fires (graceful __ts_bids = {}
  fallback).
- §4.3: add auction-eligibility gating (consent, bot UA, prefetch hints,
  HEAD method, slot match) so auctions fire on real first-page-load
  impressions only.
- §4.4: replace __ts_request_id + /ts-bids machinery with two inline
  <script> blocks — __ts_ad_slots at <head> open, __ts_bids before
  </body> via lol_html el.on_end_tag().
- §4.5: move both nurl and burl to client-side firing from
  slotRenderEnded after hb_adid match. Server-side firing rejected to
  avoid billing inflation on bids that never render.
- §4.6: replace fetch+Promise pattern with synchronous __ts_bids read.
  Add lazy slim-Prebid loader (post-window.load) for scroll/refresh
  auctions and Phase B identity warm-up. Add ts_initial=1 slot-ownership
  sentinel.
- §4.7: switch Cache-Control from private, no-store to private,
  max-age=0 to preserve browser BFCache eligibility while still
  preventing intermediate-cache leaks.
- §4.8 (new): document the EC/KV identity model as load-bearing auction
  input — Phase A retrieval at request time, Phase B post-render
  enrichment via slim-Prebid userID modules. Add bare-EC first-impression
  caveat and auction_eid_count metric. Note federated-consortium
  passphrase property and clickstream-compounding speed win.
- §5: update mermaid + cache-hit/miss timelines for bounded body hold;
  ad-visible converges to ~870ms (hit) / ~1,020ms (miss).
- §6: drop /ts-bids RTT row; add DCL row; add clickstream-compounding,
  TS-overhead, identity-coverage, and confidence-interval framing.
- §7: drop bid_cache.rs and /ts-bids endpoint from scope; add
  auction-eligibility gating and slim-Prebid bundle build target. Add
  explicit "Deleted" subsection.
- §8: drop /ts-bids edge cases; add SPA/pushState, bare-EC, bot/prefetch,
  HEAD, BFCache restoration cases.
- §9.6: server-side GAM downgraded from "Phase 2 commitment" to
  aspirational and contingent on Google agreement. §9.8 (slim-Prebid
  bundle composition), §9.9 (Privacy Sandbox), §9.10 (per-bidder consent)
  added as follow-ups.

Implementation plan at docs/superpowers/plans/2026-04-30-server-side-ad-templates.md
is now stale relative to this spec; needs regenerating before code lands.
…ities.toml

Adds the creative_opportunities field to Settings struct to deserialize
configuration for the server-side ad auction feature. Includes build.rs
stubs for types required during build-time configuration validation.

Creates creative-opportunities.toml with example slot configuration and
updates trusted-server.toml with the [creative_opportunities] section
defining GAM network ID, auction timeout, and price granularity settings.

Tests pass with proper TOML parsing of the creative_opportunities section.
…ared auction state

- Add `ad_slots_script: Option<String>` and `ad_bids_state: Arc<RwLock<Option<String>>>` fields to `HtmlProcessorConfig`
- Update `from_settings` to initialize both new fields with safe defaults
- Prepend `ad_slots_script` inside the existing `<head>` handler before integration inserts
- Add `element!("body", ...)` handler that uses `end_tag_handlers()` to inject `__ts_bids` before `</body>`; falls back to empty `{}` when auction state is `None`
- Add `IntegrationRegistry::empty_for_tests()` test helper
- Add three new tests covering all injection paths
…gibility gates; max-age=0

- Make handle_publisher_request async; add orchestrator and slots_file params
- Dispatch origin request with send_async before running auction in parallel
- Gate auction on GET, no prefetch, no bot, matched slots, TCF purpose-1 consent
- Run server-side auction and write bucketed bids to ad_bids_state Arc<RwLock>
- Compute ad_slots_script after response headers; set Cache-Control: private, max-age=0
- Fix Stream arm to thread actual ad_slots_script and ad_bids_state through
- Add build_auction_request, build_bid_map, build_bids_script, build_ad_slots_script helpers
- Update route_tests.rs to pass empty slots_file to route_request
- build_bid_map now returns serde_json::Map with full bid objects (hb_pb,
  hb_bidder, hb_adid, nurl, burl) instead of a plain CPM string map
- build_bids_script / build_ad_slots_script now emit full <script> tags
  using JSON.parse("…") for safe inline embedding; add html_escape_for_script helper
- build_ad_slots_script uses correct property names (gam_unit_path, div_id,
  formats, targeting) matching the client-side TSJS bundle expectations
- Replace map_or(false, …) with is_some_and(…) on lines 546, 549, 567
- Add # Panics doc sections to handle_publisher_request and create_html_processor
… from slotRenderEnded; slim-Prebid lazy loader
- Enable APS and adserver_mock in auction config; set providers and mediator
- Increase auction_timeout_ms from 500ms to 3000ms — 500ms was too tight
  for HTTPS round-trips to mocktioneer, leaving the mediator zero budget
- Fix mediation request: send numeric price instead of opaque encoded_price;
  mocktioneer requires a decoded price field and does not support encoded_price
- Expand creative-opportunities slot page_patterns to include /news/**
Define SlotRenderEndedEvent, SlotRenderEvent, and TestWindow types to
eliminate all @typescript-eslint/no-explicit-any violations in
gpt/index.ts and gpt/index.test.ts. Extend GptWindow with
__tsjs_slim_prebid_url so installSlimPrebidLoader avoids the any cast.
Set gam_network_id to 88059007 (autoblog production network). Update
atf_sidebar_ad slot to /88059007/autoblog/news with div_id
ad-atf_sidebar-0-_r_2_ (desktop ATF sidebar, 300x250); restrict
page_patterns to article paths only (/20**, /news/**) since that div
does not exist on the homepage. Add homepage_header_ad slot targeting
/88059007/autoblog/homepage with ad-header-0-_R_jpalubtak5lb_ for
970x90/728x90/970x250 leaderboard formats. Reduce auction_timeout_ms
from 3000 to 500 to cap TTFB at the spec-recommended ceiling.
The bids script set window.__ts_bids but never invoked the
__tsAdInit function, leaving GPT slots undefined and server-side
targeting (hb_pb, hb_bidder) never applied. Both the winning-bid
path (build_bids_script) and the no-auction fallback (html_processor
None branch) now guard-call the function after the assignment.
@prk-Jr prk-Jr self-assigned this Jun 25, 2026
@prk-Jr prk-Jr changed the title Add ts CLI diagnostics for server-side ad templates Add ts CLI ad-template config diagnostics and browser audit Jun 27, 2026
prk-Jr and others added 13 commits June 29, 2026 21:53
…tes-impl

Reconcile the server-side ad-template/auction work with main's EdgeZero
canary rollout, the Axum/Cloudflare/Spin adapters, and the edgezero
into_bytes() repin.

- main(): route to EdgeZero only when the canary rollout selects it and
  the settings carry no creative_opportunity slots (combine both gates).
- Thread the new handle_publisher_request(kv, ec_context, auction)
  parameters through the Axum, Cloudflare, and Spin adapters with an
  empty AuctionDispatch — server-side auction stays deferred there, as on
  the Fastly EdgeZero path.
- Keep HEAD's apply_floor_prices None-price drop and the corrected
  publisher OwnedProcessResponseParams / single stream_publisher_body;
  union the diverged publisher, html_processor, and orchestrator tests.
- Drop the stale into_bytes().unwrap_or_default() test calls and add the
  new HtmlProcessorConfig / PlatformBackendSpec fields and the
  route_request slots argument to the bench and adapter tests.
Blocking:
- Move tokio to [dev-dependencies] in trusted-server-core; it was only used
  by #[tokio::test] and was linking the runtime into the wasm prod build.
  Confirmed the release wasm adapter build no longer pulls tokio.
- Roll back the SPA currentPath on a failed /__ts/page-bids fetch so a
  transient error no longer permanently strands that route (gpt/index.ts).

Build/runtime parity and diagnostics:
- Bound creative-opportunity format width/height to u32 range at build time
  so values the runtime u32 cannot hold are rejected early.
- Add #[serde(deny_unknown_fields)] to the build.rs config stub to match the
  runtime type and reject mistyped table keys at build time.
- Warn when the <body> end-tag handler is absent so a silently non-rendering
  server-side ad feature is diagnosable.
- Log dropped slot bidders that are neither configured nor the aps provider.
- Log build_bid_index collisions (multiple bids per seat/imp).

JS correctness:
- Narrow uid.atype to a number before the range check in sanitizeAuctionUid.
- Resolve findInjectedSlotForRefresh by exact/container match before the
  prefix fallback, with a regression test for prefix-overlapping div_ids.
- Guard the gpt_bootstrap prefix scan against an empty div_id.
- Route injectAdmIntoSlot through findSlotElementByDivId for consistency.

Cleanup and docs:
- Remove the dead has_post_processors routing dependency from
  classify_response_route and (now unused) handle_publisher_request.
- Extract the duplicated EID resolution/consent-gating/device tail shared by
  the initial-page and page-bids dispatch paths into one helper.
- Anchor the surrogate cache-header list in a shared const so the legacy and
  EdgeZero Set-Cookie privacy paths stay aligned.
- Refresh stale docs (PublisherResponse::Stream, the publisher module
  platform-coupling note, and UserInfo.eids consent-gate location).
Moving tokio to trusted-server-core dev-dependencies removed it from the
crate's normal dependency list, so the integration-tests lockfile (which
resolves core's non-dev deps) no longer pins tokio under core. Keeps
`cargo --locked` green for the integration job.
These EdgeZero-style adapters finalize buffered, and the sync
`buffer_publisher_response` drives `stream_publisher_body`, which ignores
`params.dispatched_auction` — so they injected an empty `tsjs.bids = {}`
while Fastly (legacy streaming finalize) served real bids.

- Add `buffer_publisher_response_async` in core: for the Stream variant it
  drives `stream_publisher_body_async`, which awaits
  `collect_dispatched_auction`, writes `ad_bids_state`, and injects the bids
  before `</body>`.
- Pass the configured `creative_opportunities.slot` (not empty) to
  `handle_publisher_request` on all three adapters; it matches them against
  the request path internally.
- Call the async finalize from each adapter (Cloudflare/Spin via their
  now-async `resolve_publisher_response`).

EID targeting stays off for now (these adapters pass `kv: None`).
…ters

The auction consent gate (`consent_allows_server_side_auction`) reads
jurisdiction and TCF consent from the EC context. The adapters passed
`EcContext::default()` to `handle_publisher_request`, leaving jurisdiction
Unknown with no consent — so the gate failed closed and no auction ran
(empty `tsjs.bids`), even though the slots matched.

Build the context via `read_from_request_with_geo` (consent from the
request, geo from the platform), mirroring the Fastly entry point, and fall
back to default on a parse error. Cloudflare resolves geo from the Workers
`cf` object when deployed; Axum and Spin have no-op geo providers, so on
those a known non-GDPR jurisdiction requires the request to carry geo or the
gate needs a TCF consent signal.
When the auction does not run, this pinpoints which gate suppressed it
(slots, bot, navigation, consent, or orchestrator kill switch) instead of
only seeing `dispatch_auction: None`. Pair with the EC-context jurisdiction
log when consent_allows_auction is false.
The EdgeZero buffered path passed empty slots and finalized via the sync
`buffer_publisher_response`, so configured creative-opportunity slots were
routed to the legacy path by `edgezero_can_handle_settings`. Now that
`buffer_publisher_response_async` collects the dispatched auction, EdgeZero
can run the full ad stack:

- Pass the configured `creative_opportunities.slot` and finalize via
  `buffer_publisher_response_async` (the path's `ec.ec_context` already
  carries consent + platform geo). EID targeting stays off (`registry: None`).
- Drop the `edgezero_can_handle_settings` gate, its routing branch, the three
  tests, and the now-unused test settings helpers — EdgeZero handles
  configured slots, so the legacy fallback for them is obsolete.
The EdgeZero publisher path dispatched the auction with registry: None, so
the bid request carried no KV identity-graph EIDs (only client cookie EIDs).
It already passes ec.kv_graph as the identity KV, so wire the matching
PartnerRegistry::from_config(settings.ec.partners) into the AuctionDispatch
to resolve server-side partner EIDs — matching the legacy auction path.

Fastly-only: the sync EC identity graph (KvIdentityGraph/EcKvStore) works on
Fastly's sync KV; the async-KV portability adapters are unaffected (they
still pass registry: None until the EC graph supports async stores).
Resolve the fifth code-review pass. The blocking findings were all
cross-adapter parity gaps in the server-side auction:

- Build the geo-aware EC context in the /auction handlers on Axum,
  Cloudflare, and Spin. They passed EcContext::default(), leaving
  jurisdiction Unknown and failing the consent gate closed even for
  consented users. A shared per-adapter build_ec_context helper now
  serves /auction, page-bids, and the publisher fallback, and logs
  (rather than swallows) a malformed-consent read error.
- Wire GET /__ts/page-bids and its OPTIONS->403 CSRF guard on the
  Fastly EdgeZero path and all three portability adapters, reusing core
  handle_page_bids and a shared page_bids_preflight_denied() helper.
  Previously it was Fastly-legacy-only, so SPA re-auction silently fell
  through to the origin on every other path.
- Add trusted_server_core::response_privacy with the Set-Cookie
  cache-privacy downgrade and the uncacheable-operator-header guard, and
  call it from every adapter's apply_finalize_headers so a shared cache
  (Cloudflare) can no longer serve an operator/origin public Cache-Control
  on a cookie-bearing response.

Also address the inline and non-blocking findings: warn on a dropped
dispatched auction for bodiless responses, extract build_slot_json shared
by the initial-page and page-bids paths, use creative_opportunity_slots()
everywhere, drop the PBS id->ad_id fallback, log APS slot-id collisions,
align the parallel provider parse with the collect path, remove the dead
sync buffer_publisher_response, factor the mediator placeholder request,
drop the unused toml dependency, guard MediaType against a future
serde(default), and document the Fastly-only KV EID enrichment.

JS: dedup win/billing beacons across concurrent renders, add the SSR
guard to installSlimPrebidLoader, and short-circuit waitForSlotElements
on an already-aborted signal. Add regression tests for the currentPath
rollback and the u32::MAX format-dimension rejection.
Adopt main's ts-CLI config model over the branch's build-time embedding:
drop the build.rs config generation and [build-dependencies], accept the
trusted-server.toml deletion, remove the now-orphaned creative_slot_build_check
module, and migrate the [creative_opportunities] example into
trusted-server.example.toml. Slot validation is preserved via
Settings::prepare_runtime, which the CLI runs at config push time.

Adopt main's refactored EdgeZero finalize path in the Fastly adapter and
centralize the per-user Set-Cookie cache-privacy guard in send_edgezero_response
so it covers every EdgeZero send path.

Fix three core test helpers for main's Body::into_bytes -> Option change.
Rebuilds feature/ts-cli-ad-templates fresh on server-side-ad-templates-impl
(which already carries the ts CLI #799 + audit #800 via main), dropping the
redundant ts-cli-base history that caused the merge conflicts.

Adds the ad-template CLI:
- ts audit ad-templates verify — browser/CDP ad-template slot verification
- ts audit page — read-only page summary
- ts config ad-templates — server-side ad-template diagnostics
- shared CLI app-config loader + ad-template evidence/output models

Keeps #800's URL->draft-config bootstrap by relocating it under
audit/generate/ and exposing it as ts audit generate.

Core: extracts the server-side ad-stack gate into
creative_opportunities::evaluate_ad_stack_gate (three-state Yes/No/Unknown)
so the CLI and the runtime publisher path share one gate definition.
@prk-Jr prk-Jr force-pushed the feature/ts-cli-ad-templates branch from 73df61a to 4ec0f3f Compare July 2, 2026 15:27
prk-Jr and others added 8 commits July 2, 2026 23:31
- Stop forwarding client-supplied X-Forwarded-For to Prebid Server;
  synthesize it from the platform-attested client IP instead
- Make the APS slot ID remapping request-scoped by threading the
  AuctionRequest through parse_response_with_context, removing the
  shared provider-instance mutex that concurrent auctions could race
- Guard dispatch_auction against multi-provider fan-out on platforms
  whose HTTP client executes requests sequentially, mirroring
  run_providers_parallel
- Reject negative and non-finite creative-opportunity floor prices at
  config validation
- Re-run the Set-Cookie cache-privacy downgrade after operator response
  headers are applied so configured Set-Cookie plus public cache
  headers cannot produce a shared-cacheable response
- Install Prebid user ID modules immediately when the bundle loads
  after window.load (slim-Prebid lazy path), with a once-only listener
- Drop allow-same-origin from debug ADM fallback iframes and validate
  extracted iframe src schemes to http(s) only
Reconstruct [creative_opportunities] slots from a live page's GPT
registry and gampad/ads requests, and write them into an existing
trusted-server.toml in place, preserving all other sections.

Slots merge across runs: --page-pattern unions patterns into a re-seen
slot, existing slots are preserved, and --replace wipes. Ephemeral
div-id noise (React hashes, -container, hex UUIDs) is normalized to
stable prefixes so verify matches across renders, and TOML keys/strings
are escaped defensively.

Add --cookie to ad-templates generate and verify so a valid
bot-protection clearance cookie can carry the browser audit past an
origin challenge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reconcile the ad-template CLI + generate feature onto the commands/
module layout the base branch adopted from main (ts dev proxy, #798):

- audit/ -> commands/audit/ : mod.rs is the audit namespace; the #800
  draft-gen plus slot reconstruction move under commands/audit/generate/
  (base's duplicate commands/audit/{analyzer,browser_collector} dropped).
- config_ad_templates.rs -> commands/config/ad_templates.rs.
- app_config and ad_templates support modules stay at crate root.
- run.rs/lib.rs wiring merged with the new `ts dev` command.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Roll SPA page-bids navigation `currentPath` back to the last applied
  path instead of the immediately-previous one, so an aborted-then-failed
  navigation can no longer strand a route behind the no-op guard; add a
  regression test.
- Add a concurrent render-bridge test: two same-adId messages before the
  cache fetch resolves must collapse to one fetch (in-flight gate), two beacons.
- Assert `OPTIONS /__ts/page-bids` is denied with 403 on every adapter
  (Axum/Cloudflare/Spin) in cross-adapter parity.
- Add a `u32::MAX` banner-format test covering the imp-drop branch when all
  formats exceed `i32::MAX`.
- Dedup the page-bids GET 403 into `page_bids_preflight_denied()`.
- Fix stale comments/docs: `buffer_publisher_response_async`, soften the
  oversized-body comment, and correct the `firedBeacons` key doc.
Resolve #744 (legacy entry-point removal + streaming publisher) against the
server-side auction: keep the buffered auction path on all adapters, supersede
the streaming publisher for publisher navigation. Verified: fastly/core/spin/
axum/cloudflare tests + clippy + fmt clean.
- Escape page-controlled slot fields and validate the gampad network id
  before splicing scraped values into trusted-server.toml
- Add navigation and teardown timeouts to the verify browser collector
- Snapshot evidence before the scroll pass so load-time entries keep
  phase initial_load; add a Chrome-gated regression fixture
- Cap collector evidence lists in the injected script and after decode
- Preserve CRLF line endings and render non-finite floor_price as valid
  TOML when updating configs in place
- Cover all 128 gate combinations in the core ad-stack mirror test
- Extract slot TOML rendering/merging/splicing into slot_toml.rs
@prk-Jr prk-Jr marked this pull request as ready for review July 7, 2026 16:31

@ChristianPavilonis ChristianPavilonis 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.

Automated review: I reviewed the ad-template CLI diagnostics/generate/verify changes against server-side-ad-templates-impl. I found three non-blocking issues around in-place TOML rewriting and generated-slot defaults that should be addressed, but no blocking correctness/security/data-loss issue severe enough to request changes. CI checks currently shown by GitHub are passing (integration tests, Fastly EC lifecycle, browser integration tests, prepare integration artifacts).

// No section yet — append a fresh one with the network id and slots.
if !existing
.lines()
.any(|line| line.trim() == "[creative_opportunities]")

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.

Automated review: P1 — Inline comments on TOML table headers make the in-place updater append a duplicate section.

This detects [creative_opportunities] with an exact trimmed-string comparison. TOML permits table headers like [creative_opportunities] # ad templates, and load_settings will parse that file successfully, but this splice path treats it as if the section is absent and appends a second [creative_opportunities] table. Because ts audit ad-templates generate writes in place by default, an otherwise valid operator config can be rewritten into invalid TOML. The later header search has the same exact-match assumption.

Suggested fix: parse table headers with comment-aware logic (or, safer, use a TOML document/editing representation for locating tables) so [creative_opportunities] is recognized before any inline # comment, and add a regression test with an inline-commented section header.

// Patterns for slots seen on this run: the `--page-pattern` values, or the
// audited path when none are given (preserving single-page behavior).
let run_patterns: Vec<String> = if page_patterns.is_empty() {
vec![default_page_pattern(&target_url)]

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.

Automated review: P2 — Generated default page patterns use the requested URL even after redirects.

The collector records collected.final_url, and the verifier matches configured slots against the final post-redirect path, but ad-templates generate defaults page_patterns from target_url here. If a publisher URL redirects (for example //news/story or canonicalization), the command scrapes slots from the final page but writes patterns for the pre-redirect path, so the generated config will not match the page that was actually audited.

Suggested fix: when --page-pattern is omitted, parse collected.final_url() and pass that to default_page_pattern (falling back to target_url only if the recorded final URL is invalid). Add a test where the fake collector returns a different final URL.

'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
control if (control as u32) < 0x20 => {

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.

Automated review: P2 — toml_string still emits DEL (U+007F) unescaped.

The generated fields are partly page-controlled (GPT div IDs, unit paths, targeting/bidder params), so this escaping function is the last line of defense before writing trusted-server.toml. It escapes characters below U+0020, but TOML also rejects the DEL control character U+007F in basic strings. A hostile or malformed page value containing %7F/DEL can therefore produce an invalid generated config despite the hardening comment saying control chars are escaped.

Suggested fix: also escape \u007F (for example, handle (control as u32) < 0x20 || control == '\u{7f}') and add a parse-backed regression test for generated TOML containing DEL.

Base automatically changed from server-side-ad-templates-impl to main July 7, 2026 20:08
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.

4 participants