diff --git a/crates/trusted-server-adapter-fastly/src/app.rs b/crates/trusted-server-adapter-fastly/src/app.rs index 3b8fad6a..10bd37a0 100644 --- a/crates/trusted-server-adapter-fastly/src/app.rs +++ b/crates/trusted-server-adapter-fastly/src/app.rs @@ -1,6 +1,6 @@ //! Full `EdgeZero` application wiring for Trusted Server. //! -//! Registers all routes from the legacy [`crate::route_request`] into a +//! Registers all routes for the Trusted Server into a //! [`RouterService`]. On successful startup, attaches [`FinalizeResponseMiddleware`] //! (outermost) and [`AuthMiddleware`] (inner). When startup fails, //! [`startup_error_router`] returns a bare router without middleware. @@ -114,8 +114,8 @@ pub(crate) fn build_state() -> Result, Report> /// /// When `settings.consent.consent_store` is configured and the named KV store cannot /// be opened, returns `Err` so the caller can respond with 503 (fail-closed). This -/// matches the legacy `route_request` behavior where a misconfigured consent store -/// makes consent-dependent routes unavailable rather than proceeding without consent. +/// ensures a misconfigured consent store makes consent-dependent routes unavailable +/// rather than proceeding without consent (fail-closed). /// /// # Errors /// @@ -239,11 +239,7 @@ async fn dispatch_fallback( // Error helper // --------------------------------------------------------------------------- -/// Convert a [`Report`] into an HTTP [`Response`], -/// mirroring [`crate::http_error_response`] exactly. -/// -/// The near-identical function in `main.rs` is intentional: the legacy path -/// uses fastly HTTP types while this path uses `edgezero_core` types. +/// Converts a [`Report`] into an HTTP [`Response`]. pub(crate) fn http_error(report: &Report) -> Response { let root_error = report.current_context(); log::error!("Error occurred: {:?}", report); @@ -676,9 +672,9 @@ mod tests { #[test] fn dispatch_head_on_named_get_route_falls_through_to_publisher_fallback() { // Regression guard: HEAD /first-party/proxy must reach the publisher - // fallback, not return a router-level 405. Legacy route_request proxies - // every (method, path) combination not matched by a specific arm through - // to the publisher origin. + // fallback, not return a router-level 405. The EdgeZero dispatch path + // proxies every (method, path) combination not matched by a specific + // arm through to the publisher origin. // // Without a live backend the publisher proxy errors (502/503), but the // important invariant is that the status is NOT 405. diff --git a/crates/trusted-server-adapter-fastly/src/compat.rs b/crates/trusted-server-adapter-fastly/src/compat.rs index 9c3edea2..d34747a5 100644 --- a/crates/trusted-server-adapter-fastly/src/compat.rs +++ b/crates/trusted-server-adapter-fastly/src/compat.rs @@ -1,42 +1,9 @@ //! Compatibility bridge between `fastly` SDK types and `http` crate types. -//! -//! Contains only the functions used by the legacy `main()` entry point. -//! Relocated from `trusted-server-core` as part of removing all `fastly` crate -//! imports from the core library. use edgezero_core::body::Body as EdgeBody; -use edgezero_core::http::{Request as HttpRequest, RequestBuilder, Response as HttpResponse, Uri}; +use edgezero_core::http::Response as HttpResponse; use trusted_server_core::http_util::SPOOFABLE_FORWARDED_HEADERS; -fn build_http_request(req: &fastly::Request, body: EdgeBody) -> HttpRequest { - let uri: Uri = req - .get_url_str() - .parse() - .expect("should parse fastly request URL as URI"); - - let mut builder: RequestBuilder = edgezero_core::http::request_builder() - .method(req.get_method().clone()) - .uri(uri); - - for (name, value) in req.get_headers() { - builder = builder.header(name.as_str(), value.as_bytes()); - } - - builder - .body(body) - .expect("should build http request from fastly request") -} - -/// Convert an owned `fastly::Request` into an [`HttpRequest`]. -/// -/// # Panics -/// -/// Panics if the Fastly request URL cannot be parsed as an `http::Uri`. -pub(crate) fn from_fastly_request(mut req: fastly::Request) -> HttpRequest { - let body = EdgeBody::from(req.take_body_bytes()); - build_http_request(&req, body) -} - /// Convert a `fastly::Response` into an [`HttpResponse`]. pub(crate) fn from_fastly_response(mut resp: fastly::Response) -> HttpResponse { let status = resp.get_status(); @@ -71,19 +38,6 @@ pub(crate) fn to_fastly_response(resp: HttpResponse) -> fastly::Response { fastly_resp } -/// Convert an [`HttpResponse`] into a `fastly::Response` without a body. -/// -/// Use this when the caller will stream the body separately through -/// [`fastly::Response::stream_to_client`]. -pub(crate) fn to_fastly_response_skeleton(resp: HttpResponse) -> fastly::Response { - let (parts, _body) = resp.into_parts(); - let mut fastly_resp = fastly::Response::from_status(parts.status.as_u16()); - for (name, value) in &parts.headers { - fastly_resp.append_header(name.as_str(), value.as_bytes()); - } - fastly_resp -} - /// Sanitize forwarded headers on a `fastly::Request`. /// /// Strips headers that clients can spoof before any request-derived context diff --git a/crates/trusted-server-adapter-fastly/src/error.rs b/crates/trusted-server-adapter-fastly/src/error.rs deleted file mode 100644 index 560a2e18..00000000 --- a/crates/trusted-server-adapter-fastly/src/error.rs +++ /dev/null @@ -1,19 +0,0 @@ -//! Error conversion utilities for Fastly. -//! -//! This module provides conversions from [`TrustedServerError`] to HTTP responses. - -use error_stack::Report; -use fastly::Response; -use trusted_server_core::error::{IntoHttpResponse, TrustedServerError}; - -/// Converts a [`TrustedServerError`] into an HTTP error response. -pub fn to_error_response(report: &Report) -> Response { - // Get the root error for status code and message - let root_error = report.current_context(); - - // Log the full error chain for debugging - log::error!("Error occurred: {:?}", report); - - Response::from_status(root_error.status_code()) - .with_body_text_plain(&format!("{}\n", root_error.user_message())) -} diff --git a/crates/trusted-server-adapter-fastly/src/main.rs b/crates/trusted-server-adapter-fastly/src/main.rs index f5385caa..05f164b6 100644 --- a/crates/trusted-server-adapter-fastly/src/main.rs +++ b/crates/trusted-server-adapter-fastly/src/main.rs @@ -5,133 +5,34 @@ use edgezero_adapter_fastly::FastlyConfigStore; use edgezero_core::app::Hooks as _; use edgezero_core::body::Body as EdgeBody; use edgezero_core::config_store::ConfigStoreHandle; -use edgezero_core::http::{ - header, HeaderValue, Method, Request as HttpRequest, Response as HttpResponse, -}; +use edgezero_core::http::{header, HeaderValue, Response as HttpResponse}; use error_stack::Report; use fastly::http::Method as FastlyMethod; use fastly::{Request as FastlyRequest, Response as FastlyResponse}; -use trusted_server_core::auction::endpoints::handle_auction; -use trusted_server_core::auction::AuctionOrchestrator; -use trusted_server_core::auth::enforce_basic_auth; -use trusted_server_core::error::{IntoHttpResponse, TrustedServerError}; +use trusted_server_core::error::TrustedServerError; use trusted_server_core::geo::GeoInfo; use trusted_server_core::integrations::IntegrationRegistry; use trusted_server_core::platform::PlatformGeo as _; -use trusted_server_core::platform::RuntimeServices; -use trusted_server_core::proxy::{ - handle_first_party_click, handle_first_party_proxy, handle_first_party_proxy_rebuild, - handle_first_party_proxy_sign, -}; -use trusted_server_core::publisher::{ - handle_publisher_request, handle_tsjs_dynamic, stream_publisher_body, - OwnedProcessResponseParams, PublisherResponse, -}; -use trusted_server_core::request_signing::{ - handle_deactivate_key, handle_rotate_key, handle_trusted_server_discovery, - handle_verify_signature, -}; +use trusted_server_core::publisher::{stream_publisher_body, PublisherResponse}; use trusted_server_core::settings::Settings; use trusted_server_core::settings_data::get_settings; mod app; mod backend; mod compat; -mod error; mod logging; mod management_api; mod middleware; mod platform; -#[cfg(test)] -mod route_tests; -use crate::app::{build_state, runtime_services_for_consent_route, TrustedServerApp}; -use crate::error::to_error_response; +use crate::app::TrustedServerApp; use crate::middleware::{apply_finalize_headers, HEADER_X_TS_FINALIZED}; -use crate::platform::{build_runtime_services, FastlyPlatformGeo}; +use crate::platform::FastlyPlatformGeo; const TRUSTED_SERVER_CONFIG_STORE: &str = "trusted_server_config"; -const EDGEZERO_ENABLED_KEY: &str = "edgezero_enabled"; -const EDGEZERO_ROLLOUT_PCT_KEY: &str = "edgezero_rollout_pct"; - -/// Result of routing a request, distinguishing buffered from streaming publisher responses. -/// -/// The streaming arm keeps the publisher body out of WASM heap until it is written directly -/// to the client via [`fastly::Response::stream_to_client`]. All other legacy routes are buffered. -enum HandlerOutcome { - Buffered(HttpResponse), - Streaming { - response: HttpResponse, - body: EdgeBody, - params: OwnedProcessResponseParams, - }, -} - -impl HandlerOutcome { - fn status(&self) -> edgezero_core::http::StatusCode { - match self { - HandlerOutcome::Buffered(resp) => resp.status(), - HandlerOutcome::Streaming { response, .. } => response.status(), - } - } -} - -/// Returns `true` if the raw config-store value represents an enabled flag. -/// -/// Accepted values (after whitespace trimming): `"1"` or `"true"` in any ASCII case. -/// All other values, including the empty string, are treated as disabled. -fn parse_edgezero_flag(value: &str) -> bool { - let v = value.trim(); - v.eq_ignore_ascii_case("true") || v == "1" -} - -/// Parses a rollout percentage string into a value in `0..=100`. -/// -/// Accepts only integer strings in the range 0–100 (inclusive) after whitespace -/// trimming. Returns `None` for anything else: non-integer, out-of-range, -/// empty string. -fn parse_rollout_pct(value: &str) -> Option { - let n: u16 = value.trim().parse().ok()?; - if n > 100 { - return None; - } - Some(n as u8) -} -/// Maps an arbitrary string to a deterministic bucket in `0..100`. -/// -/// Uses FNV-1a (32-bit variant) to produce a uniform-enough distribution for -/// canary traffic splitting without pulling in any hash crates. The same input -/// always produces the same output across Rust versions because the algorithm -/// is defined here, not delegated to `DefaultHasher`. -fn fnv1a_bucket(key: &str) -> u8 { - const FNV_OFFSET: u32 = 2_166_136_261; - const FNV_PRIME: u32 = 16_777_619; - let mut hash = FNV_OFFSET; - for byte in key.as_bytes() { - hash ^= u32::from(*byte); - hash = hash.wrapping_mul(FNV_PRIME); - } - (hash % 100) as u8 -} - -/// Returns `true` if the given bucket should be routed to the `EdgeZero` path. -/// -/// `bucket` must be in `0..100`; `rollout_pct` in `0..=100`. -/// When `rollout_pct = 0` no bucket ever routes to `EdgeZero` (instant rollback). -/// When `rollout_pct = 100` every bucket routes to `EdgeZero` (full cutover). -fn canary_routes_to_edgezero(bucket: u8, rollout_pct: u8) -> bool { - debug_assert!(bucket < 100, "should be a value produced by fnv1a_bucket"); - debug_assert!( - rollout_pct <= 100, - "should be a value produced by read_rollout_pct" - ); - bucket < rollout_pct -} - -/// Opens the shared Fastly Config Store used by both the `EdgeZero` flag read and -/// `EdgeZero` dispatch metadata. +/// Opens the Fastly Config Store used by the `EdgeZero` dispatcher. /// /// # Errors /// @@ -142,58 +43,6 @@ fn open_trusted_server_config_store() -> Result Result { - let value = config_store - .get(EDGEZERO_ENABLED_KEY) - .map_err(|e| fastly::Error::msg(format!("failed to read edgezero_enabled: {e}")))?; - Ok(value.as_deref().is_some_and(parse_edgezero_flag)) -} - -/// Reads `edgezero_rollout_pct` from the config store. -/// -/// | Config store state | Return value | Effect | -/// |---------------------------------|--------------|----------------------------| -/// | Key absent | `100` | Full rollout (backward compat) | -/// | Key present, valid 0–100 | parsed value | Partial or full rollout | -/// | Key present, invalid | `0` | All legacy (safe default) | -/// | Key read error | `0` | All legacy (safe default) | -fn read_rollout_pct(config_store: &ConfigStoreHandle) -> u8 { - match config_store.get(EDGEZERO_ROLLOUT_PCT_KEY) { - Ok(Some(value)) => match parse_rollout_pct(&value) { - Some(pct) => pct, - None => { - log::warn!( - "invalid edgezero_rollout_pct value {:?}, defaulting to 0 (legacy path)", - value - ); - 0 - } - }, - Ok(None) => { - // Fires per-request when key is absent and edgezero_enabled=true. - // At production scale this creates one warn per request until the key is set. - // Resolution: set edgezero_rollout_pct = "0" before setting edgezero_enabled = "true". - log::warn!( - "edgezero_rollout_pct key absent, defaulting to 100 (full rollout — backward compat)" - ); - 100 - } - Err(e) => { - log::warn!("failed to read edgezero_rollout_pct: {e}, defaulting to 0 (legacy path)"); - 0 - } - } -} - fn health_response(req: &FastlyRequest) -> Option { if req.get_method() == FastlyMethod::GET && req.get_path() == "/health" { return Some(FastlyResponse::from_status(200).with_body_text_plain("ok")); @@ -205,7 +54,7 @@ fn health_response(req: &FastlyRequest) -> Option { /// Entry point for the Fastly Compute program. /// /// Uses an undecorated `main()` with `FastlyRequest::from_client()` instead of -/// `#[fastly::main]` so the legacy streaming publisher path can call +/// `#[fastly::main]` so the `EdgeZero` streaming publisher path can call /// [`fastly::Response::stream_to_client`] explicitly. fn main() { let req = FastlyRequest::from_client(); @@ -217,65 +66,32 @@ fn main() { } logging::init_logger(); + edgezero_main(req); +} - let edgezero_config_store = match open_trusted_server_config_store() { - Ok(config_store) => config_store, +/// Handles a request through the `EdgeZero` router path. +fn edgezero_main(mut req: FastlyRequest) { + let config_store = match open_trusted_server_config_store() { + Ok(cs) => cs, Err(e) => { - log::warn!("failed to open EdgeZero config store, falling back to legacy path: {e}"); - legacy_main(req); + log::error!("failed to open config store: {e}"); + FastlyResponse::from_status(fastly::http::StatusCode::INTERNAL_SERVER_ERROR) + .with_body_text_plain("Internal Server Error") + .send_to_client(); return; } }; - if !is_edgezero_enabled(&edgezero_config_store).unwrap_or_else(|e| { - log::warn!("failed to read edgezero_enabled flag, falling back to legacy path: {e}"); - false - }) { - log::debug!("routing request through legacy path (edgezero_enabled=false)"); - legacy_main(req); - return; - } - - let rollout_pct = read_rollout_pct(&edgezero_config_store); - let routing_key = match req.get_client_ip_addr() { - Some(ip) => ip.to_string(), - None => { - log::debug!( - "no client IP available, using empty routing key (deterministic bucket 61)" - ); - String::new() - } - }; - let bucket = fnv1a_bucket(&routing_key); - - if canary_routes_to_edgezero(bucket, rollout_pct) { - log::debug!( - "routing request through EdgeZero path (bucket={bucket}, rollout_pct={rollout_pct})" - ); - edgezero_main(req, edgezero_config_store); - } else { - log::debug!( - "routing request through legacy path (bucket={bucket}, rollout_pct={rollout_pct})" - ); - legacy_main(req); - } -} - -/// Handles a request through the `EdgeZero` router path. -fn edgezero_main(mut req: FastlyRequest, config_store: ConfigStoreHandle) { let app = TrustedServerApp::build_app(); - // Strip client-spoofable forwarded headers before handing off to the - // EdgeZero dispatcher, mirroring the sanitization done in legacy_main. + // Strip client-spoofable forwarded headers before dispatch. compat::sanitize_fastly_forwarded_headers(&mut req); // Capture client IP before the request is consumed by dispatch. let client_ip = req.get_client_ip_addr(); - // `run_app_with_config` and `run_app_with_logging` call `init_logger` - // internally. A second `set_logger` call panics because our custom fern - // logger is already initialised above. `dispatch_with_config_handle` skips logger - // initialisation and injects the config store directly. + // `dispatch_with_config_handle` skips logger initialisation and injects + // the config store directly (init_logger already called in main()). let mut response = match edgezero_adapter_fastly::dispatch_with_config_handle(&app, req, config_store) { Ok(response) => compat::from_fastly_response(response), @@ -289,11 +105,6 @@ fn edgezero_main(mut req: FastlyRequest, config_store: ConfigStoreHandle) { }; if !response_was_finalized_by_middleware(&mut response) { - // Apply finalize headers at the entry point so that router-level - // 405/404 responses for unregistered HTTP methods (e.g. TRACE, WebDAV - // verbs) carry TS/geo headers. Middleware-finalized responses are - // skipped here to avoid a second settings read and geo lookup on the - // normal registered-route path. match get_settings() { Ok(settings) => { apply_entry_point_finalize(&settings, client_ip, &mut response, |client_ip| { @@ -335,282 +146,6 @@ fn apply_entry_point_finalize( apply_finalize_headers(settings, geo_info.as_ref(), response); } -/// Handles a request using the original Fastly-native entry point. -/// -/// Preserves identical semantics to the pre-PR14 `main()`. Called when -/// the `edgezero_enabled` config flag is absent or `false`. -/// -/// The thin fastly↔http conversion layer (via `compat::from_fastly_request` / -/// `compat::to_fastly_response`) lives here in the adapter crate. -// TODO: delete after Phase 5 EdgeZero cutover — see issue #495 -fn legacy_main(mut req: FastlyRequest) { - let state = match build_state() { - Ok(state) => state, - Err(e) => { - log::error!("Failed to build application state: {:?}", e); - to_error_response(&e).send_to_client(); - return; - } - }; - log::debug!("Settings {:?}", state.settings); - - // Short-circuit the ja4 debug probe before finalize_response so that - // Cache-Control: no-store, private cannot be replaced by operator [response_headers]. - if req.get_method() == FastlyMethod::GET && req.get_path() == "/_ts/debug/ja4" { - if state.settings.debug.ja4_endpoint_enabled { - build_ja4_debug_response(&req).send_to_client(); - } else { - FastlyResponse::from_status(fastly::http::StatusCode::NOT_FOUND).send_to_client(); - } - return; - } - - // Strip client-spoofable forwarded headers at the edge before building - // any request-derived context or converting to the core HTTP types. - compat::sanitize_fastly_forwarded_headers(&mut req); - - let runtime_services = build_runtime_services(&req, std::sync::Arc::clone(&state.kv_store)); - let http_req = compat::from_fastly_request(req); - - let outcome = futures::executor::block_on(route_request( - &state.settings, - &state.orchestrator, - &state.registry, - &runtime_services, - http_req, - )) - .unwrap_or_else(|e| HandlerOutcome::Buffered(http_error_response(&e))); - - // Skip geo lookup for 401s: avoids exposing geo headers to unauthenticated callers. - let geo_info = if outcome.status() == edgezero_core::http::StatusCode::UNAUTHORIZED { - None - } else { - runtime_services - .geo() - .lookup(runtime_services.client_info().client_ip) - .unwrap_or_else(|e| { - log::warn!("geo lookup failed: {e}"); - None - }) - }; - - match outcome { - HandlerOutcome::Buffered(mut response) => { - finalize_response(&state.settings, geo_info.as_ref(), &mut response); - compat::to_fastly_response(response).send_to_client(); - } - HandlerOutcome::Streaming { - mut response, - body, - params, - } => { - finalize_response(&state.settings, geo_info.as_ref(), &mut response); - let fastly_resp = compat::to_fastly_response_skeleton(response); - let mut streaming_body = fastly_resp.stream_to_client(); - match stream_publisher_body( - body, - &mut streaming_body, - ¶ms, - &state.settings, - &state.registry, - ) { - Ok(()) => { - if let Err(e) = streaming_body.finish() { - log::error!("failed to finish streaming body: {e}"); - } - } - Err(e) => { - log::error!("streaming processing failed: {e:?}"); - if let Err(finish_err) = streaming_body.finish() { - log::error!("failed to finish streaming body after error: {finish_err}"); - } - } - } - } - } -} - -const FALLBACK_UNAVAILABLE: &str = "unavailable"; -const FALLBACK_NOT_SENT: &str = "not sent"; -const FALLBACK_NONE: &str = "none"; - -// TODO: remove after JA4 evaluation completes - see #645 -fn build_ja4_debug_response(req: &FastlyRequest) -> FastlyResponse { - let ja4 = req.get_tls_ja4().unwrap_or(FALLBACK_UNAVAILABLE); - let h2 = req - .get_client_h2_fingerprint() - .unwrap_or(FALLBACK_UNAVAILABLE); - let cipher = req - .get_tls_cipher_openssl_name() - .ok() - .flatten() - .unwrap_or(FALLBACK_UNAVAILABLE); - let tls_version = req - .get_tls_protocol() - .ok() - .flatten() - .unwrap_or(FALLBACK_UNAVAILABLE); - let ua = req.get_header_str("user-agent").unwrap_or(FALLBACK_NONE); - let ch_mobile = req - .get_header_str("sec-ch-ua-mobile") - .unwrap_or(FALLBACK_NOT_SENT); - let ch_platform = req - .get_header_str("sec-ch-ua-platform") - .unwrap_or(FALLBACK_NOT_SENT); - - let body = format!( - "ja4: {ja4}\n\ - h2_fp: {h2}\n\ - cipher: {cipher}\n\ - tls_version: {tls_version}\n\ - user-agent: {ua}\n\ - ch-mobile: {ch_mobile}\n\ - ch-platform: {ch_platform}\n" - ); - - FastlyResponse::from_status(fastly::http::StatusCode::OK) - .with_header(fastly::http::header::CACHE_CONTROL, "no-store, private") - .with_header( - fastly::http::header::VARY, - "User-Agent, Sec-CH-UA-Mobile, Sec-CH-UA-Platform", - ) - .with_content_type(fastly::mime::TEXT_PLAIN_UTF_8) - .with_body(body) -} - -async fn route_request( - settings: &Settings, - orchestrator: &AuctionOrchestrator, - integration_registry: &IntegrationRegistry, - runtime_services: &RuntimeServices, - req: HttpRequest, -) -> Result> { - // `get_settings()` should already have rejected invalid handler regexes. - // Keep this fallback so manually-constructed or otherwise unprepared - // settings still become an error response instead of panicking. - match enforce_basic_auth(settings, &req) { - Ok(Some(response)) => return Ok(HandlerOutcome::Buffered(response)), - Ok(None) => {} - Err(e) => return Err(e), - } - - // Get path and method for routing. - let path = req.uri().path().to_string(); - let method = req.method().clone(); - - // Match known routes and handle them. - match (method, path.as_str()) { - // Serve the tsjs library. - (Method::GET, path) if path.starts_with("/static/tsjs=") => { - handle_tsjs_dynamic(&req, integration_registry).map(HandlerOutcome::Buffered) - } - - // Discovery endpoint for trusted-server capabilities and JWKS. - (Method::GET, "/.well-known/trusted-server.json") => { - handle_trusted_server_discovery(settings, runtime_services, req) - .map(HandlerOutcome::Buffered) - } - - // Signature verification endpoint. - (Method::POST, "/verify-signature") => { - handle_verify_signature(settings, runtime_services, req).map(HandlerOutcome::Buffered) - } - - // Key rotation admin endpoints. - // Keep in sync with Settings::ADMIN_ENDPOINTS in crates/trusted-server-core/src/settings.rs - (Method::POST, "/admin/keys/rotate") => { - handle_rotate_key(settings, runtime_services, req).map(HandlerOutcome::Buffered) - } - (Method::POST, "/admin/keys/deactivate") => { - handle_deactivate_key(settings, runtime_services, req).map(HandlerOutcome::Buffered) - } - - // Unified auction endpoint. - (Method::POST, "/auction") => { - match runtime_services_for_consent_route(settings, runtime_services) { - Ok(auction_services) => { - handle_auction(settings, orchestrator, &auction_services, req) - .await - .map(HandlerOutcome::Buffered) - } - Err(e) => Err(e), - } - } - - // tsjs endpoints. - (Method::GET, "/first-party/proxy") => { - handle_first_party_proxy(settings, runtime_services, req) - .await - .map(HandlerOutcome::Buffered) - } - (Method::GET, "/first-party/click") => { - handle_first_party_click(settings, runtime_services, req) - .await - .map(HandlerOutcome::Buffered) - } - (Method::GET, "/first-party/sign") | (Method::POST, "/first-party/sign") => { - handle_first_party_proxy_sign(settings, runtime_services, req) - .await - .map(HandlerOutcome::Buffered) - } - (Method::POST, "/first-party/proxy-rebuild") => { - handle_first_party_proxy_rebuild(settings, runtime_services, req) - .await - .map(HandlerOutcome::Buffered) - } - (m, path) if integration_registry.has_route(&m, path) => integration_registry - .handle_proxy(&m, path, settings, runtime_services, req) - .await - .unwrap_or_else(|| { - Err(Report::new(TrustedServerError::BadRequest { - message: format!("Unknown integration route: {path}"), - })) - }) - .map(HandlerOutcome::Buffered), - - // No known route matched, proxy to publisher origin as fallback. - _ => { - log::info!( - "No known route matched for path: {}, proxying to publisher origin", - path - ); - - match runtime_services_for_consent_route(settings, runtime_services) { - Ok(publisher_services) => handle_publisher_request( - settings, - integration_registry, - &publisher_services, - req, - ) - .await - .and_then(resolve_publisher_response), - Err(e) => Err(e), - } - } - } -} - -fn resolve_publisher_response( - publisher_response: PublisherResponse, -) -> Result> { - match publisher_response { - PublisherResponse::Buffered(response) => Ok(HandlerOutcome::Buffered(response)), - PublisherResponse::Stream { - response, - body, - params, - } => Ok(HandlerOutcome::Streaming { - response, - body, - params, - }), - PublisherResponse::PassThrough { mut response, body } => { - *response.body_mut() = body; - Ok(HandlerOutcome::Buffered(response)) - } - } -} - pub(crate) fn resolve_publisher_response_buffered( publisher_response: PublisherResponse, settings: &Settings, @@ -639,208 +174,10 @@ pub(crate) fn resolve_publisher_response_buffered( } } -/// Applies all standard response headers: geo, version, staging, and configured headers. -/// -/// Called from every response path (including auth early-returns) so that all -/// outgoing responses carry a consistent set of Trusted Server headers. -/// -/// Header precedence (last write wins): geo headers are set first, then -/// version/staging, then operator-configured `settings.response_headers`. -/// This means operators can intentionally override any managed header. -fn finalize_response(settings: &Settings, geo_info: Option<&GeoInfo>, response: &mut HttpResponse) { - apply_finalize_headers(settings, geo_info, response); -} - -fn http_error_response(report: &Report) -> HttpResponse { - let root_error = report.current_context(); - log::error!("Error occurred: {:?}", report); - - let mut response = - HttpResponse::new(EdgeBody::from(format!("{}\n", root_error.user_message()))); - *response.status_mut() = root_error.status_code(); - response.headers_mut().insert( - header::CONTENT_TYPE, - HeaderValue::from_static("text/plain; charset=utf-8"), - ); - response -} - #[cfg(test)] mod tests { use super::*; use edgezero_core::http::response_builder; - use fastly::mime; - - #[test] - fn parses_true_flag_values() { - assert!(parse_edgezero_flag("true"), "should parse 'true'"); - assert!(parse_edgezero_flag("1"), "should parse '1'"); - assert!(parse_edgezero_flag(" true "), "should trim whitespace"); - assert!( - parse_edgezero_flag(" 1 "), - "should trim whitespace around '1'" - ); - assert!(parse_edgezero_flag("TRUE"), "should parse uppercase 'TRUE'"); - assert!( - parse_edgezero_flag("True"), - "should parse mixed-case 'True'" - ); - } - - #[test] - fn rejects_non_true_flag_values() { - assert!(!parse_edgezero_flag("false"), "should not parse 'false'"); - assert!(!parse_edgezero_flag(""), "should not parse empty string"); - assert!( - !parse_edgezero_flag(" "), - "should not parse whitespace-only" - ); - assert!(!parse_edgezero_flag("yes"), "should not parse 'yes'"); - } - - // --------------------------------------------------------------------------- - // parse_rollout_pct - // --------------------------------------------------------------------------- - - #[test] - fn parses_valid_rollout_percentages() { - assert_eq!(parse_rollout_pct("0"), Some(0), "should parse '0'"); - assert_eq!(parse_rollout_pct("1"), Some(1), "should parse '1'"); - assert_eq!(parse_rollout_pct("50"), Some(50), "should parse '50'"); - assert_eq!(parse_rollout_pct("100"), Some(100), "should parse '100'"); - assert_eq!( - parse_rollout_pct(" 50 "), - Some(50), - "should trim whitespace" - ); - } - - #[test] - fn rejects_invalid_rollout_percentages() { - assert_eq!( - parse_rollout_pct("101"), - None, - "should reject values above 100" - ); - assert_eq!(parse_rollout_pct(""), None, "should reject empty string"); - assert_eq!(parse_rollout_pct("abc"), None, "should reject non-integer"); - assert_eq!( - parse_rollout_pct("-1"), - None, - "should reject negative value" - ); - assert_eq!( - parse_rollout_pct("1.5"), - None, - "should reject decimal value" - ); - } - - // --------------------------------------------------------------------------- - // fnv1a_bucket - // --------------------------------------------------------------------------- - - #[test] - fn bucket_is_in_range_0_to_99() { - for key in &["1.2.3.4", "255.255.255.255", "::1", "", "unknown"] { - let b = fnv1a_bucket(key); - assert!(b < 100, "bucket must be 0..100 for key {key:?}, got {b}"); - } - } - - #[test] - fn bucket_is_deterministic() { - let key = "192.168.1.1"; - assert_eq!( - fnv1a_bucket(key), - fnv1a_bucket(key), - "same key must produce the same bucket" - ); - } - - #[test] - fn bucket_matches_known_fnv1a_vector() { - // FNV-1a 32-bit: XOR-then-multiply. Verified against reference implementation. - assert_eq!( - fnv1a_bucket("1.2.3.4"), - 85, - "should match pinned FNV-1a vector" - ); - assert_eq!( - fnv1a_bucket(""), - 61, - "should match pinned FNV-1a vector for empty key" - ); - } - - #[test] - fn bucket_distributes_across_range() { - // Smoke-test that fnv1a_bucket produces a spread of values (not a constant). - // 256 distinct IP-like keys must produce at least 50 unique buckets. - let buckets: std::collections::HashSet = (0u16..=255) - .map(|i| fnv1a_bucket(&format!("10.0.0.{i}"))) - .collect(); - assert!( - buckets.len() > 50, - "fnv1a_bucket should distribute across buckets; got only {} unique values in 256 keys", - buckets.len() - ); - } - - #[test] - fn empty_key_bucket_is_valid() { - let b = fnv1a_bucket(""); - assert!( - b < 100, - "empty key must still produce a valid bucket, got {b}" - ); - } - - // --------------------------------------------------------------------------- - // canary_routes_to_edgezero - // --------------------------------------------------------------------------- - - #[test] - fn rollout_zero_routes_all_to_legacy() { - for bucket in 0u8..100 { - assert!( - !canary_routes_to_edgezero(bucket, 0), - "pct=0 should route all to legacy, bucket={bucket}" - ); - } - } - - #[test] - fn rollout_hundred_routes_all_to_edgezero() { - for bucket in 0u8..100 { - assert!( - canary_routes_to_edgezero(bucket, 100), - "pct=100 should route all to EdgeZero, bucket={bucket}" - ); - } - } - - #[test] - fn rollout_fifty_routes_exactly_half_of_bucket_space() { - let edgezero_count = (0u8..100) - .filter(|&b| canary_routes_to_edgezero(b, 50)) - .count(); - assert_eq!( - edgezero_count, 50, - "pct=50 should route exactly 50 out of 100 buckets to EdgeZero" - ); - } - - #[test] - fn rollout_one_routes_exactly_one_bucket() { - let edgezero_count = (0u8..100) - .filter(|&b| canary_routes_to_edgezero(b, 1)) - .count(); - assert_eq!( - edgezero_count, 1, - "pct=1 should route exactly 1 out of 100 buckets to EdgeZero" - ); - } #[test] fn health_response_short_circuits_get_health() { @@ -909,58 +246,4 @@ mod tests { "401 responses should still carry geo-unavailable headers" ); } - - #[test] - fn ja4_debug_response_uses_plain_text_and_fallback_values() { - let req = FastlyRequest::get("https://example.com/_ts/debug/ja4"); - - let mut response = build_ja4_debug_response(&req); - - assert_eq!( - response.get_status(), - fastly::http::StatusCode::OK, - "should return 200 OK" - ); - assert_eq!( - response.get_content_type(), - Some(mime::TEXT_PLAIN_UTF_8), - "should return plain text content" - ); - assert_eq!( - response.get_header_str(fastly::http::header::CACHE_CONTROL), - Some("no-store, private"), - "should disable caching for the debug response" - ); - - let body = response.take_body_str(); - - assert!( - body.contains("ja4: unavailable"), - "should include JA4 fallback" - ); - assert!( - body.contains("h2_fp: unavailable"), - "should include H2 fingerprint fallback" - ); - assert!( - body.contains("cipher: unavailable"), - "should include cipher fallback" - ); - assert!( - body.contains("tls_version: unavailable"), - "should include TLS version fallback" - ); - assert!( - body.contains("user-agent: none"), - "should include user-agent fallback" - ); - assert!( - body.contains("ch-mobile: not sent"), - "should include sec-ch-ua-mobile fallback" - ); - assert!( - body.contains("ch-platform: not sent"), - "should include sec-ch-ua-platform fallback" - ); - } } diff --git a/crates/trusted-server-adapter-fastly/src/middleware.rs b/crates/trusted-server-adapter-fastly/src/middleware.rs index bf94873a..3f152b61 100644 --- a/crates/trusted-server-adapter-fastly/src/middleware.rs +++ b/crates/trusted-server-adapter-fastly/src/middleware.rs @@ -1,7 +1,7 @@ -//! Middleware implementations for the dual-path entry point. +//! Middleware implementations for the `EdgeZero` entry point. //! //! Provides two middleware types that mirror the finalization and auth logic -//! from the legacy [`crate::finalize_response`] and [`crate::route_request`]: +//! used in the legacy entry point: //! //! - [`FinalizeResponseMiddleware`] — geo lookup and standard TS header injection //! - [`AuthMiddleware`] — basic-auth enforcement via [`enforce_basic_auth`] @@ -147,8 +147,7 @@ impl Middleware for AuthMiddleware { /// Applies all standard Trusted Server response headers to the given response. /// -/// Mirrors [`crate::finalize_response`] exactly, operating on [`Response`] from -/// `edgezero_core::http` instead of `HttpResponse`. +/// Operates on [`Response`] from `edgezero_core::http`. /// /// Header write order (last write wins): /// 1. Geo headers (`x-geo-*`) — or `X-Geo-Info-Available: false` when absent diff --git a/crates/trusted-server-adapter-fastly/src/platform.rs b/crates/trusted-server-adapter-fastly/src/platform.rs index 78a0de40..84a24807 100644 --- a/crates/trusted-server-adapter-fastly/src/platform.rs +++ b/crates/trusted-server-adapter-fastly/src/platform.rs @@ -1,9 +1,5 @@ //! Fastly-backed implementations of the platform traits defined in //! `trusted-server-core::platform`. -//! -//! This module also provides [`build_runtime_services`], a free function that -//! constructs a [`RuntimeServices`] instance once at the entry point from the -//! incoming Fastly request. use std::net::IpAddr; use std::sync::Arc; @@ -12,15 +8,14 @@ use edgezero_adapter_fastly::key_value_store::FastlyKvStore; use edgezero_core::key_value_store::KvError; use error_stack::{Report, ResultExt}; use fastly::geo::{geo_lookup, Geo}; -use fastly::{ConfigStore, Request, SecretStore}; +use fastly::{ConfigStore, SecretStore}; use crate::backend::BackendConfig; pub(crate) use trusted_server_core::platform::UnavailableKvStore; use trusted_server_core::platform::{ - ClientInfo, GeoInfo, PlatformBackend, PlatformBackendSpec, PlatformConfigStore, PlatformError, - PlatformGeo, PlatformHttpClient, PlatformHttpRequest, PlatformKvStore, PlatformPendingRequest, - PlatformResponse, PlatformSecretStore, PlatformSelectResult, RuntimeServices, StoreId, - StoreName, + GeoInfo, PlatformBackend, PlatformBackendSpec, PlatformConfigStore, PlatformError, PlatformGeo, + PlatformHttpClient, PlatformHttpRequest, PlatformKvStore, PlatformPendingRequest, + PlatformResponse, PlatformSecretStore, PlatformSelectResult, StoreId, StoreName, }; // --------------------------------------------------------------------------- @@ -348,42 +343,9 @@ impl PlatformGeo for FastlyPlatformGeo { } // --------------------------------------------------------------------------- -// Entry-point helper +// KV store // --------------------------------------------------------------------------- -/// Construct a [`RuntimeServices`] instance from the incoming Fastly request. -/// -/// Call this once at the entry point before dispatching to handlers. -/// `client_info` is populated from TLS and IP metadata available on the -/// request; geo lookup is deferred to handler time via -/// `services.geo().lookup(services.client_info().client_ip)`. -/// -/// `kv_store` is an [`Arc`] opened by the caller for -/// the primary KV store. Use [`open_kv_store`] to construct it. -#[must_use] -pub fn build_runtime_services( - req: &Request, - kv_store: Arc, -) -> RuntimeServices { - RuntimeServices::builder() - .config_store(Arc::new(FastlyPlatformConfigStore)) - .secret_store(Arc::new(FastlyPlatformSecretStore)) - .kv_store(kv_store) - .backend(Arc::new(FastlyPlatformBackend)) - .http_client(Arc::new(FastlyPlatformHttpClient)) - .geo(Arc::new(FastlyPlatformGeo)) - .client_info(ClientInfo { - client_ip: req.get_client_ip_addr(), - tls_protocol: req.get_tls_protocol().ok().flatten().map(str::to_string), - tls_cipher: req - .get_tls_cipher_openssl_name() - .ok() - .flatten() - .map(str::to_string), - }) - .build() -} - /// Open a named KV store as a [`PlatformKvStore`] implementation. /// /// # Errors @@ -401,19 +363,13 @@ pub fn open_kv_store(store_name: &str) -> Result, KvErr #[cfg(test)] mod tests { use std::io; - use std::sync::Arc; use std::time::Duration; use edgezero_core::body::Body; use edgezero_core::http::request_builder; - use edgezero_core::key_value_store::NoopKvStore; use super::*; - fn noop_kv_store() -> Arc { - Arc::new(NoopKvStore) - } - // --- FastlyPlatformBackend::predict_name -------------------------------- #[test] @@ -495,36 +451,6 @@ mod tests { ); } - // --- ClientInfo extraction ---------------------------------------------- - - #[test] - fn build_runtime_services_client_info_is_none_without_tls() { - let req = Request::get("https://example.com/"); - let services = build_runtime_services(&req, noop_kv_store()); - - assert!( - services.client_info().tls_protocol.is_none(), - "should have no tls_protocol on plain test request" - ); - assert!( - services.client_info().tls_cipher.is_none(), - "should have no tls_cipher on plain test request" - ); - } - - #[test] - fn build_runtime_services_returns_cloneable_services() { - let req = Request::get("https://example.com/"); - let services = build_runtime_services(&req, noop_kv_store()); - let cloned = services.clone(); - - assert_eq!( - services.client_info().client_ip, - cloned.client_info().client_ip, - "should preserve client_ip through clone" - ); - } - // --- FastlyPlatformHttpClient ------------------------------------------- #[test] diff --git a/crates/trusted-server-adapter-fastly/src/route_tests.rs b/crates/trusted-server-adapter-fastly/src/route_tests.rs deleted file mode 100644 index 66498c6b..00000000 --- a/crates/trusted-server-adapter-fastly/src/route_tests.rs +++ /dev/null @@ -1,265 +0,0 @@ -use std::net::IpAddr; -use std::sync::Arc; - -use crate::compat; -use edgezero_core::key_value_store::NoopKvStore; -use error_stack::Report; -use fastly::http::StatusCode; -use fastly::Request; -use trusted_server_core::auction::build_orchestrator; -use trusted_server_core::error::{IntoHttpResponse, TrustedServerError}; -use trusted_server_core::integrations::IntegrationRegistry; -use trusted_server_core::platform::{ - ClientInfo, GeoInfo, PlatformBackend, PlatformBackendSpec, PlatformConfigStore, PlatformError, - PlatformGeo, PlatformHttpClient, PlatformHttpRequest, PlatformKvStore, PlatformPendingRequest, - PlatformResponse, PlatformSecretStore, PlatformSelectResult, RuntimeServices, StoreId, - StoreName, -}; -use trusted_server_core::request_signing::JWKS_CONFIG_STORE_NAME; -use trusted_server_core::settings::Settings; - -use super::{route_request, HandlerOutcome}; - -fn outcome_status(result: &Result>) -> StatusCode { - match result { - Ok(outcome) => outcome.status(), - Err(e) => e.current_context().status_code(), - } -} - -struct StubJwksConfigStore; - -impl PlatformConfigStore for StubJwksConfigStore { - fn get(&self, _store_name: &StoreName, key: &str) -> Result> { - match key { - "active-kids" => Ok("test-kid-1".to_string()), - "test-kid-1" => Ok( - r#"{"kty":"OKP","crv":"Ed25519","x":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","kid":"test-kid-1","alg":"EdDSA"}"# - .to_string(), - ), - _ => Err(Report::new(PlatformError::ConfigStore)), - } - } - - fn put( - &self, - _store_id: &StoreId, - _key: &str, - _value: &str, - ) -> Result<(), Report> { - Err(Report::new(PlatformError::Unsupported)) - } - - fn delete(&self, _store_id: &StoreId, _key: &str) -> Result<(), Report> { - Err(Report::new(PlatformError::Unsupported)) - } -} - -struct NoopSecretStore; - -impl PlatformSecretStore for NoopSecretStore { - fn get_bytes( - &self, - _store_name: &StoreName, - _key: &str, - ) -> Result, Report> { - Err(Report::new(PlatformError::Unsupported)) - } - - fn create( - &self, - _store_id: &StoreId, - _name: &str, - _value: &str, - ) -> Result<(), Report> { - Err(Report::new(PlatformError::Unsupported)) - } - - fn delete(&self, _store_id: &StoreId, _name: &str) -> Result<(), Report> { - Err(Report::new(PlatformError::Unsupported)) - } -} - -struct NoopBackend; - -impl PlatformBackend for NoopBackend { - fn predict_name(&self, _spec: &PlatformBackendSpec) -> Result> { - Err(Report::new(PlatformError::Unsupported)) - } - - fn ensure(&self, _spec: &PlatformBackendSpec) -> Result> { - Err(Report::new(PlatformError::Unsupported)) - } -} - -struct NoopHttpClient; - -#[async_trait::async_trait(?Send)] -impl PlatformHttpClient for NoopHttpClient { - async fn send( - &self, - _request: PlatformHttpRequest, - ) -> Result> { - Err(Report::new(PlatformError::Unsupported)) - } - - async fn send_async( - &self, - _request: PlatformHttpRequest, - ) -> Result> { - Err(Report::new(PlatformError::Unsupported)) - } - - async fn select( - &self, - _pending_requests: Vec, - ) -> Result> { - Err(Report::new(PlatformError::Unsupported)) - } -} - -struct NoopGeo; - -impl PlatformGeo for NoopGeo { - fn lookup(&self, _client_ip: Option) -> Result, Report> { - Ok(None) - } -} - -fn create_test_settings() -> Settings { - let settings = Settings::from_toml( - r#" - [[handlers]] - path = "^/admin" - username = "admin" - password = "admin-pass" - - [publisher] - domain = "test-publisher.com" - cookie_domain = ".test-publisher.com" - origin_url = "https://origin.test-publisher.com" - proxy_secret = "unit-test-proxy-secret" - - [edge_cookie] - secret_key = "test-secret-key" - - [request_signing] - enabled = false - config_store_id = "test-config-store-id" - secret_store_id = "test-secret-store-id" - - [consent] - consent_store = "missing-consent-store" - - [integrations.prebid] - enabled = true - server_url = "https://test-prebid.com/openrtb2/auction" - - [auction] - enabled = true - providers = ["prebid"] - timeout_ms = 2000 - "#, - ) - .expect("should parse adapter route test settings"); - - assert_eq!( - JWKS_CONFIG_STORE_NAME, "jwks_store", - "should keep the stub discovery store aligned with the production constant" - ); - - settings -} - -fn test_runtime_services(req: &Request) -> RuntimeServices { - RuntimeServices::builder() - .config_store(Arc::new(StubJwksConfigStore)) - .secret_store(Arc::new(NoopSecretStore)) - .kv_store(Arc::new(NoopKvStore) as Arc) - .backend(Arc::new(NoopBackend)) - .http_client(Arc::new(NoopHttpClient)) - .geo(Arc::new(NoopGeo)) - .client_info(ClientInfo { - client_ip: req.get_client_ip_addr(), - tls_protocol: req.get_tls_protocol().ok().flatten().map(str::to_string), - tls_cipher: req - .get_tls_cipher_openssl_name() - .ok() - .flatten() - .map(str::to_string), - }) - .build() -} - -#[test] -fn configured_missing_consent_store_only_breaks_consent_routes() { - let settings = create_test_settings(); - let orchestrator = build_orchestrator(&settings).expect("should build auction orchestrator"); - let integration_registry = - IntegrationRegistry::new(&settings).expect("should create integration registry"); - - let discovery_fastly_req = Request::get("https://test.com/.well-known/trusted-server.json"); - let discovery_services = test_runtime_services(&discovery_fastly_req); - let discovery_resp = futures::executor::block_on(route_request( - &settings, - &orchestrator, - &integration_registry, - &discovery_services, - compat::from_fastly_request(discovery_fastly_req), - )) - .expect("should route discovery request"); - assert_eq!( - discovery_resp.status(), - StatusCode::OK, - "should keep discovery available when the consent store is unavailable" - ); - - let admin_fastly_req = Request::post("https://test.com/admin/keys/rotate"); - let admin_services = test_runtime_services(&admin_fastly_req); - let admin_resp = futures::executor::block_on(route_request( - &settings, - &orchestrator, - &integration_registry, - &admin_services, - compat::from_fastly_request(admin_fastly_req), - )) - .expect("should route admin request"); - assert_eq!( - admin_resp.status(), - StatusCode::UNAUTHORIZED, - "should keep admin auth behavior unchanged when the consent store is unavailable" - ); - - let auction_fastly_req = - Request::post("https://test.com/auction").with_body(r#"{"adUnits":[]}"#); - let auction_services = test_runtime_services(&auction_fastly_req); - let auction_result = futures::executor::block_on(route_request( - &settings, - &orchestrator, - &integration_registry, - &auction_services, - compat::from_fastly_request(auction_fastly_req), - )); - let auction_status = outcome_status(&auction_result); - assert_eq!( - auction_status, - StatusCode::SERVICE_UNAVAILABLE, - "should fail auction requests when consent persistence is configured but unavailable" - ); - - let publisher_fastly_req = Request::get("https://test.com/articles/example"); - let publisher_services = test_runtime_services(&publisher_fastly_req); - let publisher_result = futures::executor::block_on(route_request( - &settings, - &orchestrator, - &integration_registry, - &publisher_services, - compat::from_fastly_request(publisher_fastly_req), - )); - let publisher_status = outcome_status(&publisher_result); - assert_eq!( - publisher_status, - StatusCode::SERVICE_UNAVAILABLE, - "should scope consent store failures to the consent-dependent routes" - ); -} diff --git a/docs/superpowers/plans/2026-05-27-pr20-legacy-cleanup.md b/docs/superpowers/plans/2026-05-27-pr20-legacy-cleanup.md new file mode 100644 index 00000000..38584a3d --- /dev/null +++ b/docs/superpowers/plans/2026-05-27-pr20-legacy-cleanup.md @@ -0,0 +1,931 @@ +# PR20 — Legacy Entry Point Cleanup Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Delete `legacy_main()`, the canary routing machinery (`edgezero_enabled`/`edgezero_rollout_pct` flag plumbing), and all code that only existed to support the legacy path — completing issue #501. + +**Architecture:** All traffic now flows through `edgezero_main()`. The entry point `main()` becomes a thin trampoline: fast-path health check → logging init → call `edgezero_main()`. The canary config-store flag read is gone; the config store is opened once inside `edgezero_main()` (required for `dispatch_with_config_handle`). Dead test coverage for `route_request()` and `HandlerOutcome` is removed; equivalent EdgeZero-path coverage already lives in `app.rs`. + +**Tech Stack:** Rust / Fastly Compute (`wasm32-wasip1`), `edgezero-adapter-fastly`, standard `http` crate types. + +--- + +## File Map + +| File | Action | What changes | +| --------------------------------------------------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `crates/trusted-server-adapter-fastly/src/main.rs` | Modify (major) | Delete `legacy_main`, `route_request`, `HandlerOutcome`, all canary flag functions/constants/tests, `build_ja4_debug_response`, `finalize_response`, `http_error_response`, `resolve_publisher_response`, FALLBACK\_\* constants; simplify `main()` and `edgezero_main()`; remove `mod error;`; clean up imports | +| `crates/trusted-server-adapter-fastly/src/route_tests.rs` | **Delete** | Entire file — all tests reference deleted `route_request`/`HandlerOutcome`; EdgeZero dispatch coverage lives in `app.rs` | +| `crates/trusted-server-adapter-fastly/src/error.rs` | **Delete** | Entire file — `to_error_response()` is the only export; it is only called from `legacy_main()` | +| `crates/trusted-server-adapter-fastly/src/compat.rs` | Modify | Delete `from_fastly_request()`, its private helper `build_http_request()`, and `to_fastly_response_skeleton()` — all legacy-only. Keep `from_fastly_response()`, `to_fastly_response()`, `sanitize_fastly_forwarded_headers()` and their tests. Update module doc comment. | +| `crates/trusted-server-adapter-fastly/src/platform.rs` | Modify | Delete `build_runtime_services()`, its `noop_kv_store()` test helper, and its two unit tests — all legacy-only. Update module doc comment. | +| `crates/trusted-server-adapter-fastly/src/middleware.rs` | Modify (doc only) | Remove stale intra-doc links to deleted symbols `finalize_response` and `route_request` | +| `crates/trusted-server-adapter-fastly/src/app.rs` | Modify (doc only) | Remove stale reference to `crate::http_error_response` in `http_error()` doc comment | +| `fastly.toml` | Modify | Remove `edgezero_enabled` and `edgezero_rollout_pct` keys from `[local_server.config_stores.trusted_server_config.contents]` | + +**Not touched:** `backend.rs`, `logging.rs`, `management_api.rs`. All other adapter crates (`axum`, `cloudflare`, `spin`) are untouched. + +--- + +## What to delete vs keep in `main.rs` + +### Delete — complete functions/types + +| Symbol | Lines (approx) | Reason | +| ------------------------------------------ | -------------- | ------------------------------------------------------------- | +| `const EDGEZERO_ENABLED_KEY` | 55 | Flag removed | +| `const EDGEZERO_ROLLOUT_PCT_KEY` | 56 | Flag removed | +| `enum HandlerOutcome` + `impl` | 62-78 | Legacy-path-only type | +| `fn parse_edgezero_flag()` | 84-87 | Flag removed | +| `fn parse_rollout_pct()` | 94-100 | Flag removed | +| `fn fnv1a_bucket()` | 108-117 | Flag removed | +| `fn canary_routes_to_edgezero()` | 124-131 | Flag removed | +| `fn is_edgezero_enabled()` | 154-159 | Flag removed | +| `fn read_rollout_pct()` | 169-195 | Flag removed | +| `const FALLBACK_UNAVAILABLE/NOT_SENT/NONE` | 433-435 | JA4 debug only | +| `fn build_ja4_debug_response()` | 438-479 | `// TODO: remove after JA4 evaluation` — was legacy-path only | +| `fn legacy_main()` | 346-431 | THE main deliverable of this PR | +| `async fn route_request()` | 481-591 | Legacy-path-only dispatcher | +| `fn resolve_publisher_response()` | 593-612 | Called only by `route_request()` | +| `fn finalize_response()` | 650-652 | Thin wrapper used only by `legacy_main()` | +| `fn http_error_response()` | 654-666 | Legacy-path-only; EdgeZero path uses `app::http_error()` | + +### Delete — `mod` declaration and import + +| Symbol | Where | Reason | +| -------------------------------------- | ---------------- | --------------------------- | +| `mod error;` | main.rs ~line 41 | `error.rs` is being deleted | +| `use crate::error::to_error_response;` | main.rs ~line 50 | `error.rs` is being deleted | + +### Delete — tests in `mod tests` + +| Test name | Reason | +| -------------------------------------------------------- | ----------------------------------- | +| `parses_true_flag_values` | `parse_edgezero_flag` deleted | +| `rejects_non_true_flag_values` | `parse_edgezero_flag` deleted | +| `parses_valid_rollout_percentages` | `parse_rollout_pct` deleted | +| `rejects_invalid_rollout_percentages` | `parse_rollout_pct` deleted | +| `bucket_is_in_range_0_to_99` | `fnv1a_bucket` deleted | +| `bucket_is_deterministic` | `fnv1a_bucket` deleted | +| `bucket_matches_known_fnv1a_vector` | `fnv1a_bucket` deleted | +| `bucket_distributes_across_range` | `fnv1a_bucket` deleted | +| `empty_key_bucket_is_valid` | `fnv1a_bucket` deleted | +| `rollout_zero_routes_all_to_legacy` | `canary_routes_to_edgezero` deleted | +| `rollout_hundred_routes_all_to_edgezero` | `canary_routes_to_edgezero` deleted | +| `rollout_fifty_routes_exactly_half_of_bucket_space` | `canary_routes_to_edgezero` deleted | +| `rollout_one_routes_exactly_one_bucket` | `canary_routes_to_edgezero` deleted | +| `ja4_debug_response_uses_plain_text_and_fallback_values` | `build_ja4_debug_response` deleted | + +### Keep — functions (unchanged or slightly updated) + +| Symbol | Notes | +| ------------------------------------------------------------- | ------------------------------------------------------------------------ | +| `const TRUSTED_SERVER_CONFIG_STORE` | Still used by `open_trusted_server_config_store()` | +| `fn open_trusted_server_config_store()` | Kept; called inside simplified `edgezero_main()`. Update doc comment. | +| `fn health_response()` | Kept; fast-path health probe in `main()` | +| `fn edgezero_main()` | Kept; signature changes: no `config_store` param; opens store internally | +| `fn response_was_finalized_by_middleware()` | Kept; used by `edgezero_main()` | +| `fn apply_entry_point_finalize()` | Kept; used by `edgezero_main()` | +| `pub(crate) fn resolve_publisher_response_buffered()` | Kept; called by `app.rs::dispatch_fallback()` | +| Tests: `health_response_*` | Kept | +| Tests: `response_was_finalized_by_middleware_strips_sentinel` | Kept | +| Tests: `entry_point_finalize_skips_geo_lookup_for_401` | Kept | + +### Imports to remove from `main.rs` + +After all deletions, these become unused (let the compiler confirm): + +```rust +// Delete entirely: +use trusted_server_core::auction::endpoints::handle_auction; +use trusted_server_core::auction::AuctionOrchestrator; +use trusted_server_core::auth::enforce_basic_auth; +use trusted_server_core::platform::RuntimeServices; +use trusted_server_core::request_signing::{ + handle_deactivate_key, handle_rotate_key, handle_trusted_server_discovery, + handle_verify_signature, +}; + +// Trim these lines (keep only what's shown): + +// Was: use crate::app::{build_state, runtime_services_for_consent_route, TrustedServerApp}; +// build_state: only called from legacy_main; runtime_services_for_consent_route: only called from +// route_request (deleted). TrustedServerApp is still needed by edgezero_main. +use crate::app::TrustedServerApp; + +// Was: use crate::platform::{build_runtime_services, FastlyPlatformGeo}; +use crate::platform::FastlyPlatformGeo; + +// Was: use edgezero_core::http::{header, HeaderValue, Method, Request as HttpRequest, Response as HttpResponse}; +// Method and Request: only used by route_request (deleted). +use edgezero_core::http::{header, HeaderValue, Response as HttpResponse}; + +// Was: use trusted_server_core::error::{IntoHttpResponse, TrustedServerError}; +// IntoHttpResponse: only in route_tests (deleted). TrustedServerError: used by +// resolve_publisher_response_buffered. Keep TrustedServerError only. +use trusted_server_core::error::TrustedServerError; + +// Was: use trusted_server_core::proxy::{handle_first_party_click, ...}; +// DELETE ENTIRE LINE — no proxy handlers remain in main.rs + +// Was: use trusted_server_core::publisher::{handle_publisher_request, handle_tsjs_dynamic, +// stream_publisher_body, OwnedProcessResponseParams, PublisherResponse}; +// handle_publisher_request, handle_tsjs_dynamic: only in route_request (deleted). +use trusted_server_core::publisher::{stream_publisher_body, OwnedProcessResponseParams, PublisherResponse}; +``` + +Also in `mod tests`: remove `use fastly::mime;` (only for ja4 test, which is deleted). + +`std::net::IpAddr`, `std::sync::Arc`, `edgezero_core::body::Body as EdgeBody`, `fastly::http::Method as FastlyMethod`, `fastly::{Request as FastlyRequest, Response as FastlyResponse}`, `edgezero_adapter_fastly::FastlyConfigStore`, `edgezero_core::config_store::ConfigStoreHandle`, `error_stack::Report`, `edgezero_core::app::Hooks as _` all remain. + +### Simplified `main()` — target state + +```rust +/// Entry point for the Fastly Compute program. +/// +/// Uses an undecorated `main()` with `FastlyRequest::from_client()` instead of +/// `#[fastly::main]` so the EdgeZero streaming publisher path can call +/// [`fastly::Response::stream_to_client`] explicitly. +fn main() { + let req = FastlyRequest::from_client(); + + // Health probe bypasses logging, settings, and app construction as a cheap liveness signal. + if let Some(response) = health_response(&req) { + response.send_to_client(); + return; + } + + logging::init_logger(); + edgezero_main(req); +} +``` + +### Simplified `edgezero_main()` — target state + +```rust +/// Handles a request through the EdgeZero router path. +fn edgezero_main(mut req: FastlyRequest) { + let config_store = match open_trusted_server_config_store() { + Ok(cs) => cs, + Err(e) => { + log::error!("failed to open config store: {e}"); + FastlyResponse::from_status(fastly::http::StatusCode::INTERNAL_SERVER_ERROR) + .with_body_text_plain("Internal Server Error") + .send_to_client(); + return; + } + }; + + let app = TrustedServerApp::build_app(); + + // Strip client-spoofable forwarded headers before dispatch. + compat::sanitize_fastly_forwarded_headers(&mut req); + + // Capture client IP before the request is consumed by dispatch. + let client_ip = req.get_client_ip_addr(); + + // `dispatch_with_config_handle` skips logger initialisation and injects + // the config store directly (init_logger already called in main()). + let mut response = + match edgezero_adapter_fastly::dispatch_with_config_handle(&app, req, config_store) { + Ok(response) => compat::from_fastly_response(response), + Err(e) => { + log::error!("EdgeZero dispatch failed: {e}"); + FastlyResponse::from_status(fastly::http::StatusCode::INTERNAL_SERVER_ERROR) + .with_body_text_plain("Internal Server Error") + .send_to_client(); + return; + } + }; + + if !response_was_finalized_by_middleware(&mut response) { + match get_settings() { + Ok(settings) => { + apply_entry_point_finalize(&settings, client_ip, &mut response, |client_ip| { + FastlyPlatformGeo.lookup(client_ip).unwrap_or_else(|e| { + log::warn!("entry-point geo lookup failed: {e}"); + None + }) + }) + } + Err(e) => { + log::warn!("entry-point finalize skipped: failed to reload settings: {e:?}"); + } + } + } + + compat::to_fastly_response(response).send_to_client(); +} +``` + +### Updated `open_trusted_server_config_store()` doc comment + +```rust +/// Opens the Fastly Config Store used by the EdgeZero dispatcher. +/// +/// # Errors +/// +/// Returns [`fastly::Error`] if the config store cannot be opened. +fn open_trusted_server_config_store() -> Result { +``` + +--- + +## What to delete vs keep in `compat.rs` + +### Delete + +| Symbol | Lines | Reason | +| --------------------------------------------- | ----- | --------------------------------------------------- | +| `fn build_http_request()` | 11-28 | Private helper only used by `from_fastly_request()` | +| `pub(crate) fn from_fastly_request()` | 35-38 | Only called from `legacy_main()` | +| `pub(crate) fn to_fastly_response_skeleton()` | 78-85 | Only called from `legacy_main()` streaming path | + +### Keep + +- `pub(crate) fn from_fastly_response()` — used by `edgezero_main()` +- `pub(crate) fn to_fastly_response()` — used by `edgezero_main()` +- `pub(crate) fn sanitize_fastly_forwarded_headers()` — used by `edgezero_main()` +- Both test functions in `mod tests` — test shared functions above + +### Update module doc comment + +Replace the current: + +```rust +//! Contains only the functions used by the legacy `main()` entry point. +//! Relocated from `trusted-server-core` as part of removing all `fastly` crate +//! imports from the core library. +``` + +With: + +```rust +//! Compatibility bridge between `fastly` SDK types and `http` crate types. +``` + +--- + +## What to delete in `platform.rs` + +- `pub fn build_runtime_services(...)` — only caller was `legacy_main()` +- `fn noop_kv_store()` test helper — only used by the two `build_runtime_services` tests +- `fn build_runtime_services_client_info_is_none_without_tls()` test +- `fn build_runtime_services_returns_cloneable_services()` test +- Update module doc: remove the sentence "This module also provides `build_runtime_services`, a free function that..." + +--- + +## What to update in `middleware.rs` + +Two stale intra-doc links after `finalize_response` and `route_request` are deleted: + +**Line ~4 (module doc):** +Remove: `from the legacy [\`crate::finalize_response\`] and [\`crate::route_request\`]:` + +**Line ~150 (fn-level doc on `apply_finalize_headers` or equivalent):** +Remove: `Mirrors [\`crate::finalize_response\`] exactly` + +Use `grep -n "finalize_response\|route_request" middleware.rs` to find exact lines before editing. + +--- + +## What to remove from `fastly.toml` + +The `[local_server.config_stores.trusted_server_config.contents]` block currently has two keys the app no longer reads. Remove them and their comments, leaving the store entry intact (the store itself is still needed for EdgeZero dispatch): + +```toml +# Before (remove these lines): + # "true" / "1" (case-insensitive) enable the EdgeZero path. Missing, + # unreadable, or any other value falls back to the legacy entry point. + # Keep "false" until EdgeZero reaches full functional parity with legacy. + edgezero_enabled = "false" + # Integer 0-100. Effective only when edgezero_enabled = "true". + # 0 -> all traffic to legacy (instant rollback — no deploy needed) + # 1-99 -> canary: clients whose fnv1a_bucket(client_ip) < this value go EdgeZero + # 100 -> all traffic to EdgeZero (full cutover) + # Key absent when edgezero_enabled = "true" is treated as 100 (full rollout). + # IMPORTANT: Set this to "0" in production BEFORE setting edgezero_enabled = "true". + edgezero_rollout_pct = "0" +``` + +The resulting `[local_server.config_stores.trusted_server_config.contents]` block should be empty `{}` — the store entry stays because `open_trusted_server_config_store()` still opens it for the EdgeZero dispatcher. + +--- + +## Tasks + +> **IMPORTANT about tasks 2-4:** Tasks 2 and 3 leave `main.rs` in a broken-compilation state. Do NOT run `cargo check` between Tasks 2 and 4 except at the explicitly marked checkpoints. Task 4 completes the main.rs rewrite and restores compilability. + +--- + +### Task 1: Delete `route_tests.rs` and its `mod` declaration + +**Files:** + +- Delete: `crates/trusted-server-adapter-fastly/src/route_tests.rs` +- Modify: `crates/trusted-server-adapter-fastly/src/main.rs` + +- [ ] **Step 1: Delete `route_tests.rs`** + +```bash +git rm crates/trusted-server-adapter-fastly/src/route_tests.rs +``` + +- [ ] **Step 2: Remove the `mod route_tests` declaration from `main.rs` (~line 46-47)** + +Delete: + +```rust +#[cfg(test)] +mod route_tests; +``` + +- [ ] **Step 3: Verify compiles** + +```bash +cargo check -p trusted-server-adapter-fastly +``` + +Expected: PASS. + +- [ ] **Step 4: Run Fastly tests to confirm baseline** + +```bash +cargo test-fastly +``` + +Expected: PASS (tests in `main.rs` and `app.rs` still run). + +- [ ] **Step 5: Commit** + +```bash +git add crates/trusted-server-adapter-fastly/src/main.rs +git commit -m "Remove legacy route_request test file + +route_tests.rs tested route_request() and HandlerOutcome, which are +legacy-path only. Equivalent EdgeZero dispatch coverage exists in +app.rs. Part of #501 legacy entry point cleanup." +``` + +--- + +### Task 2: Delete `legacy_main()` and all legacy-path-only code from `main.rs` + +> Do all deletions in one pass before running `cargo check`. They have cross-references; partial deletion will not compile. The ONLY intentional `cargo check` in this task is at Step 8 — expected to partially fail. + +**Files:** + +- Modify: `crates/trusted-server-adapter-fastly/src/main.rs` + +- [ ] **Step 1: Delete `HandlerOutcome` enum and its `impl` block (~lines 62-78)** + +Delete from the doc comment `/// Result of routing a request...` through the closing `}` of `impl HandlerOutcome`. + +- [ ] **Step 2: Delete `legacy_main()` function (~lines 346-431)** + +Delete from the doc comment `/// Handles a request using the original Fastly-native entry point...` through the closing `}` of `legacy_main`. This includes the `// TODO: delete after Phase 5...` comment on line 345. + +- [ ] **Step 3: Delete the three FALLBACK\_\* constants and `build_ja4_debug_response()` (~lines 433-479)** + +Delete: + +```rust +const FALLBACK_UNAVAILABLE: &str = "unavailable"; +const FALLBACK_NOT_SENT: &str = "not sent"; +const FALLBACK_NONE: &str = "none"; +``` + +And the entire `build_ja4_debug_response()` function and doc comment. + +- [ ] **Step 4: Delete `route_request()` function (~lines 481-591)** + +Delete from `async fn route_request(` through its closing `}`. + +- [ ] **Step 5: Delete `resolve_publisher_response()` (~lines 593-612)** + +Delete the non-buffered `fn resolve_publisher_response` — NOT `resolve_publisher_response_buffered`. + +- [ ] **Step 6: Delete `finalize_response()` thin wrapper (~lines 650-652)** + +Delete: + +```rust +fn finalize_response(settings: &Settings, geo_info: Option<&GeoInfo>, response: &mut HttpResponse) { + apply_finalize_headers(settings, geo_info, response); +} +``` + +- [ ] **Step 7: Delete `http_error_response()` (~lines 654-666)** + +Delete from `fn http_error_response(` through its closing `}`. + +- [ ] **Step 8: Check current state (expected to partially fail)** + +```bash +cargo check -p trusted-server-adapter-fastly 2>&1 | grep "^error" | head -15 +``` + +Expected errors: `main()` still calls `legacy_main()` and canary functions. These are fixed in Tasks 3 and 4. + +--- + +### Task 3: Delete canary flag plumbing from `main.rs` + +**Files:** + +- Modify: `crates/trusted-server-adapter-fastly/src/main.rs` + +- [ ] **Step 1: Delete `EDGEZERO_ENABLED_KEY` and `EDGEZERO_ROLLOUT_PCT_KEY` constants (~lines 55-56)** + +Delete: + +```rust +const EDGEZERO_ENABLED_KEY: &str = "edgezero_enabled"; +const EDGEZERO_ROLLOUT_PCT_KEY: &str = "edgezero_rollout_pct"; +``` + +- [ ] **Step 2: Delete `parse_edgezero_flag()` with its doc comment** + +- [ ] **Step 3: Delete `parse_rollout_pct()` with its doc comment** + +- [ ] **Step 4: Delete `fnv1a_bucket()` with its doc comment** + +- [ ] **Step 5: Delete `canary_routes_to_edgezero()` with its doc comment** + +- [ ] **Step 6: Delete `is_edgezero_enabled()` with its doc comment** + +- [ ] **Step 7: Delete `read_rollout_pct()` with its doc comment** + +--- + +### Task 4: Simplify `main()` and rewrite `edgezero_main()` + +> After this task, `main.rs` compiles cleanly. The remaining tasks clean up other files. + +**Files:** + +- Modify: `crates/trusted-server-adapter-fastly/src/main.rs` + +- [ ] **Step 1: Replace `main()` body** + +Replace the entire current `fn main()` (including its doc comment) with the simplified version from the "Simplified `main()`" section above. + +- [ ] **Step 2: Replace `edgezero_main()` signature and update its body** + +- Change signature from `fn edgezero_main(mut req: FastlyRequest, config_store: ConfigStoreHandle)` to `fn edgezero_main(mut req: FastlyRequest)` +- Add the `open_trusted_server_config_store()` call at the top (with error handling) +- Remove the old dispatch-succeeded/failed comment about logger initialization avoiding double-init +- Add the new comment from the target state above + +Replace `open_trusted_server_config_store()` doc comment with the simplified version above. + +- [ ] **Step 3: Verify compiles** + +```bash +cargo check -p trusted-server-adapter-fastly +``` + +Expected: PASS or unused-import warnings only (cleaned up in Task 5). + +- [ ] **Step 4: Run Fastly tests** + +```bash +cargo test-fastly +``` + +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add crates/trusted-server-adapter-fastly/src/main.rs +git commit -m "Delete legacy_main, canary routing, and flag plumbing + +Remove legacy_main(), route_request(), HandlerOutcome, and all flag-reading +machinery (edgezero_enabled / edgezero_rollout_pct). Entry point is now +a direct trampoline to edgezero_main(), which opens the config store +internally. Part of #501 legacy entry point cleanup." +``` + +--- + +### Task 5: Delete legacy-only functions from `compat.rs` and delete `error.rs` + +> Both files have functions that were only ever called from `legacy_main()` (now deleted). +> `error.rs` is entirely legacy-only and can be deleted. `compat.rs` has three survivors. + +**Files:** + +- Modify: `crates/trusted-server-adapter-fastly/src/compat.rs` +- Delete: `crates/trusted-server-adapter-fastly/src/error.rs` +- Modify: `crates/trusted-server-adapter-fastly/src/main.rs` (remove `mod error;` and its import) + +- [ ] **Step 1: Delete `build_http_request()` and `from_fastly_request()` from `compat.rs`** + +Delete lines 11-38 — the private `build_http_request()` helper and the `from_fastly_request()` function with its doc comment. + +- [ ] **Step 2: Delete `to_fastly_response_skeleton()` from `compat.rs`** + +Delete lines 74-85 — the function and its doc comment. + +- [ ] **Step 3: Update `compat.rs` module doc comment** + +Replace: + +```rust +//! Compatibility bridge between `fastly` SDK types and `http` crate types. +//! +//! Contains only the functions used by the legacy `main()` entry point. +//! Relocated from `trusted-server-core` as part of removing all `fastly` crate +//! imports from the core library. +``` + +With: + +```rust +//! Compatibility bridge between `fastly` SDK types and `http` crate types. +``` + +- [ ] **Step 4: Delete `error.rs`** + +```bash +git rm crates/trusted-server-adapter-fastly/src/error.rs +``` + +- [ ] **Step 5: Remove `mod error;` from `main.rs` (~line 41)** + +Delete the line: + +```rust +mod error; +``` + +- [ ] **Step 6: Remove `use crate::error::to_error_response;` from `main.rs` (~line 50)** + +Delete the line: + +```rust +use crate::error::to_error_response; +``` + +- [ ] **Step 7: Verify compiles** + +```bash +cargo check -p trusted-server-adapter-fastly +``` + +Expected: PASS or unused-import warnings only. + +- [ ] **Step 8: Run Fastly tests** + +```bash +cargo test-fastly +``` + +Expected: PASS. The two `compat.rs` tests still run (they test the surviving functions). + +- [ ] **Step 9: Commit** + +```bash +git add crates/trusted-server-adapter-fastly/src/compat.rs +git add crates/trusted-server-adapter-fastly/src/main.rs +git commit -m "Delete legacy-only compat functions and error module + +Remove from_fastly_request(), to_fastly_response_skeleton(), and their +build_http_request() helper from compat.rs — all were only called from +legacy_main(). Delete error.rs entirely (to_error_response() was its only +export; only caller was legacy_main()). Part of #501." +``` + +--- + +### Task 6: Delete canary tests and strip all unused imports from `main.rs` + +**Files:** + +- Modify: `crates/trusted-server-adapter-fastly/src/main.rs` + +- [ ] **Step 1: Delete canary-related test functions from `mod tests`** + +Delete these 14 test functions: + +1. `parses_true_flag_values` +2. `rejects_non_true_flag_values` +3. `parses_valid_rollout_percentages` +4. `rejects_invalid_rollout_percentages` +5. `bucket_is_in_range_0_to_99` +6. `bucket_is_deterministic` +7. `bucket_matches_known_fnv1a_vector` +8. `bucket_distributes_across_range` +9. `empty_key_bucket_is_valid` +10. `rollout_zero_routes_all_to_legacy` +11. `rollout_hundred_routes_all_to_edgezero` +12. `rollout_fifty_routes_exactly_half_of_bucket_space` +13. `rollout_one_routes_exactly_one_bucket` +14. `ja4_debug_response_uses_plain_text_and_fallback_values` + +Also delete `use fastly::mime;` from the test `use` block — only needed for `ja4` test. + +- [ ] **Step 2: Delete unused top-level imports (from the "Imports to remove" section above)** + +Delete these entire `use` lines: + +- `use trusted_server_core::auction::endpoints::handle_auction;` +- `use trusted_server_core::auction::AuctionOrchestrator;` +- `use trusted_server_core::auth::enforce_basic_auth;` +- `use trusted_server_core::platform::RuntimeServices;` +- `use trusted_server_core::request_signing::{...};` + +Trim these lines: + +```rust +// Was: +use crate::app::{build_state, runtime_services_for_consent_route, TrustedServerApp}; +// Change to: +use crate::app::TrustedServerApp; + +// Was: +use crate::platform::{build_runtime_services, FastlyPlatformGeo}; +// Change to: +use crate::platform::FastlyPlatformGeo; + +// Was: +use edgezero_core::http::{header, HeaderValue, Method, Request as HttpRequest, Response as HttpResponse}; +// Change to: +use edgezero_core::http::{header, HeaderValue, Response as HttpResponse}; + +// Was: +use trusted_server_core::error::{IntoHttpResponse, TrustedServerError}; +// Change to: +use trusted_server_core::error::TrustedServerError; + +// Delete entire line: +use trusted_server_core::proxy::{handle_first_party_click, handle_first_party_proxy, handle_first_party_proxy_rebuild, handle_first_party_proxy_sign}; + +// Was: +use trusted_server_core::publisher::{handle_publisher_request, handle_tsjs_dynamic, stream_publisher_body, OwnedProcessResponseParams, PublisherResponse}; +// Change to: +use trusted_server_core::publisher::{stream_publisher_body, OwnedProcessResponseParams, PublisherResponse}; +``` + +- [ ] **Step 3: Verify no unused-import warnings** + +```bash +cargo check -p trusted-server-adapter-fastly 2>&1 | grep "unused import" +``` + +Expected: no output. If any warnings remain, remove the flagged import. + +- [ ] **Step 4: Run Fastly tests** + +```bash +cargo test-fastly +``` + +Expected: PASS. Surviving tests: `health_response_*`, `response_was_finalized_by_middleware_strips_sentinel`, `entry_point_finalize_skips_geo_lookup_for_401`. + +- [ ] **Step 5: Commit** + +```bash +git add crates/trusted-server-adapter-fastly/src/main.rs +git commit -m "Strip canary tests and unused imports from main.rs + +Remove 14 canary-routing test functions and the JA4 debug test. +Strip all imports that were only consumed by deleted code. Part of #501." +``` + +--- + +### Task 7: Delete `build_runtime_services` from Fastly `platform.rs` + +**Files:** + +- Modify: `crates/trusted-server-adapter-fastly/src/platform.rs` + +- [ ] **Step 1: Find exact lines** + +```bash +grep -n "build_runtime_services\|fn noop_kv_store" crates/trusted-server-adapter-fastly/src/platform.rs +``` + +- [ ] **Step 2: Delete `pub fn build_runtime_services(...)` and its doc comment** + +- [ ] **Step 3: Delete `fn noop_kv_store()` test helper** + +Confirmed: `noop_kv_store()` is only used by the two `build_runtime_services` tests being deleted — safe to remove. + +- [ ] **Step 4: Delete the two test functions** + +Delete: + +- `fn build_runtime_services_client_info_is_none_without_tls()` +- `fn build_runtime_services_returns_cloneable_services()` + +- [ ] **Step 5: Update module-level doc comment** + +Find and remove the sentence "This module also provides `build_runtime_services`, a free function that..." from the module doc. + +- [ ] **Step 6: Verify compiles and tests pass** + +```bash +cargo check -p trusted-server-adapter-fastly && cargo test-fastly +``` + +Expected: PASS. + +- [ ] **Step 7: Commit** + +```bash +git add crates/trusted-server-adapter-fastly/src/platform.rs +git commit -m "Remove build_runtime_services from Fastly platform module + +build_runtime_services() was the legacy-path per-request service factory. +The EdgeZero path uses build_per_request_services() in app.rs instead. +Remove the dead function, its noop_kv_store() test helper, and its tests. +Part of #501." +``` + +--- + +### Task 8: Update stale doc comments in `middleware.rs` and `app.rs` + +**Files:** + +- Modify: `crates/trusted-server-adapter-fastly/src/middleware.rs` +- Modify: `crates/trusted-server-adapter-fastly/src/app.rs` + +- [ ] **Step 1: Find stale references in `middleware.rs`** + +```bash +grep -n "finalize_response\|route_request" crates/trusted-server-adapter-fastly/src/middleware.rs +``` + +- [ ] **Step 2: Fix module doc in `middleware.rs` (~line 4)** + +Remove the phrase `from the legacy [\`crate::finalize_response\`] and [\`crate::route_request\`]:`. Keep the description of what the middleware does; remove only the stale intra-doc links. + +- [ ] **Step 3: Fix fn-level doc in `middleware.rs` (~line 150)** + +Remove `Mirrors [\`crate::finalize_response\`] exactly` (or rephrase without the broken intra-doc link). + +- [ ] **Step 4: Fix `http_error()` doc in `app.rs` (~line 243)** + +Replace: + +```rust +/// Convert a [`Report`] into an HTTP [`Response`], +/// mirroring [`crate::http_error_response`] exactly. +/// +/// The near-identical function in `main.rs` is intentional: the legacy path +/// uses fastly HTTP types while this path uses `edgezero_core` types. +``` + +With: + +```rust +/// Converts a [`Report`] into an HTTP [`Response`]. +``` + +- [ ] **Step 5: Verify doc check** + +```bash +cargo doc --no-deps -p trusted-server-adapter-fastly 2>&1 | grep "warning\|error" | head -20 +``` + +Expected: no broken intra-doc link warnings. + +- [ ] **Step 6: Commit** + +```bash +git add crates/trusted-server-adapter-fastly/src/middleware.rs crates/trusted-server-adapter-fastly/src/app.rs +git commit -m "Remove stale legacy references from doc comments + +middleware.rs referenced deleted finalize_response() and route_request(). +app.rs referenced deleted http_error_response(). Part of #501." +``` + +--- + +### Task 9: Remove canary flag keys from `fastly.toml` + +**Files:** + +- Modify: `fastly.toml` + +- [ ] **Step 1: Remove the two flag keys and their comments** + +In `[local_server.config_stores.trusted_server_config.contents]` (around lines 46-56), remove: + +```toml + # "true" / "1" (case-insensitive) enable the EdgeZero path. Missing, + # unreadable, or any other value falls back to the legacy entry point. + # Keep "false" until EdgeZero reaches full functional parity with legacy. + edgezero_enabled = "false" + # Integer 0-100. Effective only when edgezero_enabled = "true". + # 0 -> all traffic to legacy (instant rollback — no deploy needed) + # 1-99 -> canary: clients whose fnv1a_bucket(client_ip) < this value go EdgeZero + # 100 -> all traffic to EdgeZero (full cutover) + # Key absent when edgezero_enabled = "true" is treated as 100 (full rollout). + # IMPORTANT: Set this to "0" in production BEFORE setting edgezero_enabled = "true". + edgezero_rollout_pct = "0" +``` + +The `[local_server.config_stores.trusted_server_config.contents]` block becomes empty (`{}`). The store declaration and the `[local_server.config_stores.trusted_server_config]` header stay — the store is still opened by `open_trusted_server_config_store()`. + +- [ ] **Step 2: Verify local config loads without error** + +```bash +cargo check -p trusted-server-adapter-fastly +``` + +Expected: PASS (fastly.toml is only read at runtime by Viceroy/Fastly, not by `cargo check`). + +- [ ] **Step 3: Commit** + +```bash +git add fastly.toml +git commit -m "Remove edgezero_enabled and edgezero_rollout_pct from fastly.toml + +The app no longer reads these config store keys. Remove them from the +local dev config store. The trusted_server_config store itself stays — +it is still opened by open_trusted_server_config_store(). Part of #501." +``` + +--- + +### Task 10: Run full CI gates + +**Files:** None (verification only) + +- [ ] **Step 1: Format check** + +```bash +cargo fmt --all -- --check +``` + +If it fails: `cargo fmt --all && git add -u && git commit -m "Format after legacy cleanup"` + +- [ ] **Step 2: Clippy — all adapter targets** + +```bash +cargo clippy-fastly +cargo clippy-axum +cargo clippy-cloudflare +cargo clippy-spin-native +cargo clippy-spin-wasm +``` + +Expected: zero warnings/errors on all targets. + +- [ ] **Step 3: Test — all adapter targets** + +```bash +cargo test-fastly && cargo test-axum && cargo test-cloudflare && cargo test-spin +``` + +Expected: PASS. + +- [ ] **Step 4: Parity test** + +```bash +cargo test --manifest-path crates/integration-tests/Cargo.toml --test parity +``` + +Expected: PASS. + +- [ ] **Step 5: JS tests and format** + +```bash +cd crates/js/lib && npx vitest run && npm run format +``` + +Expected: PASS. + +- [ ] **Step 6: Docs format** + +```bash +cd docs && npm run format -- --check +``` + +Expected: PASS. + +- [ ] **Step 7: Confirm all clean before PR** + +```bash +git status +``` + +Expected: clean working tree. All changes committed. + +--- + +## Execution options + +**Plan complete and saved to `docs/superpowers/plans/2026-05-27-pr20-legacy-cleanup.md`.** + +**1. Subagent-Driven (recommended)** — Fresh subagent per task, review between tasks, fast iteration + +**2. Inline Execution** — Execute tasks in this session using `executing-plans`, batch with checkpoints + +Which approach? diff --git a/fastly.toml b/fastly.toml index 09858110..6865fc95 100644 --- a/fastly.toml +++ b/fastly.toml @@ -43,17 +43,6 @@ build = """ [local_server.config_stores.trusted_server_config] format = "inline-toml" [local_server.config_stores.trusted_server_config.contents] - # "true" / "1" (case-insensitive) enable the EdgeZero path. Missing, - # unreadable, or any other value falls back to the legacy entry point. - # Keep "false" until EdgeZero reaches full functional parity with legacy. - edgezero_enabled = "false" - # Integer 0-100. Effective only when edgezero_enabled = "true". - # 0 -> all traffic to legacy (instant rollback — no deploy needed) - # 1-99 -> canary: clients whose fnv1a_bucket(client_ip) < this value go EdgeZero - # 100 -> all traffic to EdgeZero (full cutover) - # Key absent when edgezero_enabled = "true" is treated as 100 (full rollout). - # IMPORTANT: Set this to "0" in production BEFORE setting edgezero_enabled = "true". - edgezero_rollout_pct = "0" [local_server.config_stores.jwks_store] format = "inline-toml"