Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion crates/trusted-server-adapter-axum/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use edgezero_core::router::RouterService;
use error_stack::Report;
use trusted_server_core::auction::endpoints::handle_auction;
use trusted_server_core::auction::{AuctionOrchestrator, build_orchestrator};
use trusted_server_core::cache_policy::EdgeCacheHeader;
use trusted_server_core::ec::EcContext;
use trusted_server_core::error::{IntoHttpResponse as _, TrustedServerError};
use trusted_server_core::integrations::{IntegrationRegistry, ProxyDispatchInput};
Expand Down Expand Up @@ -176,7 +177,7 @@ async fn dispatch_fallback(
let method = req.method().clone();

if method == Method::GET && path.starts_with("/static/tsjs=") {
return handle_tsjs_dynamic(&req, &state.registry);
return handle_tsjs_dynamic(&req, &state.registry, EdgeCacheHeader::SMaxageFallback);
}

if state.registry.has_route(&method, &path) {
Expand Down Expand Up @@ -215,6 +216,7 @@ async fn dispatch_fallback(
&mut ec_context,
auction,
req,
EdgeCacheHeader::SMaxageFallback,
)
.await?;
// Async finalize so the dispatched auction is collected and its bids are
Expand Down
36 changes: 36 additions & 0 deletions crates/trusted-server-adapter-axum/tests/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,42 @@ async fn tsjs_route_prefix_is_handled_not_5xx() {
);
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn tsjs_route_matching_hash_uses_s_maxage_fallback() {
let mut svc = make_service();
let src = trusted_server_core::tsjs::tsjs_script_src(&["creative"]);
let req = Request::builder()
.method("GET")
.uri(src)
.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,
"matching TSJS hash should serve OK"
);
assert_eq!(
resp.headers()
.get("cache-control")
.and_then(|value| value.to_str().ok()),
Some("public, max-age=31536000, s-maxage=31536000, immutable"),
"Axum adapter should render the portable s-maxage fallback"
);
assert!(
resp.headers().get("surrogate-control").is_none(),
"s-maxage fallback must not emit Fastly Surrogate-Control"
);
}

// ---------------------------------------------------------------------------
// Middleware tests
// ---------------------------------------------------------------------------
Expand Down
8 changes: 7 additions & 1 deletion crates/trusted-server-adapter-cloudflare/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use edgezero_core::router::RouterService;
use error_stack::Report;
use trusted_server_core::auction::endpoints::handle_auction;
use trusted_server_core::auction::{AuctionOrchestrator, build_orchestrator};
use trusted_server_core::cache_policy::EdgeCacheHeader;
#[cfg(target_arch = "wasm32")]
use trusted_server_core::config_payload::settings_from_config_blob;
use trusted_server_core::ec::EcContext;
Expand Down Expand Up @@ -367,7 +368,11 @@ fn build_router(state: &Arc<AppState>) -> RouterService {
let allow_tsjs = method == Method::GET;

let result = if allow_tsjs && path.starts_with("/static/tsjs=") {
handle_tsjs_dynamic(&req, &state.registry)
handle_tsjs_dynamic(
&req,
&state.registry,
EdgeCacheHeader::CloudflareCdnCacheControl,
)
} else if state.registry.has_route(&method, &path) {
let mut ec_context = EcContext::default();
state
Expand Down Expand Up @@ -401,6 +406,7 @@ fn build_router(state: &Arc<AppState>) -> RouterService {
&mut ec_context,
auction,
req,
EdgeCacheHeader::CloudflareCdnCacheControl,
)
.await
{
Expand Down
37 changes: 37 additions & 0 deletions crates/trusted-server-adapter-cloudflare/tests/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,43 @@ async fn tsjs_route_is_routed_not_5xx() {
assert!(status < 500, "tsjs route must not 5xx: got {status}");
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn tsjs_route_emits_cloudflare_cache_header_for_matching_hash() {
let router = test_router();
let src = trusted_server_core::tsjs::tsjs_script_src(&["creative"]);
let req = request_builder()
.method("GET")
.uri(src)
.body(edgezero_core::body::Body::empty())
.expect("should build request");

let resp = route(router, req).await;

assert_eq!(
resp.status().as_u16(),
200,
"matching TSJS hash should serve OK"
);
assert_eq!(
resp.headers()
.get("cache-control")
.and_then(|value| value.to_str().ok()),
Some("public, max-age=31536000, immutable"),
"browser cache policy should be immutable for matching TSJS hash"
);
assert_eq!(
resp.headers()
.get("cloudflare-cdn-cache-control")
.and_then(|value| value.to_str().ok()),
Some("max-age=31536000"),
"Cloudflare adapter should emit the Cloudflare-specific edge header"
);
assert!(
resp.headers().get("surrogate-control").is_none(),
"Cloudflare adapter must not emit Fastly Surrogate-Control"
);
}

/// Verify that every expected explicit route is registered in the route table.
///
/// Uses [`RouterService::routes()`] for introspection rather than checking
Expand Down
3 changes: 3 additions & 0 deletions crates/trusted-server-adapter-cloudflare/wrangler.ci.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ compatibility_date = "2024-09-23"
compatibility_flags = ["nodejs_compat"]
# No [build] section — bundle is pre-built in CI; wrangler dev must not rebuild.

[cache]
enabled = true

[[kv_namespaces]]
binding = "TRUSTED_SERVER_KV"
id = "ci-local-kv"
Expand Down
3 changes: 3 additions & 0 deletions crates/trusted-server-adapter-cloudflare/wrangler.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ main = "build/index.js"
compatibility_date = "2024-09-23"
compatibility_flags = ["nodejs_compat"]

[cache]
enabled = true

[build]
command = "bash build.sh"

Expand Down
4 changes: 3 additions & 1 deletion crates/trusted-server-adapter-fastly/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ use error_stack::Report;
use trusted_server_core::auction::AuctionTelemetrySink;
use trusted_server_core::auction::endpoints::handle_auction;
use trusted_server_core::auction::{AuctionOrchestrator, build_orchestrator};
use trusted_server_core::cache_policy::EdgeCacheHeader;
use trusted_server_core::constants::{COOKIE_SHAREDID, COOKIE_TS_EIDS};
use trusted_server_core::ec::EcContext;
use trusted_server_core::ec::batch_sync::handle_batch_sync;
Expand Down Expand Up @@ -719,7 +720,7 @@ async fn dispatch_fallback(
};

let result = if uses_dynamic_tsjs_fallback(&method, &path) {
handle_tsjs_dynamic(&req, &state.registry)
handle_tsjs_dynamic(&req, &state.registry, EdgeCacheHeader::SurrogateControl)
} else if state.registry.has_route(&method, &path) {
// Integration-proxy responses are not bounded by publisher.max_buffered_body_bytes.
// Only the handle_publisher_request branch below routes through
Expand Down Expand Up @@ -794,6 +795,7 @@ async fn dispatch_fallback(
&mut ec.ec_context,
auction,
req,
EdgeCacheHeader::SurrogateControl,
)
.await
{
Expand Down
10 changes: 6 additions & 4 deletions crates/trusted-server-adapter-fastly/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use error_stack::Report;
use fastly::http::Method as FastlyMethod;
use fastly::{Request as FastlyRequest, Response as FastlyResponse};

use trusted_server_core::cache_policy::EdgeCacheHeader;
use trusted_server_core::ec::device::DeviceSignals;
use trusted_server_core::ec::finalize::ec_finalize_response;
use trusted_server_core::ec::kv::KvIdentityGraph;
Expand Down Expand Up @@ -202,7 +203,7 @@ fn edgezero_main(mut req: FastlyRequest) {
}

if let Some(policy) = asset_cache_policy {
policy.apply_after_route_finalization(&mut response);
policy.apply_after_route_finalization(&mut response, EdgeCacheHeader::SurrogateControl);
}

if let Some(ec_state) = ec_state {
Expand Down Expand Up @@ -333,10 +334,11 @@ fn send_edgezero_response(
effects.apply_to_response(&mut response);
}

// Final cache guard: EC finalization and request-filter effects may have
// added a per-user Set-Cookie after `apply_finalize_headers` ran, so
// re-apply the privacy downgrade before send.
// Final cache guards: EC finalization and request-filter effects may have
// added a per-user Set-Cookie or a private/no-store directive after
// `apply_finalize_headers` and normalized asset policy reapplication ran.
crate::middleware::enforce_set_cookie_cache_privacy(&mut response);
crate::middleware::enforce_uncacheable_cache_privacy(&mut response);

let (parts, body) = response.into_parts();

Expand Down
27 changes: 26 additions & 1 deletion crates/trusted-server-adapter-fastly/src/middleware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,9 @@ pub(crate) fn apply_finalize_headers(
/// entry point (`main.rs`) can re-apply it after
/// [`ec_finalize_response`](trusted_server_core::ec::finalize::ec_finalize_response)
/// writes the EC identity `Set-Cookie`, using the single shared implementation.
pub(crate) use trusted_server_core::response_privacy::enforce_set_cookie_cache_privacy;
pub(crate) use trusted_server_core::response_privacy::{
enforce_set_cookie_cache_privacy, enforce_uncacheable_cache_privacy,
};

// ---------------------------------------------------------------------------
// Tests
Expand Down Expand Up @@ -496,6 +498,29 @@ mod tests {
);
}

#[test]
fn enforce_uncacheable_cache_privacy_handles_late_filter_headers() {
let mut response = response_with_headers(&[
("cache-control", "private, max-age=0"),
("surrogate-control", "max-age=600"),
]);

enforce_uncacheable_cache_privacy(&mut response);

assert_eq!(
response
.headers()
.get("cache-control")
.and_then(|value| value.to_str().ok()),
Some("private, max-age=0"),
"should preserve the late private directive"
);
assert!(
response.headers().get("surrogate-control").is_none(),
"should strip the normalized edge header after late filter effects"
);
}

// ---------------------------------------------------------------------------
// FinalizeResponseMiddleware::handle tests
// ---------------------------------------------------------------------------
Expand Down
4 changes: 3 additions & 1 deletion crates/trusted-server-adapter-spin/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use edgezero_core::router::RouterService;
use error_stack::Report;
use trusted_server_core::auction::endpoints::handle_auction;
use trusted_server_core::auction::{AuctionOrchestrator, build_orchestrator};
use trusted_server_core::cache_policy::EdgeCacheHeader;
use trusted_server_core::ec::EcContext;
use trusted_server_core::error::{IntoHttpResponse as _, TrustedServerError};
use trusted_server_core::http_util::sanitize_forwarded_headers;
Expand Down Expand Up @@ -637,7 +638,7 @@ fn build_router(state: &Arc<AppState>) -> RouterService {
// Dynamic tsjs serving is GET-only; other methods fall through to the
// integration/publisher fallback.
let result = if method == Method::GET && path.starts_with("/static/tsjs=") {
handle_tsjs_dynamic(&req, &state.registry)
handle_tsjs_dynamic(&req, &state.registry, EdgeCacheHeader::SMaxageFallback)
} else if state.registry.has_route(&method, &path) {
let mut ec_context = EcContext::default();
state
Expand Down Expand Up @@ -671,6 +672,7 @@ fn build_router(state: &Arc<AppState>) -> RouterService {
&mut ec_context,
auction,
req,
EdgeCacheHeader::SMaxageFallback,
)
.await
{
Expand Down
30 changes: 30 additions & 0 deletions crates/trusted-server-adapter-spin/tests/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,36 @@ async fn tsjs_route_is_routed_not_5xx() {
assert!(status < 500, "tsjs route must not 5xx: got {status}");
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn tsjs_route_matching_hash_uses_s_maxage_fallback() {
let router = test_router();
let src = trusted_server_core::tsjs::tsjs_script_src(&["creative"]);
let req = request_builder()
.method("GET")
.uri(src)
.body(edgezero_core::body::Body::empty())
.expect("should build request");

let resp = route(router, req).await;

assert_eq!(
resp.status().as_u16(),
200,
"matching TSJS hash should serve OK"
);
assert_eq!(
resp.headers()
.get("cache-control")
.and_then(|value| value.to_str().ok()),
Some("public, max-age=31536000, s-maxage=31536000, immutable"),
"Spin adapter should render the portable s-maxage fallback"
);
assert!(
resp.headers().get("surrogate-control").is_none(),
"s-maxage fallback must not emit Fastly Surrogate-Control"
);
}

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