Skip to content
Draft
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
d5be3d9
Add admin endpoint to look up EC entries by id
prk-Jr Jul 17, 2026
12592bb
Do not bot-gate the admin EC lookup KV graph
prk-Jr Jul 17, 2026
9869ac7
Add admin endpoint to echo request EID cookies
prk-Jr Jul 17, 2026
42a1d1e
Merge branch 'feat/admin-ec-lookup-endpoint' into rc/july_admin_ec_route
prk-Jr Jul 17, 2026
966c856
Add /_ts/trace overlay to confirm creatives render on the page
prk-Jr Jul 18, 2026
8dc31cc
Add ISO 8601 companions to admin EC lookup timestamps
prk-Jr Jul 18, 2026
c13eb92
Merge branch 'feat/admin-ec-lookup-endpoint' into rc/july_admin_ec_route
prk-Jr Jul 18, 2026
f82f7c9
Merge remote-tracking branch 'origin/rc/july' into rc/july_admin_ec_r…
prk-Jr Jul 18, 2026
3efa5c0
Merge remote-tracking branch 'origin/trace-auction-winners-to-creativ…
prk-Jr Jul 18, 2026
e1a8b81
Trace client-side /auction renders and stop overclaiming GAM renders
prk-Jr Jul 18, 2026
16793b8
Merge branch 'trace-auction-winners-to-creatives' into rc/july_admin_…
prk-Jr Jul 18, 2026
e1badbb
Recover GPT slots orphaned by a client re-render
prk-Jr Jul 18, 2026
013f5e6
Show the render trace as a timeline and badge every rendered slot
prk-Jr Jul 18, 2026
c4999f7
Stop attributing GAM refreshes to the finished server-side auction
prk-Jr Jul 20, 2026
c0811bc
Number every render and keep GAM's fill signal on refresh rows
prk-Jr Jul 20, 2026
b4908cd
Show absent attribution as — instead of ? in trace tooltips
prk-Jr Jul 20, 2026
25316dd
Trace SSAT creatives the Universal Creative bridge renders inline
prk-Jr Jul 20, 2026
281df38
Fall back to the OpenRTB bid id for hb_adid when cache_id and ad_id a…
prk-Jr Jul 20, 2026
c52d9c0
Fix ext.trusted_server.request_host to use the publisher domain
prk-Jr Jul 20, 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
56 changes: 55 additions & 1 deletion crates/trusted-server-adapter-axum/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ use error_stack::Report;
use trusted_server_core::auction::endpoints::handle_auction;
use trusted_server_core::auction::{AuctionOrchestrator, build_orchestrator};
use trusted_server_core::ec::EcContext;
use trusted_server_core::ec::admin::handle_admin_eids_lookup;
use trusted_server_core::ec::registry::PartnerRegistry;
use trusted_server_core::error::{IntoHttpResponse as _, TrustedServerError};
use trusted_server_core::integrations::{IntegrationRegistry, ProxyDispatchInput};
use trusted_server_core::proxy::{
Expand All @@ -29,6 +31,7 @@ use trusted_server_core::settings::Settings;
use trusted_server_core::settings_data::{
default_config_key, default_config_store_name, get_settings_from_config_store,
};
use trusted_server_core::trace_cookie::handle_trace_mode;

use trusted_server_core::platform::RuntimeServices;

Expand Down Expand Up @@ -252,9 +255,12 @@ enum NamedRouteHandler {
TrustedServerDiscovery,
VerifySignature,
AdminNotSupported,
AdminEcNotSupported,
AdminEidsLookup,
/// Legacy `/admin/keys/*` aliases — denied locally with 404 so they never
/// reach the publisher fallback (which would leak admin credentials).
LegacyAdminDenied,
TraceMode,
Auction,
PageBids,
FirstPartyProxy,
Expand All @@ -279,7 +285,7 @@ const LEGACY_ADMIN_DENY_METHODS: &[Method] = &[
Method::DELETE,
];

fn named_routes() -> [NamedRoute; 12] {
fn named_routes() -> [NamedRoute; 16] {
[
NamedRoute {
path: "/.well-known/trusted-server.json",
Expand All @@ -304,6 +310,26 @@ fn named_routes() -> [NamedRoute; 12] {
primary_methods: &[Method::POST],
handler: NamedRouteHandler::AdminNotSupported,
},
// Admin EC lookup routes. Registered explicitly (like the key routes
// above) so they never fall through to the publisher fallback, and
// they match `Settings::ADMIN_ENDPOINTS` for auth coverage.
NamedRoute {
path: "/_ts/admin/ec",
primary_methods: &[Method::GET],
handler: NamedRouteHandler::AdminEcNotSupported,
},
NamedRoute {
path: "/_ts/admin/ec/{id}",
primary_methods: &[Method::GET],
handler: NamedRouteHandler::AdminEcNotSupported,
},
// Admin EIDs echo: pure request inspection (no KV), so the dev
// server serves the real handler.
NamedRoute {
path: "/_ts/admin/eids",
primary_methods: &[Method::GET],
handler: NamedRouteHandler::AdminEidsLookup,
},
// The legacy non-`/_ts` aliases (`/admin/keys/*`) are denied locally with
// a 404, matching the Fastly and Cloudflare adapters: the production
// basic-auth handler regex `^/_ts/admin` does not match them, and letting
Expand All @@ -320,6 +346,11 @@ fn named_routes() -> [NamedRoute; 12] {
primary_methods: LEGACY_ADMIN_DENY_METHODS,
handler: NamedRouteHandler::LegacyAdminDenied,
},
NamedRoute {
path: "/_ts/trace",
primary_methods: &[Method::GET],
handler: NamedRouteHandler::TraceMode,
},
NamedRoute {
path: "/auction",
primary_methods: &[Method::POST],
Expand Down Expand Up @@ -388,7 +419,30 @@ fn named_route_handler(
);
Ok(resp)
}
NamedRouteHandler::AdminEcNotSupported => {
// The EC identity graph is Fastly KV backed; the Axum
// dev server has no store to read.
let body = edgezero_core::body::Body::from(
"Admin EC lookup is not supported on the Axum dev server.\n\
Use the Fastly adapter (via Viceroy or deployed) to inspect EC entries.\n",
);
let mut resp = Response::new(body);
*resp.status_mut() = StatusCode::NOT_IMPLEMENTED;
resp.headers_mut().insert(
header::CONTENT_TYPE,
HeaderValue::from_static("text/plain; charset=utf-8"),
);
Ok(resp)
}
NamedRouteHandler::AdminEidsLookup => {
let partner_registry =
PartnerRegistry::from_config(&state.settings.ec.partners)?;
handle_admin_eids_lookup(&partner_registry, &req)
}
NamedRouteHandler::LegacyAdminDenied => Ok(legacy_admin_alias_denied()),
NamedRouteHandler::TraceMode => {
handle_trace_mode(&state.settings, req.uri().query())
}
NamedRouteHandler::Auction => {
// Build the geo-aware EC context so the auction consent
// gate sees the caller's jurisdiction — `EcContext::default()`
Expand Down
60 changes: 60 additions & 0 deletions crates/trusted-server-adapter-axum/tests/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ fn all_explicit_routes_are_registered() {
("POST", "/verify-signature"),
("POST", "/_ts/admin/keys/rotate"),
("POST", "/_ts/admin/keys/deactivate"),
("GET", "/_ts/admin/ec"),
("GET", "/_ts/admin/ec/{id}"),
("GET", "/_ts/admin/eids"),
("POST", "/admin/keys/rotate"),
("POST", "/admin/keys/deactivate"),
("POST", "/auction"),
Expand Down Expand Up @@ -256,6 +259,63 @@ async fn admin_route_without_credentials_returns_401() {
);
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn authenticated_admin_ec_routes_return_501() {
// The EC identity graph is Fastly KV backed, so the Axum dev server
// answers the admin EC lookup routes locally with 501 instead of letting
// them fall through to the publisher fallback.
let sample_ec_id = format!("{}.abc123", "a".repeat(64));
for path in [
"/_ts/admin/ec".to_owned(),
format!("/_ts/admin/ec/{sample_ec_id}"),
] {
let mut svc = make_service();
let req = Request::builder()
.method("GET")
.uri(&path)
.header("authorization", "Basic YWRtaW46YWRtaW4tcGFzcw==")
.body(AxumBody::empty())
.expect("should build request");
let resp = svc
.ready()
.await
.expect("should be ready")
.call(req)
.await
.expect("should respond");
assert_eq!(
resp.status().as_u16(),
501,
"{path} should report that Axum EC lookup is unsupported"
);
}
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn authenticated_admin_eids_route_returns_200() {
// The EIDs echo is pure request inspection (no KV), so the dev server
// serves the real handler.
let mut svc = make_service();
let req = Request::builder()
.method("GET")
.uri("/_ts/admin/eids")
.header("authorization", "Basic YWRtaW46YWRtaW4tcGFzcw==")
.body(AxumBody::empty())
.expect("should build request");
let resp = svc
.ready()
.await
.expect("should be ready")
.call(req)
.await
.expect("should respond");
assert_eq!(
resp.status().as_u16(),
200,
"/_ts/admin/eids should serve the real EIDs echo handler"
);
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn legacy_admin_aliases_denied_locally_not_proxied_to_publisher() {
// Regression for the credential-leak finding: the production basic-auth regex
Expand Down
46 changes: 46 additions & 0 deletions crates/trusted-server-adapter-cloudflare/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ use trusted_server_core::auction::{AuctionOrchestrator, build_orchestrator};
#[cfg(target_arch = "wasm32")]
use trusted_server_core::config_payload::settings_from_config_blob;
use trusted_server_core::ec::EcContext;
use trusted_server_core::ec::admin::handle_admin_eids_lookup;
use trusted_server_core::ec::registry::PartnerRegistry;
use trusted_server_core::error::{IntoHttpResponse as _, TrustedServerError};
use trusted_server_core::integrations::{IntegrationRegistry, ProxyDispatchInput};
use trusted_server_core::platform::RuntimeServices;
Expand All @@ -28,6 +30,7 @@ use trusted_server_core::request_signing::{
handle_trusted_server_discovery, handle_verify_signature,
};
use trusted_server_core::settings::Settings;
use trusted_server_core::trace_cookie::handle_trace_mode;

use crate::middleware::{AuthMiddleware, FinalizeResponseMiddleware};
use crate::platform::build_runtime_services;
Expand Down Expand Up @@ -242,6 +245,20 @@ fn admin_key_management_not_supported() -> Response {
response
}

fn admin_ec_lookup_not_supported() -> Response {
let body = edgezero_core::body::Body::from(
"Admin EC lookup is not supported on Cloudflare Workers.\n\
Use the Fastly adapter (via Viceroy or deployed) to inspect EC entries.\n",
);
let mut response = Response::new(body);
*response.status_mut() = StatusCode::NOT_IMPLEMENTED;
response.headers_mut().insert(
header::CONTENT_TYPE,
HeaderValue::from_static("text/plain; charset=utf-8"),
);
response
}

/// Builds the local `404 Not Found` returned for legacy `/admin/keys/*`
/// aliases on the Cloudflare adapter.
///
Expand Down Expand Up @@ -461,6 +478,35 @@ fn build_router(state: &Arc<AppState>) -> RouterService {
.post("/_ts/admin/keys/deactivate", |_ctx: RequestContext| async {
Ok::<Response, EdgeError>(admin_key_management_not_supported())
})
// Admin EC lookup routes. Registered explicitly (like the key
// routes above) so they never fall through to the publisher
// fallback, and they match `Settings::ADMIN_ENDPOINTS` for auth
// coverage. The EC identity graph is Fastly KV backed, so this
// adapter has no store to read.
.get("/_ts/admin/ec", |_ctx: RequestContext| async {
Ok::<Response, EdgeError>(admin_ec_lookup_not_supported())
})
.get("/_ts/admin/ec/{id}", |_ctx: RequestContext| async {
Ok::<Response, EdgeError>(admin_ec_lookup_not_supported())
})
// Admin EIDs echo: pure request inspection (no KV), so this
// adapter serves the real handler.
.get(
"/_ts/admin/eids",
make_handler(Arc::clone(&state), |s, _services, req| async move {
let partner_registry = PartnerRegistry::from_config(&s.settings.ec.partners)?;
handle_admin_eids_lookup(&partner_registry, &req)
}),
)
// Render-trace toggle: arms/disarms the ts-trace cookie and
// redirects to `/`. Gated by [debug] trace_route_enabled (404 when
// off).
.get(
"/_ts/trace",
make_handler(Arc::clone(&state), |s, _services, req| async move {
handle_trace_mode(&s.settings, req.uri().query())
}),
)
.post(
"/auction",
make_handler(Arc::clone(&state), |s, services, req| async move {
Expand Down
48 changes: 48 additions & 0 deletions crates/trusted-server-adapter-cloudflare/tests/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,9 @@ fn all_explicit_routes_are_registered() {
("POST", "/verify-signature"),
("POST", "/_ts/admin/keys/rotate"),
("POST", "/_ts/admin/keys/deactivate"),
("GET", "/_ts/admin/ec"),
("GET", "/_ts/admin/ec/{id}"),
("GET", "/_ts/admin/eids"),
("POST", "/auction"),
("GET", "/first-party/proxy"),
("GET", "/first-party/click"),
Expand Down Expand Up @@ -264,6 +267,51 @@ async fn authenticated_admin_routes_return_501() {
}
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn authenticated_admin_ec_routes_return_501() {
// The EC identity graph is Fastly KV backed, so Cloudflare answers the
// admin EC lookup routes locally with 501 instead of letting them fall
// through to the publisher fallback.
let sample_ec_id = format!("{}.abc123", "a".repeat(64));
for path in [
"/_ts/admin/ec".to_owned(),
format!("/_ts/admin/ec/{sample_ec_id}"),
] {
let req = request_builder()
.method("GET")
.uri(&path)
.header("authorization", "Basic YWRtaW46YWRtaW4tcGFzcw==")
.body(edgezero_core::body::Body::empty())
.expect("should build request");
let resp = route(test_router(), req).await;

assert_eq!(
resp.status().as_u16(),
501,
"{path} should report that Cloudflare EC lookup is unsupported"
);
}
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn authenticated_admin_eids_route_returns_200() {
// The EIDs echo is pure request inspection (no KV), so this adapter
// serves the real handler.
let req = request_builder()
.method("GET")
.uri("/_ts/admin/eids")
.header("authorization", "Basic YWRtaW46YWRtaW4tcGFzcw==")
.body(edgezero_core::body::Body::empty())
.expect("should build request");
let resp = route(test_router(), req).await;

assert_eq!(
resp.status().as_u16(),
200,
"/_ts/admin/eids should serve the real EIDs echo handler"
);
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn admin_route_without_credentials_returns_401() {
let router = test_router();
Expand Down
Loading
Loading