Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
71bf129
The consent decoders live in the crates named for their standards
jwrosewell Oct 14, 2026
0fa3d36
Merge 41-prebid-eids into 42-consent-decoders
jwrosewell Oct 14, 2026
947e5ea
The permissions inspector reads the entered signals through the modules
jwrosewell Oct 15, 2026
a846121
Merge 41-prebid-eids into 42-consent-decoders
jwrosewell Oct 15, 2026
564788e
Merge 41-prebid-eids into 42-consent-decoders
jwrosewell Oct 15, 2026
f8ffb21
The consent decoders design states how the auction gate changes and w…
jwrosewell Oct 15, 2026
fac5600
The consent decoders design takes its round 2 corrections
jwrosewell Oct 15, 2026
1bab1b6
Merge 41-prebid-eids into 42-consent-decoders
jwrosewell Oct 15, 2026
d4b0c4b
Merge 41-prebid-eids into 42-consent-decoders
jwrosewell Oct 15, 2026
b459b7c
Merge 41-prebid-eids into 42-consent-decoders
jwrosewell Oct 15, 2026
6c78c2c
The auction gate reads consent to storage from the signal records, an…
jwrosewell Oct 16, 2026
52e1247
The consent decoders design, guides and changelog describe the gate a…
jwrosewell Oct 16, 2026
58cc11f
Merge split/41-prebid-eids into split/42-consent-decoders
jwrosewell Oct 16, 2026
9fea555
Merge split/41-prebid-eids into split/42-consent-decoders
jwrosewell Oct 16, 2026
2348e87
Merge split/41-prebid-eids into split/42-consent-decoders
jwrosewell Oct 16, 2026
33193f9
Merge split/41-prebid-eids into split/42-consent-decoders
jwrosewell Oct 16, 2026
5f961a0
Merge split/41-prebid-eids into split/42-consent-decoders
jwrosewell Oct 16, 2026
8a94661
Merge split/41-prebid-eids into split/42-consent-decoders
jwrosewell Oct 16, 2026
fa2c78f
Merge split/41-prebid-eids into split/42-consent-decoders
jwrosewell Oct 16, 2026
3d0b528
Merge split/41-prebid-eids into split/42-consent-decoders
jwrosewell Oct 16, 2026
cbf7749
Merge split/41-prebid-eids into split/42-consent-decoders
jwrosewell Oct 16, 2026
621f870
The design header names the pull request that carries it
jwrosewell Oct 16, 2026
aa6ef6c
Merge split/41-prebid-eids into split/42-consent-decoders
jwrosewell Oct 16, 2026
242527e
Merge split/41-prebid-eids into split/42-consent-decoders
jwrosewell Oct 16, 2026
ab4af34
Merge split/41-prebid-eids into split/42-consent-decoders
jwrosewell Oct 16, 2026
b780334
Merge split/41-prebid-eids into split/42-consent-decoders
jwrosewell Oct 16, 2026
0205f54
Merge split/41-prebid-eids into split/42-consent-decoders
jwrosewell Oct 16, 2026
43ba51b
Merge split/41-prebid-eids into split/42-consent-decoders
jwrosewell Oct 16, 2026
daa6a3b
Merge split/41-prebid-eids into split/42-consent-decoders
jwrosewell Oct 18, 2026
8d2c1b4
Merge split/41-prebid-eids into split/42-consent-decoders
jwrosewell Oct 18, 2026
ee35c5d
The error for a removed consent setting reads as one sentence
jwrosewell Oct 18, 2026
844f77d
Merge split/41-prebid-eids into split/42-consent-decoders
jwrosewell Oct 18, 2026
2d80eb2
Merge split/41-prebid-eids into split/42-consent-decoders
jwrosewell Oct 18, 2026
686bddb
Merge split/41-prebid-eids into split/42-consent-decoders
jwrosewell Oct 18, 2026
d793010
Merge split/41-prebid-eids into split/42-consent-decoders
jwrosewell Oct 18, 2026
01339ef
Merge split/41-prebid-eids into split/42-consent-decoders
jwrosewell Oct 19, 2026
70d30e8
Merge split/41-prebid-eids into split/42-consent-decoders
jwrosewell Oct 19, 2026
4e9ae52
Merge split/41-prebid-eids into split/42-consent-decoders
jwrosewell Oct 19, 2026
6f7b3a1
Merge split/41-prebid-eids into split/42-consent-decoders
jwrosewell Oct 19, 2026
5a77b4a
The permissions inspector README names read_consent
jwrosewell Oct 28, 2026
7d8e43f
Merge split/41-prebid-eids into split/42-consent-decoders
jwrosewell Oct 28, 2026
c6fb786
Merge split/41-prebid-eids into split/42-consent-decoders
jwrosewell Oct 28, 2026
e19ac93
Merge split/41-prebid-eids into split/42-consent-decoders
jwrosewell Oct 28, 2026
e6471b5
Merge split/41-prebid-eids into split/42-consent-decoders
jwrosewell Oct 28, 2026
ad7a60c
Merge split/41-prebid-eids into split/42-consent-decoders
jwrosewell Oct 28, 2026
53b4828
Merge split/41-prebid-eids into split/42-consent-decoders
jwrosewell Oct 28, 2026
f7bc109
Merge split/41-prebid-eids into split/42-consent-decoders
jwrosewell Oct 28, 2026
f4e5d2c
Merge split/41-prebid-eids into split/42-consent-decoders
jwrosewell Oct 28, 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- **Breaking for crates outside core:** the consent decoders live in the crates named for their standards. The TCF string decoder is in `trusted-server-permission-signal-tcf`, the GPP string decoder in `trusted-server-permission-signal-gpp`, and the US Privacy string decoder in `trusted-server-permission-signal-us-privacy`, and each module reads its own string from the request through `PermissionSignalModule::read`, once, and reports a `SignalRecord`. `ConsentContext` is the records the configured modules reported and the request's jurisdiction, read by scheme name, with nothing of any standard in it, and a reader that needs a string in its standard's own form asks that standard's crate (`tc_string`, `applies`, `gpp_string`, `section_ids`, `us_privacy_string`, `signaled`). A record also carries the permissions its signal consents to, and the server-side auction gate keeps its rule by reading them, so in a GDPR or unknown jurisdiction, or where a record says its regulation applies, the auction needs consent to device storage (TCF Purpose 1) from a record that applies. Without `tcf` in the modules no TC String is read, and one that cannot be read, standalone or in a GPP string, is no consent to any purpose TCF maps. `[consent]` keeps its keys, with `mode` parsed by core and the rest read by the modules, and `signals.<scheme>.authoritative` in `permissions.yaml` is read for any scheme. The identity graph record writes every vouched signal under its scheme, in the shape it wrote before for the two it stored. `CONSENT_COOKIE_NAMES`, `forward_cookie_header`, the consent cookie and header constants, `ConsentForwardingMode` (now the Prebid Server crate's), the `GdprConsent` error and the never-filled Additional Consent field and `additional_consent` policy flag are removed.
- **Breaking for crates outside core:** the identifiers a request carries come from identifier modules. Core declares `IdentifierModule` in `trusted_server_core::ec::identifiers`, a registration offers one with `IntegrationRegistrationBuilder::with_identifier_module`, and core asks every running module once per request, when it reads the Edge Cookie state, then syncs the answers to the identity graph and carries them to the auction as it did the `ts-eids` cookie. The Prebid module owns that cookie, its two shapes and its size limit, and is the first identifier module. `COOKIE_EIDS` is removed from core, `ec_finalize_response` no longer takes the cookie's value, `EcContext::module_eids` holds what the modules learned, and `RuntimeServices` carries the modules through `with_identifier_modules`. A `template_cache_key_cookies` entry naming any cookie under the public prefix is refused, where the Edge Cookie and the EIDs cookie were refused by name. A deployment's settings do not change.
- **Breaking:** The React Server Components Flight rewriter is the middleware `framework.nextjs.flight` of the Next.js module, which runs on a `text/x-component` response where a `[[fetch]]` entry names it, where core rewrote every Flight response by media type. A deployment that fronts a Next.js site selects `[framework] module = "nextjs"` and adds the entry, as `trusted-server.example.toml` shows it. With the module selected and no entry, a Flight response passes through as the origin sent it, and an entry may now cover `text/x-component`. A middleware says with `Middleware::moves_origin_address` that it moves the origin's address itself, and core then leaves the addresses in a body of the media types it handles to it, whether or not an entry names it, because a plain replacement in a length prefixed row leaves the old length. **For crates outside core**, `trusted_server_core::rsc_flight` is removed.
- **Breaking:** Where content comes from is a source module. `[source]` selects the publisher's, and each `[[proxy.asset_routes]]` entry selects its own in a `source` table of the same shape, with `module` naming the source and its settings in the table beneath. Core supplies `origin`, an HTTP origin, and a module a build links can offer another through `IntegrationRegistrationBuilder::with_source_module`. A source answers for each request where that request's content comes from, and core fetches, caches, rewrites the origin host and follows the redirect rule from that answer. `[source]` is required. The origin settings move into the `origin` table, and a document that still writes one where it was is refused at startup naming the new place. An environment variable that set the address is now `TRUSTED_SERVER__SOURCE__ORIGIN__URL`. The old name is refused where the build reads variables, and `ts config push` ignores it, as it ignores any variable for a key the document does not hold. A path in an asset route's `url` names a folder the target path is added under, where a path was refused, a target path with a `.` or `..` segment under a folder is answered `400`, and a route applies `directory_index` as the publisher does. Stored pages are fetched again after the upgrade, because their keys hold a code worked out from the settings. Where `host_header` is set, the page pipeline and every middleware are now told that `Host` as the page's origin host, where they were told the address's host, so the page's own addresses under that name are rewritten to the request's host. **For crates outside core**, `handle_asset_proxy_request` takes the route's source, which `IntegrationRegistry::route_source` gives, and `Publisher::origin_host`, `origin_host_header` and `origin_query_is_stripped` are replaced by `Settings::origin_source` and the `SourceLocation` a source answers.
Expand Down
34 changes: 33 additions & 1 deletion Cargo.lock

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

3 changes: 3 additions & 0 deletions crates/ad-server/mock/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ log = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
trusted-server-core = { workspace = true }
trusted-server-permission-signal-gpp = { workspace = true }
trusted-server-permission-signal-tcf = { workspace = true }
trusted-server-permission-signal-us-privacy = { workspace = true }
url = { workspace = true }
validator = { workspace = true }

Expand Down
48 changes: 33 additions & 15 deletions crates/ad-server/mock/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,14 +270,14 @@ impl AdServerMockProvider {
json!(null)
};

// Build consent summary from ConsentContext
// The consent summary, each string asked of the crate for its scheme.
let consent_json = request.user.consent.as_ref().map(|ctx| {
json!({
"gdpr": if ctx.gdpr_applies { 1 } else { 0 },
"consent": ctx.raw_tc_string,
"us_privacy": ctx.raw_us_privacy,
"gpp": ctx.raw_gpp_string,
"gpp_sid": ctx.gpp_section_ids,
"gdpr": u8::from(trusted_server_permission_signal_tcf::applies(ctx)),
"consent": trusted_server_permission_signal_tcf::tc_string(ctx),
"us_privacy": trusted_server_permission_signal_us_privacy::us_privacy_string(ctx),
"gpp": trusted_server_permission_signal_gpp::gpp_string(ctx),
"gpp_sid": trusted_server_permission_signal_gpp::section_ids(ctx),
})
});

Expand Down Expand Up @@ -1269,7 +1269,7 @@ mod tests {

#[test]
fn test_adserver_request_includes_consent() {
use trusted_server_core::consent::ConsentContext;
use trusted_server_core::consent::{ConsentContext, SignalRecord, SignalStatus};

let config = AdServerMockSettings {
endpoint: "http://localhost:6767/mediate".to_string(),
Expand All @@ -1281,14 +1281,32 @@ mod tests {
let provider = AdServerMockProvider::new("adserver_mock", config);

let mut request = create_test_auction_request();
request.user.consent = Some(ConsentContext {
raw_tc_string: Some("BOEFEAyO".to_string()),
gdpr_applies: true,
raw_us_privacy: Some("1YNN".to_string()),
raw_gpp_string: Some("DBACNYA~CPXxRfAPXxRfA".to_string()),
gpp_section_ids: Some(vec![2, 6]),
..Default::default()
});
request.user.consent = Some(
ConsentContext::default()
.with_record(
SignalRecord::new(
trusted_server_permission_signal_tcf::SCHEME,
SignalStatus::Decoded,
)
.with_value("BOEFEAyO")
.with_applies(true),
)
.with_record(
SignalRecord::new(
trusted_server_permission_signal_us_privacy::SCHEME,
SignalStatus::Decoded,
)
.with_value("1YNN"),
)
.with_record(
SignalRecord::new(
trusted_server_permission_signal_gpp::SCHEME,
SignalStatus::Decoded,
)
.with_value("DBACNYA~CPXxRfAPXxRfA")
.with_sections(vec![2, 6]),
),
);

let adserver_req = provider
.build_adserver_request(&request, &[])
Expand Down
4 changes: 4 additions & 0 deletions crates/auction/aps/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,17 @@ log = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
trusted-server-core = { workspace = true }
trusted-server-permission-signal-gpp = { workspace = true }
trusted-server-permission-signal-tcf = { workspace = true }
trusted-server-permission-signal-us-privacy = { workspace = true }
url = { workspace = true }
validator = { workspace = true }

[dev-dependencies]
futures = { workspace = true }
tokio = { workspace = true }
trusted-server-core = { workspace = true, features = ["test-utils"] }
trusted-server-permission-signal-gpc = { workspace = true }

# The visible owner of this vendor crate, the way Prebid.js requires a named
# maintainer of every adapter. Amazon has not yet adopted this crate, so the
Expand Down
66 changes: 34 additions & 32 deletions crates/auction/aps/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1205,15 +1205,18 @@ impl ApsAuctionProvider {

fn build_regs(consent: Option<&trusted_server_core::consent::ConsentContext>) -> Option<Regs> {
let consent = consent?;
let applies = trusted_server_permission_signal_tcf::applies(consent);
let ext = RegsExt {
gdpr: Some(u8::from(consent.gdpr_applies)),
us_privacy: consent.raw_us_privacy.clone(),
gpp: consent.raw_gpp_string.clone(),
gpp_sid: consent.gpp_section_ids.clone(),
gdpr: Some(u8::from(applies)),
us_privacy: trusted_server_permission_signal_us_privacy::us_privacy_string(consent)
.map(str::to_owned),
gpp: trusted_server_permission_signal_gpp::gpp_string(consent).map(str::to_owned),
gpp_sid: trusted_server_permission_signal_gpp::section_ids(consent)
.map(<[u16]>::to_vec),
};
Some(Regs {
coppa: None,
gdpr: Some(consent.gdpr_applies),
gdpr: Some(applies),
us_privacy: ext.us_privacy.clone(),
gpp: ext.gpp.clone(),
gpp_sid: ext
Expand Down Expand Up @@ -1328,7 +1331,9 @@ impl ApsAuctionProvider {
.collect();

let consent = request.user.consent.as_ref();
let raw_tc = consent.and_then(|value| value.raw_tc_string.clone());
let raw_tc = consent
.and_then(trusted_server_permission_signal_tcf::tc_string)
.map(str::to_owned);
let user = Some(User {
id: request.user.id.clone(),
consent: raw_tc.clone(),
Expand Down Expand Up @@ -3852,45 +3857,42 @@ mod engine_tests {

#[test]
fn consent_fields_keep_an_empty_admitted_context() {
use trusted_server_core::consent::ConsentContext;
use trusted_server_core::consent::jurisdiction::Jurisdiction;

use trusted_server_core::consent::{ConsentContext, SignalRecord, SignalStatus};
use trusted_server_permission_signal_gpc as gpc;
use trusted_server_permission_signal_gpp as gpp;
use trusted_server_permission_signal_tcf as tcf;
use trusted_server_permission_signal_us_privacy as us_privacy;

let applying = SignalRecord::new(tcf::SCHEME, SignalStatus::Decoded)
.with_value("tc-string")
.with_applies(true);
let cases = [
("empty", ConsentContext::default()),
(
"gdpr",
ConsentContext {
gdpr_applies: true,
raw_tc_string: Some("tc-string".to_string()),
jurisdiction: Jurisdiction::Gdpr,
..Default::default()
},
ConsentContext::new(Jurisdiction::Gdpr).with_record(applying),
),
(
"unknown-gpc",
ConsentContext {
gpc: true,
jurisdiction: Jurisdiction::Unknown,
..Default::default()
},
ConsentContext::new(Jurisdiction::Unknown).with_record(gpc::record()),
),
(
"nonregulated-gpc",
ConsentContext {
gpc: true,
jurisdiction: Jurisdiction::NonRegulated,
..Default::default()
},
ConsentContext::new(Jurisdiction::NonRegulated).with_record(gpc::record()),
),
(
"usp-gpp",
ConsentContext {
raw_us_privacy: Some("1YNN".to_string()),
raw_gpp_string: Some("gpp-string".to_string()),
gpp_section_ids: Some(vec![7, 8]),
jurisdiction: Jurisdiction::NonRegulated,
..Default::default()
},
ConsentContext::new(Jurisdiction::NonRegulated)
.with_record(
SignalRecord::new(us_privacy::SCHEME, SignalStatus::Decoded)
.with_value("1YNN"),
)
.with_record(
SignalRecord::new(gpp::SCHEME, SignalStatus::Decoded)
.with_value("gpp-string")
.with_sections(vec![7, 8]),
),
),
];
for (name, consent) in cases {
Expand All @@ -3907,7 +3909,7 @@ mod engine_tests {
.expect("APS should preserve empty admitted context");
assert_eq!(
regs["gdpr"],
serde_json::json!(u8::from(consent.gdpr_applies)),
serde_json::json!(u8::from(tcf::applies(&consent))),
"{name}"
);
assert!(
Expand Down
4 changes: 4 additions & 0 deletions crates/auction/prebid-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ log = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
trusted-server-core = { workspace = true }
trusted-server-permission-signal-gpc = { workspace = true }
trusted-server-permission-signal-gpp = { workspace = true }
trusted-server-permission-signal-mtm = { workspace = true }
trusted-server-permission-signal-tcf = { workspace = true }
trusted-server-permission-signal-us-privacy = { workspace = true }
url = { workspace = true }

[dev-dependencies]
Expand Down
Loading
Loading