Skip to content

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

Open
prk-Jr wants to merge 160 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 160 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>.

closes #701

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 and others added 12 commits June 29, 2026 22:56
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).

Comment thread crates/trusted-server-cli/src/commands/audit/generate/slot_toml.rs Outdated
Comment thread crates/trusted-server-cli/src/commands/audit/generate/mod.rs Outdated
Comment thread crates/trusted-server-cli/src/commands/audit/generate/slot_toml.rs Outdated
Base automatically changed from server-side-ad-templates-impl to main July 7, 2026 20:08
prk-Jr added 2 commits July 8, 2026 11:23
- Recognize [creative_opportunities] table headers carrying inline
  comments in the in-place splice and replace_key_in_section, so a
  valid operator config is updated instead of gaining a duplicate
  section
- Default generated page_patterns from the recorded post-redirect
  final URL instead of the requested URL, falling back to the
  requested URL when the recorded final URL is invalid
- Escape DEL (U+007F) in toml_string, which TOML basic strings
  reject alongside chars below U+0020

Each fix carries a parse-backed regression test.
Main squash-merged the same server-side ad-templates feature (#680),
then added Tinybird auction telemetry (#818), edgezero v0.0.4 pins
(#862), and external first-party Prebid bundle loading (#743).

Resolutions:
- Files where this branch matched the impl branch tip take main's
  version (telemetry/prebid-bundle additions on top of identical
  feature code): orchestrator, endpoints, openrtb, prebid integration,
  fastly app, .gitignore, prebid docs
- creative_opportunities.rs keeps this branch's version (adds the
  shared ad-stack gate and pattern machinery used by the ts CLI;
  main's copy was identical minus those additions)
- publisher.rs takes main's version with this branch's
  should_run_server_side_ad_stack delegation to
  evaluate_ad_stack_gate re-applied
- run.rs keeps both sides' CLI tests (ad-templates/audit + prebid
  bundle)
@prk-Jr prk-Jr requested a review from ChristianPavilonis July 8, 2026 08:58

@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 PR #823 against main, focusing on the new ts CLI ad-template diagnostics, browser audit/generate paths, TOML rewriting, and the shared runtime gate. CI is currently green. I did not find a blocking runtime/security issue requiring REQUEST_CHANGES, but I found several high-confidence non-blocking correctness and compatibility issues called out inline. Existing earlier automated comments about inline-commented TOML headers, redirected default page patterns, and DEL escaping appear to have been addressed in the current head, so I did not repeat them.

match &args.command {
Some(AuditSubcommand::Page(page_args)) => page::run_page(page_args),
Some(AuditSubcommand::AdTemplates(AuditAdTemplatesCommand::Generate(gen_args))) => {
let loaded = crate::app_config::load_settings(&gen_args.config)?;

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 — ad-templates generate can persist env-overlay config back into the raw file.

load_settings applies the EdgeZero environment overlay by default, and this path passes the resulting effective creative_opportunities into the in-place merge while writing loaded.app_config_path. That means environment-provided slots / bidder params / network IDs can be copied into trusted-server.toml, or can replace/merge over the file's real slots, even though the operator is asking to rewrite the local config file. In Trusted Server deployments those overlays are often environment-specific; persisting them can corrupt the checked local config and may leak env-only ad parameters into a file.

Suggested fix: resolve the app-config path with the shared flags, but load the merge base for this mutating command with env_overlay = false (or reject unless --no-env is set). Keep env overlays for read-only diagnostics/verify where an effective view is expected. Add a regression test where an env overlay contributes a slot and generate --dry-run does not write that slot unless it exists in the raw file.

report_error("target config has no [creative_opportunities] section to update")
})?;

let is_slot_table = |line: &str| {

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 slot = [...] configs are left behind and make the rewritten TOML invalid.

The runtime config intentionally accepts slot via vec_from_seq_or_map, so a valid config can use slot = [{ ... }] (or map form) under [creative_opportunities]. The splice logic only treats [[creative_opportunities.slot]] and provider subtables as existing slots, so it inserts the generated [[creative_opportunities.slot]] blocks after the scalar block while leaving the old slot = ... key in place. TOML then rejects the result because creative_opportunities.slot was already defined as a value before being used as an array of tables.

Suggested fix: when splicing, detect a slot = ... key in the [creative_opportunities] scalar block and either remove/replace that whole value or fail with a clear message asking the operator to normalize to array-of-tables before generation. A TOML document/editing library would make this safer than line splicing. Add coverage for both inline array and map forms.

generate::run_generate(generate_args, &collector, &mut out)
}
None => match &args.legacy_url {
Some(url) => page::run_page_url(url, false),

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 — the legacy ts audit <url> command now silently does a different operation.

On main, ts audit <url> bootstraps js-assets.toml and a draft trusted-server.toml by default, with flags such as --no-config, --js-assets, --config, and --force. This PR leaves ts audit <url> as a successful hidden alias, but routes it to the read-only page summary, so existing onboarding scripts can exit 0 while producing no config artifacts. The README still describes ts audit https://publisher.example as the bootstrap command, which makes the silent behavior change especially easy to miss.

Suggested fix: preserve the old artifact-generation behavior for the bare alias until a deprecation window expires, or make the bare alias fail with a migration message pointing to ts audit generate <url> instead of silently succeeding as page. Also update the public docs/README examples if the command shape intentionally changed.

}

/// Derives a slot id from a div id by stripping the common GPT prefix.
fn slot_id_from_div(div_id: &str) -> String {

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 slot IDs can fail Trusted Server's own runtime validation.

slot_id_from_div strips div-gpt-ad- and otherwise copies the page-controlled div ID directly into the config id. Runtime validation later requires slot IDs to contain only [A-Za-z0-9_-], while real GPT div IDs often contain characters like ., :, spaces, or other framework separators. In those cases generate writes TOML successfully, but ts config validate / runtime preparation rejects the generated config even though the raw div_id itself could have remained usable as a prefix.

Suggested fix: derive a separate safe slot id (sanitize invalid characters, collapse repeats, ensure non-empty, and make it unique), while preserving the original/normalized div stem in div_id. Prefer reusing validate_slot_id in the generator path and warning/skipping only if a safe ID cannot be produced.

impl RenderSlot {
/// The stable identity used to match slots across runs: the div id (or slot
/// id), with any trailing `-` trimmed so hand-authored stems still match.
fn key(&self) -> String {

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 — rerunning generate does not merge configured div-prefix slots with their live divs.

The config model treats div_id as a prefix, and the PR docs even show div_id = "ad-atf-" matching live IDs such as ad-atf-0. This merge key only trims trailing hyphens and then requires equality, so a hand-reviewed prefix (ad-atf-) will not match the next discovered live div (ad-atf-0). A follow-up generate run appends a duplicate slot instead of unioning the new page pattern into the existing configured slot.

Suggested fix: when comparing a discovered slot to existing slots, treat an existing div_id as a prefix candidate (or reuse the verifier/runtime prefix matching rules) before falling back to the exact normalized key. Add a regression test with an existing prefix and a discovered suffixed div.

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.

Build a standalone validation tool for creative-opportunities.toml

4 participants