Skip to content

Allow consented first-party relay auctions under TCF - #1498

Open
ChristianPavilonis wants to merge 3 commits into
mainfrom
codex/1497-prebid-gdpr-relay
Open

ChristianPavilonis wants to merge 3 commits into
mainfrom
codex/1497-prebid-gdpr-relay

Conversation

@ChristianPavilonis

@ChristianPavilonis ChristianPavilonis commented Nov 4, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

With GDPR applicable and Purpose 2 consent granted, Prebid can block trustedServer before calling /auction because the first-party relay has no GVL vendor identity. This PR makes the relay's basic-ads soft vendor exception an explicit testing opt-in rather than applying it to every deployment.

  • Add integrations.prebid.relay_soft_vendor_exception, defaulting to false, and inject it as window.__tsjs_prebid.relaySoftVendorException.
  • Only when explicitly enabled, add trustedServer to the basicAds rule's softVendorExceptions before relay auctions. Keep Purpose 2 enforcement, unrelated bidders' consent checks, publisher rules, and publisher-supplied GVL identity enforcement.
  • With the setting omitted or false, leave publisher consent rules unchanged. Legacy top-level TCF configuration remains unchanged.
  • Forward original consent signals to downstream auction endpoints, which remain responsible for individual vendor enforcement.

Deployment setting

Production deployments should leave the exception disabled:

[integrations.prebid]
relay_soft_vendor_exception = false

Omitting the setting also means false. Set it to true only to opt into the relay exception for testing. Without an appropriate publisher-configured relay GVL identity or exception, normal TCF vendor enforcement can still block relay auctions. This option does not set Prebid Server's test_mode or make traffic non-billable. No production deployment configuration was changed by this PR.

Changes

  • Rust browser configuration and head injection carry the flag from operator config to the existing Prebid shim.
  • The TypeScript shim gates the relay consent guard on explicit true.
  • Rust tests cover omitted, false, and true through planned head injection, with the auction plan both enabled and disabled.
  • Real production-artifact Prebid tests cover omitted/disabled settings without rule mutation, enabled relay requests, Purpose 2 denial, mapped vendor denial, and unrelated bidders.
  • The integration guide and example config document the default and testing opt-in.

Verification

  • Full JavaScript suite: 81 files, 3,314 tests; no type errors.
  • Focused relay guard and real Prebid artifact tests: 26 tests.
  • Full native core suite: 3,364 passed, 2 ignored, including 182 Prebid tests.
  • JavaScript build, ESLint, and formatting; Rust formatting; docs and repository Markdown formatting.
  • Fastly, reusable Fastly sandbox, Axum, Cloudflare, and Spin adapter test aliases.
  • Native core Clippy and all eight target-specific Clippy aliases.
  • Cross-adapter parity: 29 tests; native template build-digest: 10 tests and lint.
  • Fastly deadline tests under Viceroy 0.21.0 and Cloudflare/Spin runtime deadline fixtures, including fixture builds and lint. Spin runtime: 4.1.0.
  • Docs-proposal scripts and ShellCheck, release-script tests/lint, domain and WASM clock lints, and CI Node pin checks.

GitHub CI is still running. All listed local checks passed against commit f22a83fce.

The earlier live Chrome reproduction showed that temporarily applying the source guard restored /auction requests without changing CMP consent. That was a local browser patch, not a deployment. The new configuration opt-in has not been exercised in a live production deployment.

Closes #1497.

@ChristianPavilonis ChristianPavilonis self-assigned this Nov 4, 2026
ChristianPavilonis added a commit that referenced this pull request Nov 4, 2026
Backport Trusted Server PR #1498 to the EdgeZero RC demo branch. Keep the shim size regression bound matched to this branch, which does not include the newer trace implementation.

(cherry picked from commit 4b63348)

@aram356 aram356 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please make softVendorExceptions be config This should on for testing TS

@ChristianPavilonis

Copy link
Copy Markdown
Collaborator Author

Addressed in f22a83f. Added integrations.prebid.relay_soft_vendor_exception, defaulting to false. Set it to true to opt into the relay soft vendor exception for testing; production should leave it false or omitted. The existing server-injected config carries the flag to the shim. Tests cover omitted, false, and true, including actual Prebid enforcement of Purpose 2 and publisher GVL mappings. Updated the PR description, integration guide, and example config. This option does not enable non-billable test traffic.

@ChristianPavilonis

Copy link
Copy Markdown
Collaborator Author

Included the current feature delta from this PR at f22a83fceac98767979ac0dd406701e4a000c1db in compatible-baseline port #1545 and draft RC #1541, without merging newer-main ancestry or changing this PR's head/base/readiness.

The baseline replay reproduced the known automatic-CMP ownership regression with the testing flag explicitly enabled: a later publisher denial plus stale CMP grant produced a stubbed identity request/cookie. Port #1545 carries the existing guarded-setter correction and normal/deferred/throwing-subscriber regressions; this extra correction is not claimed present on this source branch. The default-off/testing-only setting and Purpose2/GVL controls remain unchanged. The port's45 focused artifact/consent cases and180 Rust Prebid cases pass, as do31 broader local gates and a fresh container smoke. Hosted checks for the port/advanced RC are pending; no deployment or production privacy acceptance is claimed.

@aram356
aram356 requested review from dhruv8sh and prk-Jr November 12, 2026 05:26

@aram356 aram356 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Summary

This round makes the relay basic-ads soft vendor exception an explicit testing opt-in (integrations.prebid.relay_soft_vendor_exception, default off), as requested last round. The config plumbing, the default, and the Prebid Purpose 2 and GVL-mapping enforcement hold up. One blocking issue remains: with the flag on and managed User IDs configured, the relay rule write breaks the shim's automatic CMP ownership handoff, and a later publisher GDPR denial can be overridden by a CMP grant.

1 of the inline comments below carries a one-click GitHub suggestion. Use Commit suggestion to apply it as a commit on the PR branch. The other comments describe the fix in prose because the change touches multiple files or lines outside the diff and can't be auto-applied.

Blocking

🔧 wrench

  • Relay rule writes go through the managed-ID ownership wrapper: see inline at crates/trusted-server-js/lib/src/integrations/prebid/index.ts:2487
  • Two CI checks failed: see Cross-cutting below

Non-blocking

🤔 thinking / ⛏ nitpick / 📝 note

  • 🤔 Leave the key out of the template: see inline at trusted-server.example.toml:533
  • ⛏ Keep the measured sizes in the budget comment: see inline at crates/trusted-server-js/lib/test/prebid-artifact-integration.test.mjs:415
  • ⛏ Configuration reference and CHANGELOG: see Cross-cutting below
  • 📝 Description bullet with no code change: see Cross-cutting below

Cross-cutting / body-level findings

  • 🔧 Two CI checks failed: browser integration tests and trace runtime and browser acceptance both failed while installing browser packages (Temporary failure resolving 'azure.archive.ubuntu.com'), before any test ran. Neither is required by branch protection, and the cause is the runner's apt mirror, not this diff. Please re-run both once the mirror recovers.
  • ⛏ Configuration reference and CHANGELOG: the Prebid browser-field table in docs/guide/configuration.md (around line 2005) is the full configuration reference but has no relay_soft_vendor_exception row; only the integration guide was updated. CHANGELOG.md [Unreleased] also has no Added entry for the key. That entry is the place to say it is testing-only, defaults to false, and is rejected by older binaries, so it must be removed before a rollback.
  • 📝 Description bullet with no code change: "Forward original consent signals to downstream auction endpoints" describes existing server behavior; nothing in this diff changes forwarding. The claim itself holds: /auction fails closed on TCF Purpose 1 for GDPR traffic and forwards the TC string to PBS.

CI Status

  • Analyze (actions): PASS
  • Analyze (javascript-typescript): PASS
  • Analyze (python): PASS
  • Analyze (rust): PASS
  • browser integration tests: FAIL
  • cargo check (cloudflare native + wasm32-unknown-unknown): PASS
  • cargo check/build/test (spin native + wasm32-wasip1): PASS
  • cargo fmt: PASS (required)
  • cargo test: PASS (required)
  • cargo test (axum native): PASS
  • cargo test (cross-adapter parity): PASS
  • cargo test (ts CLI, native) (macos-latest): PASS
  • cargo test (ts CLI, native) (ubuntu-latest): PASS
  • CI Node pin guard: PASS
  • CLAUDE.md symlink guard: PASS
  • CodeQL: PASS
  • docs-proposal scripts: PASS
  • format-docs: PASS (required)
  • format-typescript: PASS (required)
  • HTML EOF runtime (Fastly): PASS
  • integration tests: PASS
  • integration tests (Fastly EC lifecycle): PASS
  • prepare integration artifacts: PASS
  • release scripts: PASS
  • trace runtime and browser acceptance: FAIL
  • ts dev lint domains (changed lines): PASS
  • vitest: PASS
  • worker-deadlines (cloudflare): PASS
  • worker-deadlines (spin): PASS

const prepareRelayConsent = createRelayConsentGuard(
ADAPTER_CODE,
(key) => (pbjs as unknown as { getConfig?: PrebidGetConfig }).getConfig?.(key),
(value) => pbjs.setConfig(value as PbjsConfig)

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: Relay rule writes go through the managed-ID ownership wrapper

With managed_user_ids configured and a CMP on the page, the shim activates IAB consent itself and replaces pbjs.setConfig with a wrapper that treats any consentManagement.gdpr write as a publisher takeover. This setter resolves to that wrapper at call time, so the first non-empty auction with relay_soft_vendor_exception = true runs this sequence:

  1. retireAutomaticTcfConsent reads the relay rules as publisher ownership. It sets acceptCmpEvents = false and writes { gdpr: { enabled: false } }.
  2. The rules write turns cmpApi: 'iab' back on. Prebid 10.26 setConsentConfig calls loadConsentData() synchronously, so Prebid immediately opens a new __tcfapi addEventListener subscription on the unguarded API.
  3. A later publisher setConfig with a static GDPR denial finds no automatic owner left to retire. The next CMP event on the unguarded subscription overwrites the publisher's denial.

Repro on f22a83f: apply the three relay cases from the #1545 port of prebid-consent-enforcement.test.mjs unchanged (git diff <this head> <#1545 head> -- crates/trusted-server-js/lib/test/prebid-consent-enforcement.test.mjs | git apply). 4 of the 5 runs fail. After the relay auction and a static Purpose 1 / vendor 97 denial, IdentityLink still requests the LiveRamp envelope and writes idl_env. The checkpoint log shows the writes {"gdpr":{"enabled":false}} then {"gdpr":{"cmpApi":"iab","rules":[...]}}, with automatic listener 2 replaced by unguarded listener 3. Your comment on this PR notes that the correction is not on this branch; the PR description should say so too until it lands.

Scope: only relay_soft_vendor_exception = true together with managed_user_ids and a CMP. That is the testing setup this flag exists for.

Proposed fix (apply manually: it touches activateManagedUserIdTcfConsent outside this diff and adds tests in another file, so it can't be a one-click suggestion). Route internal relay writes through the automatic owner, and keep its __tcfapi guard installed while Prebid subscribes:

type ManagedTcfConsentActivation = {
  acceptCmpEvents: boolean;
  applyConfig: typeof pbjs.setConfig;
};

// activateManagedUserIdTcfConsent: replace the one-shot guarded setConfig.
const activation: ManagedTcfConsentActivation = {
  acceptCmpEvents: true,
  applyConfig: setConfig,
};
// ...
activation.applyConfig = (value) => {
  const previousTcfApi = tcfWindow.__tcfapi;
  tcfWindow.__tcfapi = guardedTcfApi;
  try {
    return setConfig(value);
  } finally {
    if (tcfWindow.__tcfapi === guardedTcfApi) tcfWindow.__tcfapi = previousTcfApi;
  }
};
activation.applyConfig({
  consentManagement: { ...effectiveConsent, gdpr: { cmpApi: 'iab' } },
} as PbjsConfig);

// installPrebidNpm, before the managed-ID block.
let applyRelayConsentConfig = (value: PbjsConfig) => pbjs.setConfig(value);

// Inside the managed-ID block, after `let automaticTcfConsentActivation`.
// Relay rule updates are internal writes, not a publisher ownership claim.
applyRelayConsentConfig = (value) => {
  const activeConsent = automaticTcfConsentActivation;
  return activeConsent?.acceptCmpEvents
    ? activeConsent.applyConfig(value)
    : originalSetConfig(value);
};

// Here.
(value) => applyRelayConsentConfig(value as PbjsConfig)

Then add the three #1545 relay regression tests to prebid-consent-enforcement.test.mjs. I applied this change to f22a83f in a scratch tree: all 5 relay cases pass, the full vitest run passes (81 files, 3,318 tests, no type errors), Prettier and ESLint are clean, and the shim is 53,628 characters, under the 54,000 guard.

Comment on lines +533 to +534
# Testing opt-in only. Leave false for production consent enforcement.
relay_soft_vendor_exception = 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.

🤔 thinking: Leave the key out of the template

ts config init writes this file verbatim, so every new operator config would carry relay_soft_vendor_exception = false. PrebidIntegrationConfig is deny_unknown_fields, so a binary built before this PR refuses that config on rollback, even though the line only restates the default. Commenting it out matches the optional # excluded_gam_ad_unit_path_suffixes entry below. The example block at docs/guide/integrations/prebid.md:30 and the "leave relay_soft_vendor_exception = false or omit it" sentence at :302 could steer operators toward omitting it as well.

Suggested change
# Testing opt-in only. Leave false for production consent enforcement.
relay_soft_vendor_exception = false
# Testing-only opt-in. Omitted means false; keep it omitted in production so
# older binaries can still load this config on rollback.
# relay_soft_vendor_exception = true

Scratch-verified: the native core config tests and the CLI tests that load this template pass with the suggestion applied.

Comment on lines +412 to +415
// Keep a narrow 54 KB guard including relay consent handling.
// Prebid remains a separate artifact above 200 KB, and importing
// its runtime or private rendering helpers must still fail this regression.
expect(shimCode.length).toBeLessThan(52_500);
expect(shimCode.length).toBeLessThan(54_000);

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.

⛏ nitpick: Keep the measured sizes in the budget comment

The old comment recorded measured sizes so the next bump could see its headroom; this version drops them. With the same Vite options: main is 52,462 characters, f22a83f is 53,465 (+1,003), and the ownership fix from the index.ts comment brings it to 53,628. Recording the final number next to the 54,000 guard keeps that trail. I didn't post a suggestion because the number changes once that fix lands.

@prk-Jr prk-Jr left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Summary

The default-off configuration plumbing and Purpose 2/vendor-mapping controls hold up. I independently reproduced the existing managed-ID consent ownership blocker at f22a83fce; no additional runtime defects found.

No one-click suggestions are included: the consent correction requires changes outside the diff and combined lifecycle tests.

Blocking

  • 🔧 P1 — Relay updates can override a later publisher denial — see inline at crates/trusted-server-js/lib/src/integrations/prebid/index.ts:2487.
  • 🔧 Two CI gates need successful reruns — browser integration tests and trace runtime and browser acceptance failed during browser dependency installation with Ubuntu mirror DNS errors. These are infrastructure failures, but browser acceptance remains unverified. Neither check is required by branch protection; both remain applicable repository gates.

Non-blocking

  • ⛏ Configuration reference and rollback guidance — document relay_soft_vendor_exception in the configuration reference and explain rollback compatibility: older binaries reject even an explicit false. Existing configurations that omit the key remain compatible.

Verification

All seven changed files and affected consumers were reviewed. Under pinned Node 24.20.0, 45 existing focused relay/artifact/consent tests passed, with no type errors. A separate production-artifact JSDOM probe reproduced an identity request and idl_env cookie after publisher denial with the flag enabled; its flag-disabled control passed. Rust suites were assessed through hosted CI, not rerun locally. No live deployment verification was performed.

CI Status

  • trace runtime and browser acceptance: FAIL
  • integration tests: PASS
  • browser integration tests: FAIL
  • integration tests (Fastly EC lifecycle): PASS
  • CodeQL: PASS
  • cargo check/build/test (spin native + wasm32-wasip1): PASS
  • CLAUDE.md symlink guard: PASS
  • Analyze (actions): PASS
  • worker-deadlines (spin): PASS
  • ts dev lint domains (changed lines): PASS
  • cargo test (ts CLI, native) (macos-latest): PASS
  • cargo test (ts CLI, native) (ubuntu-latest): PASS
  • docs-proposal scripts: PASS
  • cargo test: PASS (required)
  • cargo fmt: PASS (required)
  • prepare integration artifacts: PASS
  • cargo check (cloudflare native + wasm32-unknown-unknown): PASS
  • cargo test (cross-adapter parity): PASS
  • Analyze (javascript-typescript): PASS
  • cargo test (axum native): PASS
  • format-docs: PASS (required)
  • vitest: PASS
  • HTML EOF runtime (Fastly): PASS
  • format-typescript: PASS (required)
  • release scripts: PASS
  • CI Node pin guard: PASS
  • worker-deadlines (cloudflare): PASS
  • Analyze (rust): PASS
  • Analyze (javascript-typescript): PASS
  • Analyze (python): PASS

const prepareRelayConsent = createRelayConsentGuard(
ADAPTER_CODE,
(key) => (pbjs as unknown as { getConfig?: PrebidGetConfig }).getConfig?.(key),
(value) => pbjs.setConfig(value as PbjsConfig)

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.

🔧 P1 — Relay updates can override a later publisher denial

With managed User IDs, a CMP, and relay_soft_vendor_exception = true, a nonempty auction makes this internal rules update go through the publisher ownership wrapper. It retires the automatic CMP guard, then Prebid synchronously creates an unguarded CMP subscription. A later CMP grant can overwrite the publisher's static denial.

I independently reproduced this with the built Prebid artifact and shim: after a static Purpose 1 denial and a subsequent CMP grant, the page made one stubbed identity request and wrote idl_env. The same probe with the flag disabled passed. This confirms the existing unresolved finding at this revision; the new static-consent relay matrix does not exercise the combined managed-ID ownership lifecycle.

Route internal updates through the managed consent owner and guard every resulting subscription, with combined lifecycle regression tests. Simply bypassing the publisher wrapper is insufficient if the replacement subscription loses its callback guard. Apply manually: the correction touches the existing activation/ownership code outside this diff and tests in another file, so it cannot be expressed as a one-click suggestion.

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

Superseded by a follow-up review with inline comments: #1498 (review)

@dhruv8sh dhruv8sh left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Summary

Adds an opt-in integrations.prebid.relay_soft_vendor_exception flag (default false) that lets the Prebid shim add trustedServer to the basicAds soft vendor exceptions before relay auctions. The guard is idempotent, keeps Purpose 2 and publisher GVL mappings enforced, and Prebid's per-purpose defaults keep the storage rule in place when the shim adds a basicAds rule. The code looks right; the only blocker is two browser CI jobs that failed on runner infrastructure.

1 of the inline comments below carries a one-click GitHub suggestion — use Commit suggestion to apply it as a commit on the PR branch.

Blocking

🔧 wrench

  • Two browser CI jobs failed — see cross-cutting below

Non-blocking

🤔 thinking / ⛏ nitpick

  • "Closes #1497" overstates the fix — see cross-cutting below
  • Keep the running size breakdown when raising the shim guard — see inline at crates/trusted-server-js/lib/test/prebid-artifact-integration.test.mjs:412

Cross-cutting / body-level findings

  • 🔧 Two browser CI jobs failed — browser integration tests and trace runtime and browser acceptance failed (neither is in the required set). Both logs show Temporary failure resolving 'azure.archive.ubuntu.com' while installing Playwright browser dependencies, so this looks like a runner DNS problem, not this diff. Please re-run them.
  • 🤔 "Closes #1497" overstates the fix — The flag defaults to false and is documented as a testing-only opt-in, so a production deployment that merges this still sees relay auctions blocked under GDPR, which is what #1497 reports. Consider "Refs #1497" (and a title like "Add an opt-in relay soft vendor exception for TCF testing") so the issue stays open until there is a production consent path, such as a publisher-configured relay GVL identity.

CI Status

  • cargo test: PASS (required)
  • cargo fmt: PASS (required)
  • format-docs: PASS (required)
  • format-typescript: PASS (required)
  • vitest: PASS
  • Analyze (rust), Analyze (javascript-typescript), Analyze (python), Analyze (actions), CodeQL: PASS
  • cargo test (axum native), cargo test (cross-adapter parity), cargo test (ts CLI, native) (ubuntu-latest / macos-latest): PASS
  • cargo check (cloudflare native + wasm32-unknown-unknown), cargo check/build/test (spin native + wasm32-wasip1): PASS
  • worker-deadlines (cloudflare / spin), HTML EOF runtime (Fastly): PASS
  • integration tests, integration tests (Fastly EC lifecycle), prepare integration artifacts: PASS
  • ts dev lint domains (changed lines), release scripts, docs-proposal scripts, CI Node pin guard, CLAUDE.md symlink guard: PASS
  • browser integration tests: FAIL
  • trace runtime and browser acceptance: FAIL

Comment on lines +412 to 414
// Keep a narrow 54 KB guard including relay consent handling.
// Prebid remains a separate artifact above 200 KB, and importing
// its runtime or private rendering helpers must still fail this regression.

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.

⛏ nitpick — Keep the running size breakdown when raising the limit. The old comment recorded where the characters went; the new one drops it, so the next bump has no baseline. The current shim builds to 53,465 characters.

Suggested change
// Keep a narrow 54 KB guard including relay consent handling.
// Prebid remains a separate artifact above 200 KB, and importing
// its runtime or private rendering helpers must still fail this regression.
// The original shim measures 40,354 characters. Shared strict trace JSON
// validation, request-ID hook association, and bounded pending lifecycle add
// 11,676 characters (52,030 total); relay consent handling brings it to
// 53,465. Keep a narrow 54 KB guard: Prebid remains a separate artifact above
// 200 KB, and importing its runtime or private rendering helpers must still
// fail this regression.

This branch has not been deployed

No deployments
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.

Auction endpoint not being called from UK IP despite consent given and EC being present - Fine on US VPN

4 participants