Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
f208663
The Model Terms cookie name is declared by the module that reads it
jwrosewell Oct 9, 2026
a3016c1
Merge branch 'review/16-analytics' into review/17-cookie-constant
Jamesr51d Oct 12, 2026
eebde11
Merge branch 'review/16-analytics' into review/17-cookie-constant
Jamesr51d Oct 12, 2026
f3cf800
Merge 16-analytics into 17-cookie-constant
Jamesr51d Oct 12, 2026
891a43c
Merge 16-analytics into 17-cookie-constant
jwrosewell Oct 13, 2026
9680cdf
Merge split/16-analytics into split/17-cookie-constant
Jamesr51d Oct 13, 2026
2018369
Merge split/16-analytics into split/17-cookie-constant
Jamesr51d Oct 13, 2026
af8af38
Merge 16-analytics into 17-cookie-constant
jwrosewell Oct 13, 2026
f8d0ccf
Core names no Model Terms cookie, and the module reads one of three w…
jwrosewell Oct 16, 2026
3899034
Prebid Server receives the Model Terms preference only where it recei…
jwrosewell Oct 16, 2026
71e039d
A test checks the cookie name against the platform's own Set-Cookie s…
jwrosewell Oct 16, 2026
1ac08af
Merge split/16-analytics into split/17-cookie-constant
jwrosewell Oct 16, 2026
d3c6900
Merge split/16-analytics into split/17-cookie-constant
jwrosewell Oct 16, 2026
3c6f5f0
Merge the review fixes into split/17-cookie-constant
jwrosewell Oct 16, 2026
05ed597
Merge split/16-analytics into split/17-cookie-constant
jwrosewell Oct 16, 2026
f3294e6
Merge split/16-analytics into split/17-cookie-constant
jwrosewell Oct 16, 2026
b8c4016
Merge split/16-analytics into split/17-cookie-constant
jwrosewell Oct 16, 2026
c879e69
Merge split/16-analytics into split/17-cookie-constant
jwrosewell Oct 18, 2026
4196e9c
Merge split/16-analytics into split/17-cookie-constant
jwrosewell Oct 18, 2026
7165dd6
Merge split/16-analytics into split/17-cookie-constant
jwrosewell Oct 18, 2026
6b0a24e
Merge split/16-analytics into split/17-cookie-constant
jwrosewell Oct 18, 2026
3650824
Merge split/16-analytics into split/17-cookie-constant
jwrosewell Oct 19, 2026
35f8654
Merge split/16-analytics into split/17-cookie-constant
jwrosewell Oct 19, 2026
2511572
Merge split/16-analytics into split/17-cookie-constant
jwrosewell Oct 19, 2026
f702084
Merge split/16-analytics into split/17-cookie-constant
jwrosewell Oct 19, 2026
fb49a67
Merge split/16-analytics into split/17-cookie-constant
jwrosewell Oct 28, 2026
c17b60c
Merge split/16-analytics into split/17-cookie-constant
jwrosewell Oct 28, 2026
b54272f
Merge split/16-analytics into split/17-cookie-constant
jwrosewell Oct 28, 2026
0b12c2f
Merge split/16-analytics into split/17-cookie-constant
jwrosewell Oct 28, 2026
7843a38
Merge split/16-analytics into split/17-cookie-constant
jwrosewell Oct 28, 2026
8fba2d3
Merge split/16-analytics into split/17-cookie-constant
jwrosewell Oct 28, 2026
8138c92
Merge split/16-analytics into split/17-cookie-constant
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 @@ -68,6 +68,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Security

- With `consent_forwarding = "openrtb_only"`, the Model Terms for Marketing preference cookie, `__mtm_pref`, is withheld from Prebid Server along with the consent cookies. It was forwarded in every mode.
- A response no longer carries `X-Served-By`, `X-Cache` or `X-Cache-Hits`, unless this server answered a request for a path beneath `/_ts`. The three name the node that answered and say how a cache treated the request, and arrive from the publisher's origin or a cache on the way to it. Every adapter removes them before it finalizes a response, so one set in `[response_headers]` is still sent.
- No adapter writes a version on a response. A deployment that wants the versions on its pages names core's `service_version` middleware in a `[[serve]]` entry, which writes `X-TS-Version` from the git version the build compiled in and `X-TS-Service-Version` from the version the adapter reports.
- A module's table that cannot be read or fails its validation is refused naming the setting at fault and never its value, because at startup the table holds the module's secrets resolved from the secret store and each adapter logs a refusal whole.
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions crates/auction/prebid-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ log = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
trusted-server-core = { workspace = true }
trusted-server-permission-signal-mtm = { workspace = true }
url = { workspace = true }

[dev-dependencies]
Expand Down
85 changes: 81 additions & 4 deletions crates/auction/prebid-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ use trusted_server_core::platform::RuntimeServices;
use trusted_server_core::request_signing::{RequestSigner, SIGNING_VERSION, SigningParams};
#[cfg(test)]
use trusted_server_core::settings::{IntegrationConfig, Settings};
use trusted_server_permission_signal_mtm::COOKIE_MTM_PREF;

/// The name an `implementation` line gives this implementation, its module
/// path, which also serves as its builder's id.
Expand Down Expand Up @@ -1064,9 +1065,9 @@ fn non_empty_override_object(
/// synthesized from `client_ip` — the platform-attested client address —
/// matching the trusted IP already sent in `OpenRTB` `device.ip`.
///
/// In [`ConsentForwardingMode::OpenrtbOnly`] mode, consent cookies are
/// stripped from the `Cookie` header since consent travels exclusively
/// through the `OpenRTB` body.
/// In [`ConsentForwardingMode::OpenrtbOnly`] mode, the consent cookies and
/// the Model Terms for Marketing preference are stripped from the `Cookie`
/// header since consent travels exclusively through the `OpenRTB` body.
#[cfg(test)]
fn copy_request_headers(
from: &http::Request<EdgeBody>,
Expand Down Expand Up @@ -1142,7 +1143,12 @@ fn apply_prebid_header_values(
}
match cookie_value.to_str() {
Ok(value) => {
let stripped = strip_cookies(value, CONSENT_COOKIE_NAMES);
// The Model Terms for Marketing preference records the visitor's
// choice as the consent cookies do, so it is withheld with them.
let stripped = strip_cookies(
&strip_cookies(value, CONSENT_COOKIE_NAMES),
&[COOKIE_MTM_PREF],
);
if !stripped.is_empty()
&& let Ok(cookie_header) = HeaderValue::from_str(&stripped)
{
Expand Down Expand Up @@ -7189,6 +7195,77 @@ set = { networkId = 42 }
}
}

/// A page request from a visitor who has answered a consent prompt and
/// the Model Terms for Marketing preference question.
const ANSWERED: &str = "session=fictional; euconsent-v2=fictional-tcf; __mtm_pref=standard";

/// The `Cookie` header Prebid Server receives in `mode` for a page request
/// carrying `cookie`, through the dispatch path.
fn cookie_sent_to_prebid_server(
mode: ConsentForwardingMode,
cookie: &'static str,
) -> Option<Vec<u8>> {
let stub = Arc::new(StubHttpClient::new());
stub.push_response(204, Vec::new());
let services = build_services_with_http_client_and_client_ip(
Arc::clone(&stub) as Arc<dyn trusted_server_core::platform::PlatformHttpClient>,
std::net::IpAddr::from([203, 0, 113, 9]),
);
let settings = make_settings();
let mut config = base_config();
config.consent_forwarding = mode;
let provider = PrebidAuctionProvider::new(config);
let inbound = http::Request::builder()
.uri("https://pub.example/auction")
.header(header::COOKIE, cookie)
.body(EdgeBody::empty())
.expect("should build inbound request");
let context = AuctionContext {
settings: &settings,
request: &inbound,
timeout_ms: 321,
transport_timeout_ms: 321,
provider_responses: None,
services: &services,
};

futures::executor::block_on(
provider.request_bids(&create_test_auction_request(), &context),
)
.expect("should dispatch PBS request");

let headers = stub.recorded_request_header_bytes();
assert_eq!(headers.len(), 1, "should dispatch one request");
header_value(&headers[0], "cookie").map(<[u8]>::to_vec)
}

#[test]
fn openrtb_only_withholds_the_model_terms_preference_from_prebid_server() {
assert_eq!(
cookie_sent_to_prebid_server(ConsentForwardingMode::OpenrtbOnly, ANSWERED).as_deref(),
Some(b"session=fictional".as_slice()),
"should withhold the preference wherever the consent cookies are withheld"
);
}

#[test]
fn cookies_only_forwards_the_model_terms_preference_to_prebid_server() {
assert_eq!(
cookie_sent_to_prebid_server(ConsentForwardingMode::CookiesOnly, ANSWERED).as_deref(),
Some(ANSWERED.as_bytes()),
"should forward the preference with the consent cookies"
);
}

#[test]
fn both_forwards_the_model_terms_preference_to_prebid_server() {
assert_eq!(
cookie_sent_to_prebid_server(ConsentForwardingMode::Both, ANSWERED).as_deref(),
Some(ANSWERED.as_bytes()),
"should forward the preference with the consent cookies"
);
}

#[test]
fn pbs_request_serialization_goldens_cover_disabled_and_deterministic_enabled_signing() {
let provider = PrebidAuctionProvider::new(base_config());
Expand Down
25 changes: 23 additions & 2 deletions crates/permission-signal/mtm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@

use std::sync::OnceLock;

use trusted_server_core::constants::COOKIE_MTM_PREF;
use trusted_server_core::evidence::RequestInfo;
use trusted_server_core::module_context::ModuleCall;
use trusted_server_core::permission_signal::{PermissionSignalModule, SignalInput};
Expand Down Expand Up @@ -67,6 +66,13 @@ fn short() -> &'static str {
/// The scheme, as a [`ValidSignal`] names it.
pub const SCHEME: &str = "mtm";

/// The cookie holding the preference, one of the three words `standard`,
/// `personalized` and `non-marketing`. It is written as a first party cookie
/// by whatever preference platform asked the visitor, and is not a Trusted
/// Server name, so that anyone can build a platform that sets it. This module
/// reads the cookie and never writes it.
pub const COOKIE_MTM_PREF: &str = "__mtm_pref";

/// The terms the data is available under when a word is present. Versioned,
/// and never a page that can be edited, because whoever receives the data
/// has to be able to prove what terms were in force when it was sent.
Expand Down Expand Up @@ -231,7 +237,7 @@ impl PermissionSignalModule for MtmModule {
}

/// The word, when one is present. There is nothing else to validate,
/// because a value that is not one of the two words is not an answer.
/// because a value that is not one of the three words is not an answer.
fn valid_signal(&self, call: ModuleCall<'_>) -> Option<ValidSignal> {
call.inject(self, Self::read_signal).ok().flatten()
}
Expand Down Expand Up @@ -330,6 +336,21 @@ mod tests {
}
}

/// Every string the platform writes names the cookie this module reads, so
/// a change to either name fails here rather than leaving every visitor's
/// answer unread.
#[test]
fn the_platforms_own_set_cookie_strings_name_the_cookie_the_module_reads() {
for (_, set_cookie) in PMP_SET_COOKIE {
let pair = set_cookie.split(';').next().unwrap_or_default().trim();
let (name, _) = pair.split_once('=').expect("a cookie is name=value");
assert_eq!(
name, COOKIE_MTM_PREF,
"the module must read the name the platform writes: {set_cookie}",
);
}
}

/// The cookie has to stay readable by the script that owns the question.
///
/// A browser lets a script neither read nor replace a cookie carrying
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -612,7 +612,7 @@ fn a_visitor_in_the_eu_with_no_record_is_awaiting_what_tcf_could_grant() {

#[test]
fn a_pmp_answer_in_the_eu_settles_what_the_model_terms_cover_and_declares_them() {
use trusted_server_core::constants::COOKIE_MTM_PREF;
use trusted_server_permission_signal_mtm::COOKIE_MTM_PREF;

let geo = GeoInfo {
city: String::new(),
Expand Down
7 changes: 0 additions & 7 deletions crates/trusted-server-core/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,6 @@ pub const COOKIE_TS_EC_PULL_COMPLETE: &str = "ts-ec-pull-complete";
/// JSON array of Extended User IDs (`[{ source, uids }]`) from identity providers.
pub const COOKIE_TS_EIDS: &str = "ts-eids";
pub const COOKIE_TS_TESTER: &str = "ts-tester";
/// The Model Terms for Marketing preference, one of the three words
/// `standard`, `personalized` and `non-marketing`, written as a first party
/// cookie by whatever preference platform asked the visitor and read by the
/// Model Terms for Marketing signal module. Not a Trusted Server name, so
/// that anyone can build a platform that sets it. Trusted Server reads this
/// cookie and never writes it.
pub const COOKIE_MTM_PREF: &str = "__mtm_pref";
pub const COOKIE_SHAREDID: &str = "sharedId";

pub const HEADER_X_PUB_USER_ID: HeaderName = HeaderName::from_static("x-pub-user-id");
Expand Down
4 changes: 2 additions & 2 deletions crates/trusted-server-core/src/permission_signal/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ reads its own signal from the request, without a change to core. The fifth,
Model Terms for Marketing (MTM), is the first terms scheme, where a publisher
and the parties it passes data to agree to be bound by a published set of
terms, and what the module reads is the visitor's answer to a preference
platform, one of three words in the first party cookie
`__mtm_pref`, a name any platform may set. It is one of many terms schemes
platform, one of three words in a first party cookie whose name the module
declares and any platform may set. It is one of many terms schemes
rather than the only one, because a publisher, a trade body or a regulator can
each publish terms and each set becomes a module. The five here are a
starting set and not the list.
Expand Down
14 changes: 7 additions & 7 deletions docs/guide/permission-signals.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,13 +114,13 @@ question about a jurisdiction and a publisher.

## The modules that ship

| Name | Crate | Reads |
| ------------ | ------------------------------------- | --------------------------------------------------------------------- |
| `gpc` | `crates/permission-signal/gpc` | The `Sec-GPC` header, Global Privacy Control |
| `gpp` | `crates/permission-signal/gpp` | The US sale opt-out carried in a GPP string |
| `us-privacy` | `crates/permission-signal/us-privacy` | The sale opt-out in a US Privacy string |
| `tcf` | `crates/permission-signal/tcf` | A TCF v2 record, with the purpose mapping in code |
| `mtm` | `crates/permission-signal/mtm` | The PMP answer, one of two words, under the Model Terms for Marketing |
| Name | Crate | Reads |
| ------------ | ------------------------------------- | ----------------------------------------------------------------------- |
| `gpc` | `crates/permission-signal/gpc` | The `Sec-GPC` header, Global Privacy Control |
| `gpp` | `crates/permission-signal/gpp` | The US sale opt-out carried in a GPP string |
| `us-privacy` | `crates/permission-signal/us-privacy` | The sale opt-out in a US Privacy string |
| `tcf` | `crates/permission-signal/tcf` | A TCF v2 record, with the purpose mapping in code |
| `mtm` | `crates/permission-signal/mtm` | The PMP answer, one of three words, under the Model Terms for Marketing |

The three opt-outs are separate so that a publisher who does not act on Global
Privacy Control can remove it and keep the other two. What an opt-out takes
Expand Down
Loading