Repository navigation
Move consent decoders into their standards' crates - #1383
Open
jwrosewell wants to merge 39 commits into
Open
jwrosewell wants to merge 39 commits into
jwrosewell wants to merge 39 commits into
Conversation
Core decoded the TCF, GPP and US Privacy strings itself, in its consent module, and the permission signal crates named for those standards read what core had decoded. A standard is a vendor's thing as much as a product is, so each decoder moves to the crate for its standard: the TC String decoder, record and purpose mapping to permission-signal/tcf, the GPP string decoder with the iab_gpp dependency to permission-signal/gpp, and the US Privacy string decoder to permission-signal/us-privacy. Each module reads its own string from the request's cookies and headers, once, through a new `read` method on the permission signal trait, and reports a `SignalRecord`: the string as received, whether it decoded, could not be read, has expired or was left as received, where it came from, whether the module says its regulation applies, the sections a sectioned signal carries, and the decoded form in the module's own type, opaque to core. Core keeps the neutral types only. `ConsentContext` is the records the configured modules reported and the request's jurisdiction, `ConsentSource` says where a signal came from, and `SignalStatus` what became of it. The TCF module settles a standalone string against the EU section of a GPP string under the `[consent]` conflict rule, expires a stale record and says the regulation applies from a section hint, asking the GPP crate for the section and the identifiers. The US Privacy module constructs a string from a Global Privacy Control header in a US state under the declared defaults, asking the GPC crate whether the header was sent. Every reader reads the resolved state or asks the crate for the standard. The server-side auction gate reads the resolved permissions, running the auction where basic ad selection is set, where it read the decoded TCF record for one purpose under a jurisdiction rule of its own. The identity graph record writes every vouched signal under its scheme, in the shape it wrote before for the two it stored. Telemetry reads whether any module says its regulation applies. The OpenRTB request builder still in core reads the strings by scheme name through the neutral interface until it leaves core. The mock ad server and the demand crates ask the crates for the standards. `[consent]` keeps its keys. Core reads `mode` and holds the rest as written for the modules to read into types of their own, so a key no linked module reads is kept rather than refused. The policy reads `signals.<scheme>.authoritative` for any scheme and the opt-out sources by the names the modules give them, where it parsed three fixed names. Removed with the pipeline they served: the consent cookie constants, the consent cookie list and cookie forwarder in core, the `GdprConsent` error that nothing raised, the never-filled Additional Consent field and the demand policy flag that sent it, the core consent decoding bench, and the consent forwarding mode enum, which is now the Prebid Server crate's own.
# Conflicts: # CHANGELOG.md # crates/trusted-server-core/src/auction/endpoints.rs # crates/trusted-server-core/src/publisher.rs
The consent decoders left core, so the inspector puts the page's strings in the cookies and header the signal modules read and asks the same modules through read_consent, as the server does.
…hat is untested Review round 1 found that the design said the Cloudflare adapter read core's decoded fields when no adapter's source did, wrote the move of the auction gate to the resolved permissions as a move when it checks TCF Purpose 2 where it checked Purpose 1, and said core reads `mode` when core only parses it. The design now lists each way the gate differs, with its rollout effect and an open question, adds the adapters, the defaults and what is no longer checked, the failure paths, the cost of the deleted benchmark, the public items that go and the paths with no test, and records the review round. The guide sentences that said every module reads `mode` and that core decodes the cookie the InMobi prompt writes are corrected.
Review round 2 found a sixth way the auction gate changes. A request whose only EU signal is carried by GPP, with no TC record the TCF module can read, blocked in every place under the old gate and now runs the auction where the place grants basic ad selection. The design lists it as the sixth difference, with its rollout effect and in the open question, and corrects the failure paragraph for an EU section inside a GPP string, which is reported as absent and refuses nothing. The design also cites the open specs pull request in place of the closed one, says the mtm grant qualifies the expired string item, and lists the stale comments in permission_signal/mod.rs as left for a later change. The CHANGELOG entry now says core parses mode, says the gate changes behavior, and names the consent header constant it removes.
…d an unreadable string is no consent Each permission signal record now carries the permissions its signal consents to, read from the signal alone, so the server-side auction gate keeps the rule it had before the decoders moved. In a GDPR or unknown place, or where a record says its regulation applies, the auction runs only when a record that applies consents to device storage (TCF Purpose 1). A TC String, the EU section of a GPP string, a GPP string or a US Privacy string that cannot be read now refuses every permission its scheme maps instead of counting as consent.
…nd unreadable strings The design states the gate's rule and its three differences from the gate it replaced, moves the fixed items out of Open questions and keeps three questions that need a decision. The changelog and the permission signals and GDPR guides say the same.
This was referenced Oct 17, 2026
The message had lost its line continuations, so an operator saw two runs of eighteen spaces in the middle of it. The text is unchanged.
jwrosewell
marked this pull request as ready for review
October 19, 2026 20:24
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Changes
crates/trusted-server-core/srccrates/permission-signal/tcfcrates/permission-signal/us-privacycrates/permission-signal/gppcrates/trusted-server-adapter-axum/testscrates/auction/prebid-serverdocs/superpowerscrates/permission-signal/gpctools/permissions-inspectordocs/guidecrates/auction/apscrates/ad-server/mockCargo.lockcrates/trusted-server-adapter-cloudflare/srcCHANGELOG.mdcrates/trusted-server-core/Cargo.tomlcrates/trusted-server-core/benchesCloses
Closes #1335
Spec
Spec: consent-decoders
Test plan
CI on this pull request at 6f7b3a1 passed all 29 checks.
cargo test-fastly && cargo test-axumcargo clippy-fastly && cargo clippy-axumcargo fmt --all -- --checkcd crates/trusted-server-js/lib && npx vitest runcd crates/trusted-server-js/lib && npm run formatcd docs && npm run formatcargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1fastly compute serveChecklist
unwrap()in production code (clippy deniesunwrap_usedin CI)logmacros as AGENTS.md asks (notprintln!)On top of the Prebid cookie series (#1382). Issue: #1335.
What this adds
What changes.
The consent decoders live in the crates named for their standards. The TC String decoder goes to
crates/permission-signal/tcf, the GPP decoder and its section reading tocrates/permission-signal/gpp, the US privacy string tocrates/permission-signal/us-privacy, and theSec-GPCheader tocrates/permission-signal/gpc, each with the extraction of its own string from the request. Core'sconsent/extraction.rs,tcf.rs,gpp.rsandus_privacy.rsare gone, with core's dependency on the GPP package.How it works.
PermissionSignalModule::readis the one new method on the interface, and core asks each module once before the permissions are assembled.consent_allows_server_side_auction. The EID gate reads the resolved permission state, the telemetry's applies column reads the record, and the mock ad server, APS and Prebid Server ask the standards' crates for what they need in a standard's own form, as one vendor crate may depend on another.[consent]ismodeplus an open table the modules read, so the TCF module's expiry and conflict resolution and the US privacy module's defaults keep their keys, read by the crate that uses them.signals.<scheme>.authoritativeand the opt out sources are names, so a policy can say the same of any scheme.Why
The permission model made how a permission becomes held the business of modules outside core. Decoding the strings those modules are named for inside core left the model half done, and left core carrying every change the standards make. A standard is a vendor's thing as much as a product is.
What breaks
The auction gate keeps the rule it had, and reads it from the signal records in place of core's decoded TC String. Where the place is GDPR or unknown, or a record says its regulation applies, the auction runs only when a record that applies consents to device storage (TCF Purpose 1). The gate reads neither the resolved permissions nor the policy. It differs from the old gate in three ways. A TC String, or the EU section of a GPP string, that cannot be read is no consent whatever the other string says. With
tcfleft out of[permission-signal] modulesno module reads a TC String, so a GDPR or unknown place never runs the auction and a place outside GDPR runs it whatever the string says. In proxy mode a GPP string that names the EU section with no__gpp_sidhint keeps the auction from running outside GDPR. Three questions that need a decision are listed under "Open questions" in the design page. An unknown key under[consent]is no longer refused at startup, where the typed table refused it, andconsent_storestill is. Crates outside core that read a standard's decoded fields from the context read the crate of that standard instead. The additional consent string, which core never populated, and the never-constructed GDPR consent error are gone. A key under[consent]written with the wrong type is no longer refused when the configuration loads. The module that reads it uses all of its defaults and logs one warning. An age outside 1 to 3650 days is clamped without a log line. The names insignals.us_opt_out.sourcesand thesignals.<scheme>blocks are not checked against the modules.The commits
read_consent, as the server does.What is not in this pull request
The OpenRTB request builder in core still reads two neutral fields of a record, whether it applies and its sections, for the regulations object. The OpenRTB series (#1388) moves the builder into its own crate and keeps reading them through the neutral interface, by scheme name, so the fields stay.