Skip to content

Move consent decoders into their standards' crates - #1383

Open
jwrosewell wants to merge 39 commits into
feature/41-prebid-eidsfrom
feature/42-consent-decoders
Open

jwrosewell wants to merge 39 commits into
feature/41-prebid-eidsfrom
feature/42-consent-decoders

Conversation

@jwrosewell

@jwrosewell jwrosewell commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Changes

Area Change
crates/trusted-server-core/src 31 files, 1334 lines added, 3252 removed
crates/permission-signal/tcf 3 files, 1040 lines added, 296 removed
crates/permission-signal/us-privacy 3 files, 672 lines added, 58 removed
crates/permission-signal/gpp 3 files, 618 lines added, 53 removed
crates/trusted-server-adapter-axum/tests 1 file, 176 lines added, 123 removed
crates/auction/prebid-server 2 files, 175 lines added, 127 removed
docs/superpowers 1 file, 109 lines added, 0 removed
crates/permission-signal/gpc 2 files, 91 lines added, 18 removed
tools/permissions-inspector 3 files, 81 lines added, 21 removed
docs/guide 5 files, 79 lines added, 61 removed
crates/auction/aps 2 files, 38 lines added, 32 removed
crates/ad-server/mock 2 files, 36 lines added, 15 removed
Cargo.lock 1 file, 33 lines added, 1 removed
crates/trusted-server-adapter-cloudflare/src 1 file, 3 lines added, 2 removed
CHANGELOG.md 1 file, 1 line added, 0 removed
crates/trusted-server-core/Cargo.toml 1 file, 0 lines added, 5 removed
crates/trusted-server-core/benches 1 file, 0 lines added, 236 removed

Closes

Closes #1335

Spec

Spec: consent-decoders

Test plan

CI on this pull request at 6f7b3a1 passed all 29 checks.

  • cargo test-fastly && cargo test-axum
  • cargo clippy-fastly && cargo clippy-axum
  • cargo fmt --all -- --check
  • JS tests: cd crates/trusted-server-js/lib && npx vitest run
  • 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
  • Manual testing via fastly compute serve
  • Other: the full workflow set of this repository, run on this pull request at 6f7b3a1

Checklist

  • Changes follow AGENTS.md conventions
  • No unwrap() in production code (clippy denies unwrap_used in CI)
  • Uses log macros as AGENTS.md asks (not println!)
  • New code has tests
  • No secrets or credentials committed

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 to crates/permission-signal/gpp, the US privacy string to crates/permission-signal/us-privacy, and the Sec-GPC header to crates/permission-signal/gpc, each with the extraction of its own string from the request. Core's consent/extraction.rs, tcf.rs, gpp.rs and us_privacy.rs are gone, with core's dependency on the GPP package.

How it works.

  • Core keeps the neutral record. The request's consent context is a list of signal records, each with its scheme, the string as received, whether it was decoded, where it came from, whether it applies, its sections and an opaque decoded form. PermissionSignalModule::read is the one new method on the interface, and core asks each module once before the permissions are assembled.
  • Readers read the records, the resolved state or the standard's crate. The auction endpoints and the publisher gate on the signal records, through 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.
  • Each module reads its own settings. [consent] is mode plus 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.
  • The policy names schemes, not one standard. signals.<scheme>.authoritative and the opt out sources are names, so a policy can say the same of any scheme.
  • The stored record keys by scheme. The identity graph's consent record holds every vouched record under its scheme name, reads earlier records as written, and may now carry schemes it did not store before.

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 tcf left out of [permission-signal] modules no 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_sid hint 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, and consent_store still 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 in signals.us_opt_out.sources and the signals.<scheme> blocks are not checked against the modules.

The commits

  1. The consent decoders live in the crates named for their standards.
  2. The permissions 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.
  3. The consent decoders design states how the auction gate changes and what is untested, and takes its round 2 corrections.
  4. The auction gate reads consent to storage from the signal records, and an unreadable string is no consent. Each record carries the permissions its signal consents to, the gate keeps the rule it had before the decoders moved, and a TC String, the EU section of a GPP string, a GPP string or a US Privacy string that cannot be read refuses every permission its scheme maps.
  5. The consent decoders design, guides and changelog describe the gate and unreadable strings.
  6. Merges of the series below, which carry the fixes made lower in the stack, the page origin change and Tech Lab main at 6ef07ae.

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.

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.
@jwrosewell
jwrosewell added this pull request to stack #1401 October 15, 2026 17:56
@jwrosewell
jwrosewell removed this pull request from stack #1401 October 15, 2026 20:51
…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.
@jwrosewell
jwrosewell removed this pull request from stack #1408 October 18, 2026 20:15
@jwrosewell jwrosewell closed this Oct 18, 2026
@jwrosewell jwrosewell reopened this Oct 18, 2026
@jwrosewell
jwrosewell added this pull request to stack #1418 October 19, 2026 12:18
@jwrosewell
jwrosewell removed this pull request from stack #1418 October 19, 2026 16:30
@jwrosewell jwrosewell closed this Oct 19, 2026
@jwrosewell jwrosewell reopened this Oct 19, 2026
@jwrosewell
jwrosewell added this pull request to stack #1421 October 19, 2026 17:38
@jwrosewell jwrosewell changed the title The consent decoders live in the crates named for their standards Move consent decoders into their standards' crates Oct 19, 2026
@jwrosewell
jwrosewell removed this pull request from stack #1421 October 19, 2026 18:29
@jwrosewell
jwrosewell added this pull request to stack #1422 October 19, 2026 19:20
@jwrosewell
jwrosewell marked this pull request as ready for review October 19, 2026 20:24

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.

Core decodes the TCF, GPP and US privacy strings itself

1 participant