Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
fde55b1
docs: design mobile ad-render trace endpoint
prk-Jr Sep 1, 2026
8899dca
docs: harden mobile ad trace design
prk-Jr Sep 1, 2026
534a691
Add server auction evidence to mobile trace design
prk-Jr Sep 1, 2026
20debdf
Merge branch 'main' into spec/mobile-ad-render-trace-endpoint
prk-Jr Sep 10, 2026
282d78b
Merge branch 'main' into spec/mobile-ad-render-trace-endpoint
prk-Jr Sep 14, 2026
c2aa6be
Merge branch 'main' into spec/mobile-ad-render-trace-endpoint
prk-Jr Sep 19, 2026
e3f371f
Reconcile mobile trace design contracts
prk-Jr Sep 19, 2026
488de2c
Merge branch 'main' into spec/mobile-ad-render-trace-endpoint
prk-Jr Sep 21, 2026
14c274c
Clarify mobile trace design contracts
prk-Jr Sep 21, 2026
7a82a94
Merge branch 'main' into spec/mobile-ad-render-trace-endpoint
aram356 Sep 24, 2026
155d3b0
Clarify trace body validation and browser cleanup
prk-Jr Sep 24, 2026
d361b8e
Merge branch 'main' into spec/mobile-ad-render-trace-endpoint
prk-Jr Sep 24, 2026
c4d664a
Merge branch 'main' into spec/mobile-ad-render-trace-endpoint
aram356 Sep 25, 2026
a180741
Merge branch 'main' into spec/mobile-ad-render-trace-endpoint
prk-Jr Sep 30, 2026
4811d98
Clarify trace failure and export cleanup contracts
prk-Jr Oct 1, 2026
162b3a4
Merge branch 'main' into spec/mobile-ad-render-trace-endpoint
prk-Jr Oct 1, 2026
2028bc6
Clarify mobile trace capture and truncation rules
prk-Jr Oct 2, 2026
20f4a0c
Merge branch 'main' into spec/mobile-ad-render-trace-endpoint
aram356 Oct 4, 2026
750ba8d
Add default-off mobile ad-rendering trace workflow
prk-Jr Oct 6, 2026
e076f77
Link trace release checklist to the repository plan
prk-Jr Oct 6, 2026
b2930d7
Preserve advertising behavior and trace controls
prk-Jr Oct 6, 2026
fb45d1c
Run skipped-page trace regression in CI
prk-Jr Oct 6, 2026
f6c3bd5
Record CodeQL test-fixture triage
prk-Jr Oct 6, 2026
b7e48c7
Merge branch 'main' into spec/mobile-ad-render-trace-endpoint
prk-Jr Oct 6, 2026
6e9e7c4
Bound trace capture cost and complete review corrections
prk-Jr Oct 6, 2026
e5becfc
Treat only commas that border reserved cookies as ambiguous
prk-Jr Oct 7, 2026
e1b062a
Lead the trace viewer with what happened and per-slot cards
prk-Jr Oct 7, 2026
dc69eb4
Pin trace path classification across Fastly request conversion
prk-Jr Oct 8, 2026
285c75f
Merge main into spec/mobile-ad-render-trace-endpoint
prk-Jr Oct 8, 2026
117373e
Merge branch 'main' into spec/mobile-ad-render-trace-endpoint
prk-Jr Oct 9, 2026
92dd203
Authenticate forwarded public origins before request dispatch
prk-Jr Oct 9, 2026
cf36844
Document random forwarder tokens and temporary digest comparison
prk-Jr Oct 9, 2026
5c66d64
Merge branch 'main' into spec/mobile-ad-render-trace-endpoint
aram356 Oct 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 103 additions & 14 deletions .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: "Integration Tests"
name: 'Integration Tests'

permissions:
contents: read
Expand Down Expand Up @@ -30,11 +30,14 @@ jobs:
uses: ./.github/actions/setup-integration-test-env
with:
origin-port: ${{ env.ORIGIN_PORT }}
install-viceroy: "false"
build-cloudflare: "true"
install-viceroy: 'false'
build-cloudflare: 'true'

- name: Generate integration Viceroy configs
run: ./scripts/generate-integration-viceroy-configs.sh
run: |
./scripts/generate-integration-viceroy-configs.sh
INTEGRATION_APP_CONFIG_PATH=crates/trusted-server-integration-tests/fixtures/configs/trusted-server.trace.toml INTEGRATION_BIDDER_ORIGIN_URL="http://127.0.0.1:$ORIGIN_PORT" ARTIFACTS_DIR="$ARTIFACTS_DIR/trace" ./scripts/generate-integration-viceroy-configs.sh
INTEGRATION_APP_CONFIG_PATH=crates/trusted-server-integration-tests/fixtures/configs/trusted-server.trace-auth.toml ARTIFACTS_DIR="$ARTIFACTS_DIR/trace-auth" ./scripts/generate-integration-viceroy-configs.sh
env:
INTEGRATION_ORIGIN_PORT: ${{ env.ORIGIN_PORT }}

Expand Down Expand Up @@ -68,10 +71,10 @@ jobs:
uses: ./.github/actions/setup-integration-test-env
with:
origin-port: ${{ env.ORIGIN_PORT }}
install-viceroy: "true"
build-wasm: "false"
build-axum: "false"
build-test-images: "false"
install-viceroy: 'true'
build-wasm: 'false'
build-axum: 'false'
build-test-images: 'false'

- name: Download integration test artifacts
uses: actions/download-artifact@v4
Expand Down Expand Up @@ -105,6 +108,7 @@ jobs:
--target x86_64-unknown-linux-gnu
-- --include-ignored
--skip test_wordpress_fastly --skip test_nextjs_fastly
--skip trace_browser_workflow --skip trace_runtime_boundary
--test-threads=1
env:
WASM_BINARY_PATH: ${{ env.WASM_ARTIFACT_PATH }}
Expand All @@ -127,9 +131,9 @@ jobs:
uses: ./.github/actions/setup-integration-test-env
with:
origin-port: ${{ env.ORIGIN_PORT }}
install-viceroy: "true"
build-wasm: "false"
build-test-images: "false"
install-viceroy: 'true'
build-wasm: 'false'
build-test-images: 'false'

- name: Download integration test artifacts
uses: actions/download-artifact@v4
Expand All @@ -152,6 +156,83 @@ jobs:
VICEROY_CONFIG_PATH: ${{ env.ARTIFACTS_DIR }}/configs/viceroy.toml
RUST_LOG: info

trace-runtime-tests:
name: trace runtime and browser acceptance
needs: prepare-artifacts
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v4

- name: Set up trace test runtime
id: shared-setup
uses: ./.github/actions/setup-integration-test-env
with:
origin-port: ${{ env.ORIGIN_PORT }}
install-viceroy: 'true'
build-wasm: 'false'
build-axum: 'false'
build-test-images: 'false'

- name: Download integration test artifacts
uses: actions/download-artifact@v4
with:
name: integration-test-artifacts
path: ${{ env.ARTIFACTS_DIR }}

- name: Restore runtime binaries and framework images
run: |
chmod +x "$AXUM_ARTIFACT_PATH"
mkdir -p crates/trusted-server-adapter-cloudflare/build
cp -r "$CF_BUILD_ARTIFACT_PATH/." crates/trusted-server-adapter-cloudflare/build/
docker load --input "$DOCKER_ARTIFACT_PATH"

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ steps.shared-setup.outputs.node-version }}
cache: npm
cache-dependency-path: |
crates/trusted-server-integration-tests/browser/package-lock.json
crates/trusted-server-js/lib/package-lock.json

- name: Install browser dependencies
run: |
npm ci --prefix crates/trusted-server-js/lib
npm ci --prefix crates/trusted-server-integration-tests/browser
cd crates/trusted-server-integration-tests/browser
npx playwright install --with-deps chromium
npm install -g wrangler@4.83.0

- name: Install the verified Spin runtime
run: |
trace_spin_dir="$RUNNER_TEMP/trace-spin"
mkdir -p "$trace_spin_dir"
curl --fail --location --silent --show-error https://github.com/spinframework/spin/releases/download/v4.0.0/spin-v4.0.0-linux-amd64.tar.gz --output "$trace_spin_dir/spin.tar.gz"

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.

♻️ refactor — Pin the Spin tarball digest

This step is named "Install the verified Spin runtime", but nothing verifies the download, and the acceptance job then executes that binary. Pinning the SHA-256 from Spin's checksums-v4.0.0.txt release asset makes a replaced or corrupted tarball fail closed:

Suggested change
curl --fail --location --silent --show-error https://github.com/spinframework/spin/releases/download/v4.0.0/spin-v4.0.0-linux-amd64.tar.gz --output "$trace_spin_dir/spin.tar.gz"
curl --fail --location --silent --show-error https://github.com/spinframework/spin/releases/download/v4.0.0/spin-v4.0.0-linux-amd64.tar.gz --output "$trace_spin_dir/spin.tar.gz"
echo "e705c9bfd9484a9175f392a116856680862a40f31d1a85618bed331f34ffccfa $trace_spin_dir/spin.tar.gz" | sha256sum --check --strict

tar -xzf "$trace_spin_dir/spin.tar.gz" -C "$trace_spin_dir" spin
echo "$trace_spin_dir" >> "$GITHUB_PATH"

- name: Build the production Spin component
run: cargo build --package trusted-server-adapter-spin --target wasm32-wasip1 --features spin --release

- name: Run trace acceptance against prepared runtimes
run: |
cargo test --manifest-path crates/trusted-server-integration-tests/Cargo.toml --target x86_64-unknown-linux-gnu --test integration trace_runtime_boundary -- --ignored --test-threads=1 --nocapture
cargo test --manifest-path crates/trusted-server-integration-tests/Cargo.toml --target x86_64-unknown-linux-gnu --test integration trace_browser_workflow -- --ignored --test-threads=1 --nocapture
env:
WASM_BINARY_PATH: ${{ env.WASM_ARTIFACT_PATH }}
AXUM_BINARY_PATH: ${{ env.AXUM_ARTIFACT_PATH }}
CLOUDFLARE_WRANGLER_DIR: ${{ github.workspace }}/crates/trusted-server-adapter-cloudflare
INTEGRATION_ORIGIN_PORT: ${{ env.ORIGIN_PORT }}

- name: Upload trace browser evidence
uses: actions/upload-artifact@v4
if: always()
with:
name: trace-runtime-browser-evidence
path: crates/trusted-server-integration-tests/browser/test-results/trace-runtime-*/
retention-days: 7

browser-tests:
name: browser integration tests
needs: prepare-artifacts
Expand All @@ -165,9 +246,9 @@ jobs:
uses: ./.github/actions/setup-integration-test-env
with:
origin-port: ${{ env.ORIGIN_PORT }}
install-viceroy: "true"
build-wasm: "false"
build-test-images: "false"
install-viceroy: 'true'
build-wasm: 'false'
build-test-images: 'false'

- name: Download integration test artifacts
uses: actions/download-artifact@v4
Expand Down Expand Up @@ -204,6 +285,10 @@ jobs:
working-directory: crates/trusted-server-integration-tests/browser
run: node --test initial-render/pages.test.cjs

- name: Test trace fixture cleanup
working-directory: crates/trusted-server-integration-tests/browser
run: node --test trace-fixture.test.cjs

- name: Test initial render ownership
working-directory: crates/trusted-server-integration-tests/browser
# Let the runner build its Rubicon + Shared ID artifact; the bidder-only
Expand All @@ -225,6 +310,8 @@ jobs:
WASM_BINARY_PATH: ${{ env.WASM_ARTIFACT_PATH }}
INTEGRATION_ORIGIN_PORT: ${{ env.ORIGIN_PORT }}
VICEROY_CONFIG_PATH: ${{ env.ARTIFACTS_DIR }}/configs/viceroy.toml
TRACE_VICEROY_CONFIG_PATH: ${{ env.ARTIFACTS_DIR }}/trace/configs/viceroy.toml
TRACE_AUTH_VICEROY_CONFIG_PATH: ${{ env.ARTIFACTS_DIR }}/trace-auth/configs/viceroy.toml
TEST_FRAMEWORK: nextjs
PLAYWRIGHT_HTML_REPORT: playwright-report-nextjs
run: npx playwright test
Expand All @@ -244,6 +331,8 @@ jobs:
WASM_BINARY_PATH: ${{ env.WASM_ARTIFACT_PATH }}
INTEGRATION_ORIGIN_PORT: ${{ env.ORIGIN_PORT }}
VICEROY_CONFIG_PATH: ${{ env.ARTIFACTS_DIR }}/configs/viceroy.toml
TRACE_VICEROY_CONFIG_PATH: ${{ env.ARTIFACTS_DIR }}/trace/configs/viceroy.toml
TRACE_AUTH_VICEROY_CONFIG_PATH: ${{ env.ARTIFACTS_DIR }}/trace-auth/configs/viceroy.toml
TEST_FRAMEWORK: wordpress
PLAYWRIGHT_HTML_REPORT: playwright-report-wordpress
run: npx playwright test
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ jobs:
- name: Run tests
run: cargo test-fastly

- name: Run trace emitted-script regressions
run: |
cargo test -p trusted-server-core --target x86_64-unknown-linux-gnu --lib trace_document_skipped_emitted_script_leaves_ad_state_untouched -- --ignored
cargo test -p trusted-server-core --target x86_64-unknown-linux-gnu --lib trace_document_emitted_script_deep_freezes_owned_context_in_node -- --ignored

- name: Run tests (reusable-sandbox feature)
run: cargo test-fastly-reuse

Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- Explicit TS Console activation with `?ts_console=1` now sets a 30-minute diagnostics cookie instead of a browser-session cookie, independently of the mobile trace flag. Ordinary requests do not refresh it; end and explicitly enable diagnostics again to adopt the lifetime for an existing session.
- Publisher documents with diagnostics active now use private, no-store responses and strip conditional and range request headers before fetching the origin. An unexpected origin `304` becomes a private `502` because it cannot supply an instrumented document, including when mobile tracing is disabled.
- **Breaking:** `ts prebid bundle` is now `ts prebid client`, alongside the new `ts prebid server` namespace. Update scripts and runbooks to use `ts prebid client` with the same arguments. The old `bundle` spelling is no longer accepted and has no compatibility alias.
- The S2S `/_ts/api/v1/batch-sync` endpoint now validates the full batch and calls the CAS-protected update path once per distinct normalized EC ID. The last valid UID wins within a group, and infrastructure failures reject the failing and each unprocessed group, so accepted and `kv_unavailable` input indexes may interleave.
- **Breaking:** Auction providers and bidder routes now use the configuration-first `[auction.providers.<id>]` and `[auction.bidders.<id>]` maps. The removed `[auction].providers = [...]` list and removed server fields under `[integrations.prebid]` and `[integrations.aps]` are rejected even when those integrations are disabled, and `ts config push` rejects the old shape before publication. Move PBS `server_url` to provider `endpoint`, server timeout to provider `timeout_ms`, request controls and bidder-parameter overrides to the `prebid-server` `profile_config`, notification suppression to `notifications`, and each former server bidder to an `[auction.bidders.<id>]` route. Move APS endpoint, timeout, account, inventory, debug, and creative controls to an `aps` provider and its `profile_config`. Browser Prebid settings remain under `[integrations.prebid]`; values such as timeout and debug that previously affected both browser and server behavior must now be configured for each owner. Provider endpoints must be absolute HTTPS URLs. Only bidder codes present in `[auction.bidders]` are folded into Trusted Server requests; unlisted publisher bids remain native browser demand. Provider response names now use the configured provider ID, such as `pbs-main`, instead of the legacy literal `prebid`; audit consumers that match `AuctionResponse.provider`. This schema has no mixed-version-safe deployment order: old binaries reject the maps and new binaries reject the retired fields, so activate the new binary and config blob together. Rollbacks must restore an old-schema blob together with the old binary.
Expand All @@ -27,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Security

- Reserve the application-visible `/_ts/trace*` prefix locally on every adapter, including when mobile tracing is disabled. Existing authentication runs first; disabled or unknown trace routes then return a local `404` instead of forwarding to the publisher.

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.

🔧 wrench — Record the forwarding trust change and the new [trusted_forwarder] section

This PR changes what decides the public host and scheme on every adapter, and the changelog doesn't say so:

  • RequestInfo::from_request no longer reads unauthenticated Forwarded / X-Forwarded-Host / X-Forwarded-Proto (the fallbacks and test_request_info_x_forwarded_host_precedence / test_request_info_chained_proxy_scenario are removed in http_util.rs), and prepare_trusted_forwarder strips all three before routing on every adapter.
  • On main, Fastly and Spin already stripped them, but Axum and Cloudflare did not, so a client-supplied X-Forwarded-Host steered first-party URL rewriting there. Closing that is a security fix worth recording.
  • The operator-visible side: Axum's EdgeZero ingress origin is always http://<Host>, so an Axum service behind a TLS-terminating proxy — including ts dev proxy against a local Axum — now emits http:// first-party URLs unless [trusted_forwarder] is configured.
  • [trusted_forwarder] is a new top-level section with a secret-store reference, but ### Added only mentions trace_page_enabled.

Suggested entry under ### Security, covering both the fix and the opt-in:

Suggested change
- Reserve the application-visible `/_ts/trace*` prefix locally on every adapter, including when mobile tracing is disabled. Existing authentication runs first; disabled or unknown trace routes then return a local `404` instead of forwarding to the publisher.
- Reserve the application-visible `/_ts/trace*` prefix locally on every adapter, including when mobile tracing is disabled. Existing authentication runs first; disabled or unknown trace routes then return a local `404` instead of forwarding to the publisher.
- Stop deriving the public host and scheme from unauthenticated `Forwarded`, `X-Forwarded-Host` and `X-Forwarded-Proto` fields on every adapter, and strip those fields before routing. Fastly and Spin already removed them; Axum and Cloudflare previously let a client-supplied value steer first-party URL rewriting. A deployment behind a TLS-terminating or Host-rewriting proxy, including `ts dev proxy` against Axum, must now configure the opt-in `[trusted_forwarder]` section, which authenticates one publisher-domain-bounded `X-Forwarded-Host`/`X-Forwarded-Proto` pair with a secret-store-resolved shared secret; without it, first-party URLs use the transport host and scheme.

- `/first-party/sign` now rejects valid targets outside `proxy.allowed_domains` before minting a proxy token. The creative runtime keeps image and iframe assignments blocked after this `403` policy response instead of loading the rejected URL directly; fetch-time checks still cover the initial target and every redirect.
- Reserved the complete admin namespace at the publisher-fallback boundary. Percent-encoded separators (`/_ts/admin%2Fec`, `%2f`, and double-encoded forms) matched the `^/_ts/admin` Basic-auth handler but escaped the literal-slash namespace check, so an authenticated request fell through to publisher fallback and forwarded its `Authorization` header and body to the publisher origin. The reservation now spans the whole `/_ts/admin` prefix plus the retired `/admin/keys` aliases — including trailing, descendant, and encoded-separator forms — evaluated on the raw path and on each of its bounded percent-decodings, so multi-encoded separators such as `/admin%252Fkeys/rotate` cannot survive to fallback for a proxy or origin to decode again, and applies to every adapter.
- Validate synthetic ID format on inbound values from the `x-synthetic-id` header and `synthetic_id` cookie; values that do not match the expected format (`64-hex-hmac.6-alphanumeric-suffix`) are discarded and a fresh ID is generated rather than forwarded to response headers, cookies, or third-party APIs
Expand All @@ -41,6 +44,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Add opt-in mobile ad-render tracing through `[integrations.gpt_diagnostics].trace_page_enabled` (default `false`, requiring GPT diagnostics). The same-tab `/_ts/trace` viewer presents bounded, redacted browser-local request, auction and GPT observations with copy, download, share and independent local/server cleanup controls. See the GPT diagnostics guide for deployment and rollout checks.
- Added the `[auction].rewrite_creatives` (default `true`) and `[auction].sanitize_creatives` (default `false`) options. `rewrite_creatives` rewrites winning-bid adm to first-party endpoints across `POST /auction` and publisher SSAT/page-bids delivery (proxy/click URL conversion, bidder `<base>` removal; creative TSJS injection on `POST /auction` only). Enabling `sanitize_creatives` strips executable markup from winning-bid adm before delivery.
- `creative_opportunities.slot.gam_unit_path` is now a template supporting `{network_id}`, `{slot_id}`, and `{section}`, so a publisher whose ad unit varies by site section expresses it in one slot rule instead of one per (slot × section). `{section}` derives from the request path: `[creative_opportunities].section_segment` selects which path segment names the section (0-based, default `0`; set `1` for locale-prefixed URLs), and `section_root` supplies the value for paths with no such segment. `section_root` is required when a template uses `{section}`. Existing static and absent `gam_unit_path` configs are unchanged. Startup rejects a blank `gam_network_id` only when an absent/default path or `{network_id}` template consumes it. Trusted Server conservatively caps whole rendered dynamic paths at 100 UTF-8 bytes, informed by Google's 100-character per-ad-unit-code limit; an over-limit request-specific path omits that slot without failing the response. During typed/startup finalization, every placeholder-bearing template that omits `section_segment` materializes `section_segment = 0`, so an older binary rejects the blob loudly. Static and absent paths remain legacy-schema compatible only when both `section_root` and `section_segment` are omitted. Before rolling back below this feature, replace or remove dynamic paths, remove both keys, re-push and finalize the config, then roll back the binary.
- Added opt-in APS HTTP debug metadata for controlled test sites, exposing the direct request and response under `/auction` provider metadata using the Prebid Server `debug.httpcalls` shape.
Expand Down
23 changes: 15 additions & 8 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading