Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
82942de
Extract existing Sourcepoint consent synchronization
ChristianPavilonis Nov 11, 2026
a275941
Carry the lower baseline check corrections into Sourcepoint
ChristianPavilonis Nov 11, 2026
e09e884
Carry the documentation site correction into Sourcepoint
ChristianPavilonis Nov 11, 2026
ce378aa
Carry the startup probe correction through the compatible baseline
ChristianPavilonis Nov 11, 2026
e267296
Carry the browser CI host compatibility fix through the baseline
ChristianPavilonis Nov 11, 2026
7e9ca97
Carry the tagged EdgeZero snapshot through the stack
ChristianPavilonis Nov 11, 2026
2d27271
Carry the monthly EdgeZero tag through the stack
ChristianPavilonis Nov 11, 2026
0ebe0eb
Keep real Sourcepoint consent reconciliation on direct ingress
ChristianPavilonis Nov 12, 2026
7140746
Retain the owning partial-Geo parity fixture correction
ChristianPavilonis Nov 12, 2026
314aebd
Retain the concurrent cutover documentation closeout
ChristianPavilonis Nov 12, 2026
8414bff
Retain prepared native development documentation
ChristianPavilonis Nov 12, 2026
7c397a6
Retain current main validation and mediator fallback
ChristianPavilonis Nov 12, 2026
af6e51c
Retain the Cargo target-aware cache harness
ChristianPavilonis Nov 12, 2026
397d4ce
Retain target-aware GPT bundle lookup for the cache fixture
ChristianPavilonis Nov 12, 2026
e85f66a
Carry hosted qualification fixes through PR 1535
ChristianPavilonis Nov 12, 2026
01a052d
Carry hosted qualification fixes through PR 1535
ChristianPavilonis Nov 12, 2026
cfaac73
Carry hosted qualification fixes through PR 1535
ChristianPavilonis Nov 12, 2026
5f884f9
Carry hosted qualification fixes through PR 1535
ChristianPavilonis Nov 12, 2026
0666a9b
Carry hosted qualification fixes through PR 1535
ChristianPavilonis Nov 12, 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
33 changes: 32 additions & 1 deletion crates/trusted-server-adapter-axum/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,14 @@ where
{
let services = state.services_for_request(&ctx)?;
let mut req = ctx.into_request()?;
if req.uri().path() == trusted_server_core::ec::consent_update::CONSENT_UPDATE_PATH {
req = match trusted_server_core::ec::consent_update::prepare_consent_update(&services, req)
.await
{
Ok(request) => request,
Err(response) => return Ok(*response),
};
}
if let Err(error) = trusted_server_core::integrations::gpt_diagnostics::prepare_request(
&state.settings,
&mut req,
Expand Down Expand Up @@ -292,6 +300,7 @@ enum EcRoute {
Publisher,
Auction,
PageBids,
ConsentUpdate,
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -355,6 +364,9 @@ async fn dispatch_ec_route(
registry: Some(&state.partners),
};
let result = match route {
EcRoute::ConsentUpdate => Ok(
trusted_server_core::ec::consent_update::handle_consent_update(&req, ec.graph.as_ref()),
),
Comment on lines +367 to +369

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.

♻️ refactor — ConsentUpdate passes the unfiltered ec.graph, but NativeEcRequest::finalize (line 280-283) only writes through the graph when self.browser || ec_consent_withdrawn(..). A non-browser grant therefore gets 204 on Axum while Fastly, which passes finalize_kv_graph (same filter), returns 503. Apply the finalize filter so the acknowledgement matches what finalization can actually persist and the adapters agree.

Suggested change
EcRoute::ConsentUpdate => Ok(
trusted_server_core::ec::consent_update::handle_consent_update(&req, ec.graph.as_ref()),
),
EcRoute::ConsentUpdate => Ok(
trusted_server_core::ec::consent_update::handle_consent_update(
&req,
ec.graph
.as_ref()
.filter(|_| ec.browser || ec_consent_withdrawn(ec.context.consent())),
),
),

EcRoute::Auction => {
handle_auction(
&state.settings,
Expand Down Expand Up @@ -481,6 +493,7 @@ enum NamedRouteHandler {
LegacyAdminDenied,
Auction,
PageBids,
ConsentUpdate,
Identify,
FirstPartyProxy,
FirstPartyClick,
Expand All @@ -504,8 +517,23 @@ const LEGACY_ADMIN_DENY_METHODS: &[Method] = &[
Method::DELETE,
];

fn named_routes() -> [NamedRoute; 18] {
fn named_routes() -> [NamedRoute; 19] {
[
NamedRoute {
path: trusted_server_core::ec::consent_update::CONSENT_UPDATE_PATH,
primary_methods: &[

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 — Listing every method (including CONNECT/TRACE) for a POST-only endpoint looks surprising without context; the reason only appears in the Axum route test. A short comment here (and on the matching Fastly NAMED_ROUTES entry) would save the next reader a trip.

Suggested change
primary_methods: &[
// Claim every method so non-POST requests reach
// `prepare_consent_update` and get a local 405 with `Allow: POST`
// instead of falling through to the publisher proxy.
primary_methods: &[

Method::GET,
Method::POST,
Method::HEAD,
Method::OPTIONS,
Method::PUT,
Method::PATCH,
Method::DELETE,
Method::TRACE,
Method::CONNECT,
],
handler: NamedRouteHandler::ConsentUpdate,
},
NamedRoute {
path: "/_ts/api/v1/identify",
primary_methods: &[Method::GET, Method::OPTIONS],
Expand Down Expand Up @@ -683,6 +711,9 @@ fn named_route_handler(
handle_admin_eids_lookup(&state.partners, &req)
}
NamedRouteHandler::LegacyAdminDenied => Ok(legacy_admin_alias_denied()),
NamedRouteHandler::ConsentUpdate => {
dispatch_ec_route(&state, &services, req, EcRoute::ConsentUpdate).await
}
NamedRouteHandler::Identify => {
// Read-only identity lookup never enters generation or
// response finalization, including cookie ingestion.
Expand Down
56 changes: 56 additions & 0 deletions crates/trusted-server-adapter-axum/tests/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,61 @@ fn axum_main_exits_non_zero_when_settings_fail() {
);
}

#[tokio::test]
async fn consent_route_rejects_other_methods_and_foreign_origins_locally() {
// CONNECT is registered defensively, but HTTP authority-form targets cannot
// address this path. Core's method tests cover its application rejection.
assert_route_registered("CONNECT", "/_ts/consent");
let server = TestServer::with_test_settings().await;
for method in ["GET", "HEAD", "OPTIONS", "PUT", "PATCH", "DELETE", "TRACE"] {
assert_route_registered(method, "/_ts/consent");
let response = send(server.request(method, "/_ts/consent")).await;
assert_eq!(
response.status().as_u16(),
405,
"should reject {method} locally"
);
assert!(!response.headers().contains_key("set-cookie"));
assert_eq!(response.headers()["allow"], "POST");
}
let response = send(
server
.request("POST", "/_ts/consent")
.header("origin", "https://foreign.example.com")
.header("content-type", "application/json")
.body(r#"{"gppString":null,"applicableSections":[]}"#),
)
.await;
assert_eq!(
response.status().as_u16(),
403,
"should reject a foreign origin"
);
assert!(!response.headers().contains_key("set-cookie"));
server.assert_no_outbound_requests();
}

#[tokio::test]
async fn consent_route_without_storage_is_unavailable_and_does_not_mint() {
let server = TestServer::with_test_settings().await;
let origin = format!("http://{}", server.server.address());
let response = send(
server
.request("POST", "/_ts/consent")
.header("origin", origin)
.header("content-type", "application/json")
.body(r#"{"gppString":null,"applicableSections":[]}"#),
)
.await;
assert_eq!(
response.status().as_u16(),
503,
"should not acknowledge unavailable storage"
);
assert!(!response.headers().contains_key("set-cookie"));
server.assert_no_outbound_requests();
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn aps_profile_serves_renderer_through_adapter_fallback() {
let mut settings = test_settings();
Expand Down Expand Up @@ -299,6 +354,7 @@ fn all_explicit_routes_are_registered() {
("POST", "/admin/keys/rotate"),
("POST", "/admin/keys/deactivate"),
("POST", "/auction"),
("POST", "/_ts/consent"),
// SPA re-auction endpoint, plus its deprecated `/__ts/` alias. Both
// paths are spelled out as literals rather than referencing
// `PAGE_BIDS_PATH` / `PAGE_BIDS_LEGACY_PATH` so this test pins the
Expand Down
16 changes: 16 additions & 0 deletions crates/trusted-server-adapter-cloudflare/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,22 @@ fn router_builder(state: &Arc<AppState>) -> RouterBuilder {
) -> Result<Response, EdgeError> {
let services = state.services_for_request(&ctx)?;
let mut req = ctx.into_request()?;
if req.uri().path() == trusted_server_core::ec::consent_update::CONSENT_UPDATE_PATH {
// Reserve this path without an identity graph; never forward
// consent snapshots or acknowledge an unpersisted revocation.
return Ok(
match trusted_server_core::ec::consent_update::prepare_consent_update(
&services, req,
)
.await
{
Ok(req) => trusted_server_core::ec::consent_update::handle_consent_update(
&req, None,
),
Err(response) => *response,
},
);
}
if let Some(response) = deny_admin_diagnostic_fallback(&req) {
return Ok(response);
}
Expand Down
38 changes: 38 additions & 0 deletions crates/trusted-server-adapter-fastly/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,14 @@ async fn execute_named(
) -> Result<Response, EdgeError> {
let services = build_per_request_services(&state, &ctx)?;
let mut req = ctx.into_request()?;
if matches!(handler, NamedRouteHandler::ConsentUpdate) {
req = match trusted_server_core::ec::consent_update::prepare_consent_update(&services, req)
.await
{
Ok(req) => req,
Err(response) => return Ok(*response),
};
}

// S2S batch sync uses Bearer auth (not EC cookies), so it skips EC
// context creation entirely — mirroring the dedicated early arm in the
Expand Down Expand Up @@ -698,6 +706,12 @@ async fn run_named_route(
.await
}
}
NamedRouteHandler::ConsentUpdate => Ok(

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.

🤔 Fastly and Axum disagree for bot-gated clients

Fastly passes finalize_kv_graph, which is None for a client that fails looks_like_browser() unless the request is a withdrawal, so a grant from such a client gets 503. Axum passes the ungated ec.graph, so the same request gets 204. On Fastly, a privacy-hardened real browser misclassified by the bot gate will re-POST on every focus. Minor, but worth picking one rule and documenting it.

Related coverage note: /_ts/consent has route-level tests only in trusted-server-adapter-axum/tests/routes.rs; the Fastly wiring (prepare before build_ec_request_state, 503 marking through the entry-point finalize) and the Cloudflare/Spin reservations are exercised only indirectly through the core unit tests.

trusted_server_core::ec::consent_update::handle_consent_update(
&req,
ec.finalize_kv_graph.as_ref(),
),
),
NamedRouteHandler::SetTester => handle_set_tester(&state.settings),
NamedRouteHandler::ClearTester => handle_clear_tester(&state.settings),
NamedRouteHandler::Auction => {
Expand Down Expand Up @@ -807,6 +821,14 @@ async fn dispatch_fallback(
services: &RuntimeServices,
mut req: Request,
) -> Response {
if req.uri().path() == trusted_server_core::ec::consent_update::CONSENT_UPDATE_PATH {
return match trusted_server_core::ec::consent_update::prepare_consent_update(services, req)
.await
{
Ok(req) => trusted_server_core::ec::consent_update::handle_consent_update(&req, None),
Err(response) => *response,
};
}
if let Some(response) = deny_admin_diagnostic_fallback(&req) {
return response;
}
Expand Down Expand Up @@ -1091,6 +1113,7 @@ enum NamedRouteHandler {
LegacyAdminDenied,
BatchSync,
Identify,
ConsentUpdate,
SetTester,
ClearTester,
Auction,
Expand Down Expand Up @@ -1123,6 +1146,21 @@ const LEGACY_ADMIN_DENY_METHODS: &[Method] = &[
];

const NAMED_ROUTES: &[NamedRoute] = &[
NamedRoute {
path: trusted_server_core::ec::consent_update::CONSENT_UPDATE_PATH,
primary_methods: &[
Method::GET,
Method::POST,
Method::HEAD,
Method::OPTIONS,
Method::PUT,
Method::PATCH,
Method::DELETE,
Method::TRACE,
Method::CONNECT,
],
handler: NamedRouteHandler::ConsentUpdate,
},
NamedRoute {
path: "/.well-known/trusted-server.json",
primary_methods: &[Method::GET],
Expand Down
16 changes: 16 additions & 0 deletions crates/trusted-server-adapter-spin/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -820,6 +820,22 @@ fn router_builder(state: &Arc<AppState>) -> RouterBuilder {
) -> Result<Response, EdgeError> {
let services = state.services_for_request(&ctx)?;
let mut req = ctx.into_request()?;
if req.uri().path() == trusted_server_core::ec::consent_update::CONSENT_UPDATE_PATH {
// Reserve this path without an identity graph; never forward
// consent snapshots or acknowledge an unpersisted revocation.
return Ok(
match trusted_server_core::ec::consent_update::prepare_consent_update(
&services, req,
)
.await
{
Ok(req) => trusted_server_core::ec::consent_update::handle_consent_update(
&req, None,
),
Err(response) => *response,
},
);
}
if let Some(response) = deny_admin_diagnostic_fallback(&req) {
return Ok(response);
}
Expand Down
Loading
Loading