Skip to content

Commit c412735

Browse files
authored
Add Fermyon Spin adapter (wasm32-wasip1) (#735)
* Exclude Cloudflare adapter from wasm32-wasip1 test job The adapter's tokio dev-dependency uses rt-multi-thread which is not supported on wasm32. It is already tested in the dedicated test-cloudflare job; exclude it from the workspace wasm test to avoid the compile error. * Address round-3 review findings - Revert proxy.rs merge artifact: restore per-request allowed_domains at both redirect_is_permitted call sites; remove dead_code allow and stale comment — integration proxies defaulting to &[] get open mode again as documented - Drop unused trusted-server-js dep from adapter Cargo.toml - Fix check_response: gate body read behind error branch so 2xx paths do not buffer and discard the response body - Remove self-referential SECRET_UPSERT_METHOD test - Reorder write-cost doc so outbound HTTPS round-trip leads; handle-open caching noted as negligible - Refactor make_request to take fastly::http::Method; drop string match and unreachable arm; remove SECRET_UPSERT_METHOD const - Add SigningStoreIds named struct in endpoints.rs; update both call sites to destructure by name * Address PR review: add body-size caps and remove orchestrator duplication - Add VERIFY_MAX_BODY_BYTES (4096) cap to handle_verify_signature - Add AUCTION_MAX_BODY_BYTES (65536) cap to handle_auction - Add ADMIN_MAX_BODY_BYTES (4096) cap to handle_rotate_key and handle_deactivate_key - Delete platform_response_to_fastly from orchestrator; both call sites now use crate::compat::to_fastly_response directly - Add sign_rejects_oversized_body and rebuild_rejects_oversized_body regression tests asserting 413 on oversized POST bodies * Address PR review: fmt, testlight body cap, auction constant, stale README - Run cargo fmt --all (7+ files had formatting failures) - Cap testlight POST body with collect_body_bounded + INTEGRATION_MAX_BODY_BYTES - Use dedicated AUCTION_MAX_BODY_BYTES (256 KiB) for /auction instead of INTEGRATION_MAX_BODY_BYTES - Update auction/README.md: parse_response is now async, parallel execution via select() is live * Address PR14 review findings: middleware finalize and missing HTTP methods FinalizeResponseMiddleware now absorbs errors from inner middleware/handlers by converting EdgeError to a Response via IntoResponse, so apply_finalize_headers always runs regardless of handler outcome. Geo lookup is moved after next.run and skipped for UNAUTHORIZED responses to avoid unnecessary backend calls. Register HEAD and OPTIONS catch-all routes so cache-validation and CORS preflight requests reach the publisher origin instead of returning 405. Update module docstring to note startup_error_router skips middleware. * Make AppState private — not part of any public API * Address PR15 review findings: diagnostic regression, header dedup, compat tests - predict_backend_name_for_url now returns Result<String, Report<TrustedServerError>> instead of Option<String>; call sites use inspect_err(...).ok() to preserve the Option<String> trait interface while logging the actual failure reason - Promote SPOOFABLE_FORWARDED_HEADERS to pub in http_util; replace inline copy in compat.rs with a use import — single source of truth for the strip list - Add sanitize_fastly_forwarded_headers_strips_spoofable test (all 4 entries + Host preserved) and to_fastly_response_with_streaming_body_produces_empty_body test (pins silent-drop behaviour) to compat.rs - Change Consent KV non-ItemNotFound error log from debug "lookup miss" to warn "lookup failed" for operational visibility - Drop stale "will be removed in PR 15" forward references from app.rs and main.rs - Drop unnecessary u16 type suffixes on port literal defaults * Address PR16 review findings: unused deps, stale lockfile, middleware bypass, refactors Blocking: - Remove unused serde_json and trusted-server-js from Cargo.toml - Delete crate-local Cargo.lock (now silently ignored as workspace member) - Fix CLAUDE.md workspace layout comment (drop "excluded from workspace") - Fix log message naming NoopKvStore → UnavailableKvStore in build_runtime_services - Wrap startup_error_router with FinalizeResponseMiddleware(Settings::default()) so startup-error responses carry X-Geo-Info-Available and operator response_headers Refactor: - Move AxumPlatformHttpClient to AppState; share Arc across requests via build_runtime_services(ctx, Arc::clone(&state.http_client)) to preserve the reqwest connection pool - Remove build_per_request_services no-op wrapper; inline at call sites - Use temp_env::with_var in config/secret store tests for proper isolation - Replace .unwrap() with .expect("should ...") throughout test code per CLAUDE.md Nitpick: - Delete redundant crate-local .gitignore (covered by workspace .gitignore) * Fix fixed-port TIME_WAIT flakiness and add send_async/select divergence breadcrumbs Port fix: - main.rs reads PORT env var at startup; when set, uses AxumDevServer::with_config with a dynamic SocketAddr instead of the run_app default (hardcoded 8787) - Integration test spawner (axum.rs) now calls find_available_port() and passes PORT=<port> to the child process, matching the Fastly env's dynamic-port pattern - Fallback to AXUM_DEFAULT_PORT=8787 if find_available_port fails (offline runner) Divergence breadcrumbs: - send_async: debug log noting that execution is eager and errors surface immediately, not at select() time as they do on Fastly - select: debug log noting that index 0 is popped unconditionally (sequential, not first-to-complete) — any fan-out ordering tests should use the Fastly runtime * Restore request-scoped Axum HTTP client and fix dynamic-port logging * Resolve review findings * Resolve PR review findings * Resolve PR review findings * Resolve PR15 consent and backend review findings * Resolve PR review findings * Use append_header in place of set_header * fix lint * Route Fastly cookie calls through compat bridge after PR10 merge cookies.rs set_ec_cookie/expire_ec_cookie now take Response<EdgeBody> to satisfy the migration_guards invariant. registry.rs and publisher.rs call the Fastly-typed equivalents in compat instead. Remove synthetic.rs entry from migration_guards (file deleted in PR10). * Remove unused Logger import * fix cargo fmt * resolve PR review findings * Added E2E tests dispatch_auth_rejected_401_carries_finalize_headers, dispatch_unregistered_method_returns_405_at_router_level * fix cargo fmt * Resolve PR review suggestions * Resolve PR review findings * Merge feature/edgezero-pr15-remove-fastly-core into PR16 Conflict resolutions: - Cargo.lock: regenerated after adding futures dep - docs/guide/testing.md: kept Axum adapter test command (PR16) and EC ID rename + specific-test example (PR15) - .cargo/config.toml: kept PR16 version (no WASM default target) because trusted-server-adapter-axum is now a workspace member and tokio/reqwest cannot compile to wasm32-wasip1 - AGENTS.md: updated cargo test instruction to document both required commands now that the workspace contains both WASM-only and native-only crates API breakage from PR15 (handle_publisher_request now returns PublisherResponse): - Add resolve_publisher_response() helper mirroring the Fastly adapter pattern (Buffered / Stream / PassThrough -> Response) - Drop the removed None arg from handle_auction and handle_publisher_request call sites * Complete PR15 merge and restructure workspace for mixed-target adapters PR15 merge — modifications to existing files not captured in the merge commit: - Fastly adapter: migrate to EdgeZero router/middleware, rewrite management API, update platform/compat/logging/backend to new EdgeZero abstractions - Core: remove KV-backed consent layer (consent/kv.rs deleted), update auction orchestrator and integrations to new API surface, refactor html_processor, cookies, and error types - Delete stale Fastly KV store test fixtures (counter_store.json, opid_store.json) - Update fastly.toml, trusted-server.toml, and integration test config to match PR16 workspace restructuring: - Add both adapters to workspace members; set default-members=[core, axum] so bare `cargo test` runs natively without WASM target conflicts - Replace test-wasm alias with test-fastly (--workspace --exclude axum --target wasm32-wasip1) and keep test-axum (-p trusted-server-adapter-axum) - Update CI: cargo test-fastly in the Fastly job, cargo test-axum in native job - Axum admin routes (/admin/keys/rotate, /admin/keys/deactivate) return 501 instead of falling through to an error handler - Update CLAUDE.md, AGENTS.md, README, and all guide docs to use new aliases * Resolve PR review findings * Resolve PR review findings * Add alias for clouflare tests * Fix lint error * Set default-members to fastly adapter so Viceroy can locate the binary Viceroy internally runs `cargo run --bin trusted-server-adapter-fastly` against the default-run packages. With core + axum as defaults, Cargo fails to find the binary. Restricting default-members to the fastly adapter fixes the lookup. Updated stale comments in CLAUDE.md and .cargo/config.toml to reflect the new setup. * Resolve PR review findings * Fix fmt lint * Rename synthetic_id_cookie_value_is_safe → ec_cookie_value_is_safe * Resolve PR review findings * Resolve PR review findings * Address PR review findings * Resolve PR 624 review findings Blocking fixes: - Remove debug_assert! in compat::to_fastly_response that contradicted the documented silent-drop fallback and caused to_fastly_response_with_streaming_body_produces_empty_body to panic in debug builds - Restore streaming dispatch for PublisherResponse::Stream, which was regressed by the PR 11 compat shim. Introduce HandlerOutcome enum so route_request can signal streaming intent back to the adapter. main() handles HandlerOutcome::Streaming via to_fastly_response_skeleton + stream_to_client + stream_publisher_body directly, avoiding the Vec<u8> buffer that delayed TTFB and scaled WASM memory with body size. Non-blocking fixes: - Add to_fastly_response_skeleton for headers-only fastly response conversion - Add geo-401 comment: skip geo lookup for unauthenticated callers - Add audited-callers comment on EdgeBody::Stream arms in compat - Add O(N) / PR 15 comment on bytes.to_vec() calls - Deduplicate auction/publisher status extraction in route_tests via outcome_status helper * Call StreamingBody::finish() to properly terminate chunked response StreamingBody in fastly 0.11.12 has no Drop impl — dropping it without calling finish() leaves the chunked HTTP stream with no terminal chunk, causing clients to receive "unexpected EOF during chunk size line". Match on stream_publisher_body result and call finish() in both the success and error paths so the response is always properly terminated. * Apply cargo fmt formatting * Resolve PR 626 review findings Blocking fixes: - Fix doc_markdown clippy errors in integrations/mod.rs (BAD_GATEWAY, max_bytes, one_chunk wrapped in backticks) - Drop change_context() on collect_body_bounded() calls in auction, datadome, didomi, lockr, permutive — RequestTooLarge (413) was being rewritten to Integration/Auction (502); now propagates unchanged via ? so oversized bodies correctly surface as PAYLOAD_TOO_LARGE - Remove debug_assert! in compat::to_fastly_response (already fixed in PR 12, missed in PR 13 merge) Non-blocking fixes: - Migrate testlight upstream response read from unbounded collect_body to collect_response_bounded(UPSTREAM_RTB_MAX_RESPONSE_BYTES) matching all other RTB integrations; remove now-unused collect_body function - Upgrade orchestrator predicted-backend-name fallback log from debug to warn so operators see silent bid-loss fallback in production * Apply cargo fmt formatting * Address review Non blocker findings * Restore EdgeZero consent KV wiring * Resolve PR review findings and add startup listen log Blocking fixes: - Fix cargo fmt failure in auction handler Ok() block (app.rs) - Extract SpawnedRequestResult type alias to fix clippy::type_complexity (platform.rs) - Rename TRUSTED_SERVER__SYNTHETIC__SECRET_KEY to TRUSTED_SERVER__EDGE_COOKIE__SECRET_KEY in scripts/integration-tests.sh and setup-integration-test-env/action.yml (PR15 rename miss) - Update CLAUDE.md CI Gates to reference cargo test-fastly && cargo test-axum - Add clippy-fastly/clippy-axum cargo aliases; split format.yml clippy into two target-matched steps so wasm32-wasip1 paths are linted in CI Non-blocking: - Refactor startup_error_router: rename make closure to make_handler (one indirection level) - Remove redundant rotate_handler/deactivate_handler aliases; pass admin_not_supported directly - Log warn on invalid PORT env var value instead of silently falling back - Log debug when buffering Body::Stream to Vec<u8> for outbound requests - Move simple_logger to workspace dependencies - Update agent docs (pr-reviewer, verify-app, pr-creator) to use cargo test-fastly/clippy-fastly - Emit "Listening on http://{addr}" at startup for both PORT and run_app paths - Format docs markdown tables with Prettier * Address PR 628 review findings P1: Cap EdgeZero publisher fallback buffering via configurable publisher.max_buffered_body_bytes setting. When unset (default), buffering is unbounded, restoring pre-PR14 parity. When set, a BoundedWriter enforces the limit and returns a 500 on overflow instead of growing the Wasm heap without bound. P2: Extract resolve_geo_for_response helper to middleware.rs so the 401-skip rule is defined once and shared by both FinalizeResponseMiddleware and apply_entry_point_finalize. P2: Update get_settings doc to reflect OnceLock caching — first call loads and validates, subsequent calls clone the cached value. P2: Narrow startup-error module doc — responses may still receive entry-point finalization when settings reload succeeds after build_state fails. P3: Expand legacy_main doc to list all safe-fallback cases: disabled flag, config-store open failure, key-read error, and non-truthy values. * Fix .gitignore comment: attribute .edgezero/ to upstream framework * Remove unused backend.rs — duplicate DEFAULT_FIRST_BYTE_TIMEOUT already in platform/mod.rs * Resolve PR review findings Blocking: - Fix cargo fmt failure in integration-tests cloudflare.rs (long import, struct init) - Replace `use worker::*` with explicit imports in adapter lib.rs - Return generic 500 body from top-level dispatch error; log detail server-side - Fix workerd process-group leak: spawn wrangler as group leader, killpg on drop - Use find_available_port() for wrangler dev instead of hardcoded 8787 - Reject multi-provider fan-out in select() with PlatformError::HttpClient instead of a noisy warn; per-provider timeout is now enforced by failing loudly rather than silently degrading to sum(latencies) - Fix clippy::type_complexity in axum platform.rs with ResponseTriplet alias - Fix docs prettier formatting Non-blocking: - Return Err from execute() on Body::Stream outbound instead of silent empty body - Assert unreachable! on Body::Stream in send_async (execute always returns Once) - Extract shared dispatch() helper from get_fallback/post_fallback duplicates - Rename auth test to auth_middleware_runs_in_chain_for_protected_routes - Update stale #[ignore] reasons for Cloudflare integration tests * Fix integration test failure * Fix cargo clippy fastly * Update pr review findings * Resolve PR review findings: Axum fallback method parity and stale clippy docs Register HEAD/OPTIONS/PUT/PATCH/DELETE on publisher fallback paths and non-primary methods on named routes so the Axum router matches the Fastly adapter's publisher_fallback_methods coverage. Unify get_fallback/post_fallback into a single general_fallback handler shared across all methods. Update startup_error_router to loop over all seven fallback methods consistently. Fix stale `cargo clippy --workspace` command in CLAUDE.md, README.md, and AGENTS.md — replace with `cargo clippy-fastly && cargo clippy-axum` to match CI and the mixed-target workspace setup. * Add Phase 5 verification plan for PR-18 * Add route smoke tests for all Cloudflare adapter routes Adds 10 tests to tests/routes.rs covering every explicitly registered route plus the tsjs catch-all wildcard. Assertions are scoped to routing only (not 404) for handlers that require live settings or outbound connections, matching the pattern established by the existing auction test. * Fix rustfmt formatting in Cloudflare route smoke tests * Add basic-auth parity tests to Axum and Cloudflare adapters Verifies that /admin/* routes return 401 without credentials, include a WWW-Authenticate: Basic realm=... header, and reject wrong credentials; also confirms /.well-known and /auction are not gated by admin auth. * Fix unwrap_or and comment inconsistency in basic-auth parity tests * Add admin key route full path coverage to Axum and Cloudflare adapters * Tighten storage-fail assertion and add duplicate context comments * Add cross-adapter in-process parity test suite (Axum vs Cloudflare) * Fix parity test review issues: expect messages, dead if guard, unused base64 dep * Add error-correlation unit tests for PlatformResponse backend_name * Add HTML rewriting golden tests, response size check, and Criterion benchmarks - Add golden_script_tag_injected_at_head_start: verifies script tag is the first child of <head> with nothing between the opening tag and the injected <script>. - Add golden_url_rewriting_replaces_origin_in_href: verifies origin host is fully replaced by proxy host in href/src attributes. - Add golden_integration_script_is_not_double_injected: verifies the /static/tsjs= script tag appears exactly once. - Add response_size_does_not_grow_disproportionately: verifies processed HTML stays within 2× of input size to catch buffer/double-processing bugs. - Add Criterion benchmark (html_processor_bench) for process_chunk at 10 KB and 100 KB payload sizes. * Add criterion::black_box to benchmark input for measurement purity * Add cross-adapter parity and benchmark CI gates for Phase 5 verification * Add inline comment explaining -- --test flag in benchmark CI step * Update Cargo.lock files after adding parity test dependencies * Pin axum/tower/tokio versions in integration-tests to match workspace * Pin tokio to exact workspace version =1.52.3 in integration-tests * Create parent directory before writing build output config The build script writes trusted-server-out.toml to ../../target/ relative to crates/trusted-server-core/. When the test-parity CI job builds this crate as a dependency from crates/integration-tests/ (workspace-excluded), the workspace-root target/ directory may not yet exist, causing a panic. Add fs::create_dir_all for the parent path before the write to handle this case robustly. * Remove redundant wrong-credentials tests from admin key route coverage The renamed tests duplicated coverage already provided by admin_route_with_wrong_credentials_returns_401. Auth middleware rejects any wrong credentials with 401 regardless of body content, so the extra variants added no unique signal. * Fix admin_rotate_unauthenticated_parity: assert both adapters return 401 The previous comment described the wrong divergence (authenticated path). For unauthenticated requests both adapters return 401. Add the missing assert_eq!(axum_status, 401) and assert_eq!(axum_status, cf_status) so the parity claim is actually verified for both adapters. * Add clippy gate for integration-tests crate in test-parity CI job crates/integration-tests is workspace-excluded so cargo clippy --workspace is blind to it. Add an explicit step so lint regressions in parity.rs are caught on every PR. * Extract MAX_GROWTH_FACTOR constant in html_processor growth test 2.0 was a magic number. Named constant with comment makes the bound self-documenting: 2× covers injected script tag plus URL rewrites. * Fix trailing blank line in Cloudflare routes test file * Add clippy component to test-parity toolchain setup The setup-rust-toolchain action does not guarantee clippy is installed when restoring a shared cache. Explicitly request the component so the Clippy (parity test crate) step can find cargo-clippy. * Use multi_thread tokio flavor for all CF adapter route tests Matches the convention used by the Axum adapter tests and parity tests. Single-threaded tokio can miss races in middleware that spawns tasks. * Add first-party route smoke tests to Axum adapter Matches the five first-party route tests already present in the Cloudflare adapter test suite. A silently removed route in the Axum adapter now fails the test run instead of going undetected. * Strengthen parity test assertions for unauthenticated admin routes - Assert Axum also returns WWW-Authenticate header on 401 (was CF-only) - Add admin_deactivate_unauthenticated_parity covering the deactivate path - Rename cookie_behavior_note → publisher_proxy_fallback_parity (name now reflects what the test actually verifies) - Fix expect("collect body") → expect("should collect body") per style guide * Document tokio exact pin in integration-tests Cargo.toml Adds inline comment so future maintainers know why the version is pinned with `=` rather than a range constraint. * Generate plan for spin adapter * Resolve PR review findings: Axum/Fastly parity and stale docs - Disable reqwest auto-redirects in AxumPlatformHttpClient so core proxy code receives 3xx responses and applies its own allowed_domains policy - Map select() task panics and per-request errors into ready: Err(...) so the auction orchestrator logs provider failures and continues rather than treating one bad provider as a fatal auction error - Extract buffer_publisher_response into trusted_server_core::publisher; both adapters now call it, removing duplicated buffering logic - Extract origin_response_metadata and apply_image_passthrough_metadata helpers in proxy.rs; both finalizers share one copy of metadata extraction and pixel heuristics - Rewrite app.rs with NamedRouteHandler enum, execute_handler, and a named_routes table mirroring the Fastly adapter; remove per-route boilerplate closures (~180 lines) - Add crate-level and per-module doc comments to axum adapter lib.rs - Replace bare cargo build/test/clippy docs with target-specific aliases across README, getting-started, and testing guides - Document PORT env var override for Axum dev server * Resolve PR review findings: Axum/Fastly parity and stale docs Fix EdgeZero HTTPS scheme detection regression: capture tls_protocol and tls_cipher from the raw FastlyRequest before dispatch_with_config_handle consumes it, inject as trusted internal headers x-ts-tls-protocol / x-ts-tls-cipher (added to INTERNAL_HEADERS), and read them in build_per_request_services to populate ClientInfo. This restores parity with the legacy path where RequestInfo::from_request detects HTTPS via ClientInfo TLS fields rather than falling back to the default "http". Skip the from_fastly_response -> to_fastly_response round-trip for middleware-finalized responses in edgezero_main. Checking x-ts-finalized on the FastlyResponse directly and calling send_to_client() without the intermediate take_body_bytes() call avoids re-materializing the full response body in WASM heap on the normal registered-route path. Update integration-guide.md and pull_request_template.md to use the target-specific cargo aliases (cargo test-fastly / cargo test-axum, cargo clippy-fastly / cargo clippy-axum) instead of the removed --workspace variants. * Tighten the spin adapter integration plan * Add Fermyon Spin adapter with EdgeZero rev bump and CI Adds trusted-server-adapter-spin: Spin entrypoint via edgezero_adapter_spin::run_app, EdgeZero and Spin runtime manifests, platform runtime services (null geo, sync Spin variable secret adapter, conservative buffered HTTP client with gzip/br decompression policy), route/auth smoke tests, and EdgeZero manifest validation test. Bumps EdgeZero deps to ce6bcf74b529d9066d08ba87b2971af8379eb29e to access edgezero-adapter-spin. The new rev requires fastly = "0.12" (edgezero-adapter-fastly workspace dependency) and worker = "0.8" (Cloudflare adapter type compatibility). Pins viceroy 0.16.5 to resolve the Fastly SDK 0.12 bot-analysis hostcall issue. Extends parity tests to include Spin as a third in-process adapter. Adds target-matched cargo aliases and CI jobs for Spin native and wasm32-wasip1 targets. Updates CLAUDE.md lint and build guidance for the mixed-runtime workspace. Known MVP limits: Spin component variables do not map cleanly to all Trusted Server config keys; authenticated key rotation success is not claimed. Spin KV TTL is unavailable in the current EdgeZero Spin KV adapter. * Bump worker-build to ^0.8 for worker 0.8 compatibility * Reject digit-leading KIDs in validate_kid KIDs starting with a digit would alias in the Spin variable encoder (both "1foo" and "n1foo" map to "v_n1foo"). Blocking them at validation eliminates the risk without changing the encoding scheme. * Fix Spin adapter WASI HTTP proxy and variable encoding - Filter WASI HTTP P2 forbidden outbound headers (host, connection, keep-alive, transfer-encoding, upgrade, proxy-connection) before building the Spin outbound request; the host header caused every proxy request to fail with HeaderError::Forbidden - Fix push_spin_variable_escape to emit _xhh not _hh; corrects all variable names in spin.toml and encoded test expectations - Prepend n to digit-leading keys in spin_variable_name so Spin's letter-led segment requirement is met; aliasing now unreachable because validate_kid rejects digit-leading KIDs - Fix with_capacity to account for the optional n prefix - Remove redundant localhost entries from allowed_outbound_hosts (http://*:* already covers them) - Update spin.toml encoding comment: collision-free → collision-resistant * Resolve PR 624 round-6 review findings Blocking fixes: - Restore drop(streaming_body) on error path so client sees EOF/truncated response instead of a silently completed partial response (cd4c621 regression) - Add HandlerOutcome::AuthChallenge variant to distinguish our enforce_basic_auth 401s from origin-forwarded 401s; gate geo lookup on AuthChallenge only so origin-forwarded 401s still carry geo headers Non-blocking fixes: - Fix stream_publisher_body doc: remove incorrect async claim; function is sync - Clarify to_fastly_request_ref doc: non-empty body is a caller error - Add enforce_max_body_size helper in http_util; replace 6 duplicated body-size cap blocks across proxy, auction, and request_signing endpoints - Add test for to_fastly_response_skeleton covering status + header round-trip - Restore "bytes" unit in enforce_max_body_size error message for log clarity - Widen enforce_max_body_size what param from &'static str to &str * Resolve PR 626 round-1 review findings Blocking fixes: - Use collect_response_bounded for sourcepoint JS response bodies, replacing the EdgeBody::Stream branch that silently returned an empty body; streaming responses now drain up to the 5 MiB cap and surface oversized bodies as Integration/502, matching all other integrations - Run cargo fmt to fix 8 formatting hunks in sourcepoint.rs introduced by the May 12 merge Non-blocking: - Orchestrator now identifies the failing provider on select() Err by diffing backend_to_provider keys against the new remaining list; logs with provider name and pushes AuctionResponse::error so the response array counts correctly Refactor: - Add first_byte_timeout: Option<Duration> to ensure_integration_backend; proxy call sites pass None (keeps 15 s default); auction providers (aps, adserver_mock, prebid) switch from BackendConfig::from_url_with_first_byte_timeout to ensure_integration_backend with the auction-scoped timeout, going through the platform abstraction consistently * Resolve PR 628 round-1 review findings Blocking: - Default max_buffered_body_bytes to Some(16 MiB) so EdgeZero flag-flip fails-safe; operators must opt out for pages larger than 16 MiB Non-blocking: - Rename response_was_finalized_by_middleware to take_finalize_sentinel so the side-effecting header removal is visible in the name - Inline apply_entry_point_finalize at its single call site; remove the thin wrapper function; update the test to call resolve_geo_for_response and apply_finalize_headers directly - Extract build_state_from_settings(settings) so build_state() and the test-only app_state_for_settings() share a single constructor path - BoundedWriter: use std::io::Error::other() for consistency - Convert named_routes() -> [NamedRoute; 9] to const NAMED_ROUTES: &[NamedRoute]; removes the size literal coupling and avoids a stack copy on every call * Resolve PR 635 round-1 review findings Blocking: - select(): return Err(PlatformError::HttpClient) when get_backend_name() returns None instead of silently propagating "" and losing bid correlation - Remove dead certificate_check: bool from predict_integration_backend_name; hardcode true inside to match ensure_integration_backend_with_timeout so predict/ensure cannot silently diverge and break orchestrator correlation Non-blocking: - Fix duplicate enable_ssl().sni_hostname() in BackendConfig::ensure; call once unconditionally, then add check_certificate() conditionally - Add debug_assert!(false) in to_fastly_response Stream arm to catch caller misuse in debug production builds; gated with #[cfg(not(test))] so the behavior-documentation test still passes - Add integration-route assertion to edgezero_missing_consent_store test: GET /integrations/datadome/tags.js must not return 503 when consent KV is misconfigured, locking in that integration proxy bypasses consent gating - Rename AppState.kv_store -> default_kv_store to signal it is only the fallback when no per-route consent KV override applies * Resolve PR 643 round-2 review findings Blocking: - Strip x-ts-tls-protocol and x-ts-tls-cipher from the inbound request before conditionally re-setting them from the Fastly SDK in edgezero_main; without this, a plain-HTTP client injecting X-TS-TLS-Protocol spoofs the scheme detection path, affecting cookie Secure and URL rewriting - Add regression test in app.rs asserting tls_protocol/tls_cipher are None when the trusted headers are absent after the strip+set logic Non-blocking: - Register GET /health -> 200 ok on TrustedServerApp so health_check_path() is satisfied and the shared wait_for_ready helper can be used - Promote stateless shims (AxumPlatformConfigStore, AxumPlatformSecretStore, UnavailableKvStore, AxumPlatformBackend, AxumPlatformGeo) to OnceLock statics in build_runtime_services so Arc::clone is used per request instead of allocating a fresh Arc each time - Unify the two divergent main() branches into a single code path using AxumDevServer::with_config; log logger init failure with eprintln! instead of silently swallowing it - Exit non-zero on PORT parse error instead of falling back silently to axum.toml default — the fallback surprises tooling expecting the server at the explicitly requested port * Fix missing compat::from_fastly_response conversion in edgezero_main The merge conflict resolution took PR15's take_finalize_sentinel code expecting an HttpResponse but left the fastly_response variable unconverted. Add the missing let mut response = compat::from_fastly_response(fastly_response) before the sentinel check. * Fix dead code and spurious mut from PR15 merge in main.rs - Remove BoundedWriter and resolve_publisher_response_buffered from main.rs; they were copied in by the merge but belong in app.rs for the EdgeZero path; the legacy path uses stream_publisher_body directly - Remove the now-unused std::io::Write import - Fix let mut fastly_response to let fastly_response (no longer mutated) * Resolve PR 644 round-4 review findings Blocking: - Extract reject_multi_provider_fanout(len) as a target-agnostic free function gated #[cfg(any(target_arch = \"wasm32\", test))]; add 4 unit tests covering len=0 (pass), len=1 (pass), len=2 (reject), len=5 (reject with count in msg) - Inline-confirm Secret::to_string() returns the raw JsValue string with no wrapping — verified against worker-rs src/env.rs Display impl Non-blocking: - from_utf8_lossy -> std::str::from_utf8 with a hard error on non-UTF-8 outbound header values; lossy conversion silently mutated bytes - to_ascii_lowercase() -> eq_ignore_ascii_case() for content-encoding / transfer-encoding header checks — no allocation, same semantics - Remove to_ascii_uppercase() on Method::from; worker 0.7 uppercases internally - Strip Transfer-Encoding before setting Content-Length in buffered publisher response (defense-in-depth; Workers likely strips it anyway) - build.sh: gate cargo install with command -v to skip reinstall when warm - wrangler.toml: add comment above placeholder KV ID explaining local vs remote - wrangler.toml: add comment explaining compatibility_date and when to bump it * Fix Method::from — worker 0.7 implements From<String> not From<&str> The prior commit used Method::from(method.as_str()) based on reviewer note that From<&str> is case-insensitive, but From<&str> is not implemented at all in worker 0.7 — only From<String>. http::Method::to_string() already returns uppercase so the to_ascii_uppercase() allocation is removed without changing semantics. * Add compile_error! for cloudflare feature on non-wasm32 targets Enables the cloudflare feature on a native target pulls worker which requires wasm-bindgen and produces cryptic linker errors. The guard catches it immediately with a clear message pointing to the correct --target wasm32-unknown-unknown flag. * Collapse 13 handler closures into make_handler factory in app.rs Extract make_handler<F, Fut>(state, f) -> impl Fn(RequestContext) -> BoxedHandlerFuture that owns the build_per_request_services + ctx.into_request() boilerplate. The routing table now reads as a flat list of (METHOD, PATH, handler-expr) triples instead of 10 named handler variables each spanning 7-9 lines. ~120 lines removed. * Resolve PR 725 round-1 review findings Route smoke tests — false positives via wildcard fallback: - Add registered_routes() helper using RouterService::routes() for introspection - Replace 9 individual assert_ne!(404) tests with all_explicit_routes_are_registered() which checks the route table directly; a removed explicit route now fails even when a wildcard fallback would have returned non-404 Cross-adapter parity tests — value equality not just presence: - admin_rotate_unauthenticated_parity: extract both WWW-Authenticate header values and assert they are equal; assert the shared value starts with Basic - admin_deactivate_unauthenticated_parity: same fix - geo_header_parity_on_all_responses: compare X-Geo-Info-Available values across adapters rather than only checking presence; catches true vs false divergence CI clippy: - Add --all-targets to the integration-tests clippy invocation so test targets (tests/parity.rs, tests/routes.rs) are actually linted * Fix Spin publisher HTML processor bypass caused by missing Host header Spin's WASI HTTP bridge does not surface the incoming Host header via IncomingRequest::headers() — the authority is only accessible through Spin's spin-full-url synthetic header. EdgeZero's into_core_request forwards all headers including spin-full-url, but the Host header is absent. extract_request_host() returns "" which causes classify_response_route to fall back to BufferedUnmodified, skipping the HTML processor entirely: no TSJS injection, no URL rewriting, no GTM proxying. Inject Host from spin-full-url in dispatch before routing, via a simple host_from_spin_url helper that strips the scheme and path. The fix is Spin-specific and does not touch shared publisher code. Also addresses PR review findings: remove build_per_request_services passthrough wrapper, reduce MAX_DECOMPRESSED_SIZE to 8 MiB, annotate streaming body limitation, expand validate_kid digit test, upgrade parity WWW-Authenticate assertions from presence to value equality, and document the anyhow exception at the WASM FFI boundary. * Resolve PR 735 round-1 review findings Blocking: - Reject keys not starting with a lowercase ASCII letter at the spin_variable_name encoder boundary; removes the aliasing-prone digit-leading n-prefix branch and fixes with_capacity to worst-case (key.len() * 4 + 3). Updates affected tests to assert rejection. - Document the no-configurable-outbound-timeout limitation in SpinPlatformHttpClient rustdoc; PlatformBackendSpec::first_byte_timeout is ignored by NoopBackend and Spin's http::send has no per-request timeout API. Non-blocking: - startup_error_router now logs the error and returns a generic "Service Unavailable" body so deployment state is not leaked to anonymous callers; adds PUT/DELETE handlers for all catch-all routes. - Collapse duplicate fp_sign_get_handler/fp_sign_post_handler into one closure + clone, matching the get_fallback/post_fallback pattern. - Add # Limitations rustdoc section to SpinSecretStoreAdapter covering UTF-8-only variables and plaintext-at-rest in the default manifest. Security (P1 — proxy-rebuild): - handle_first_party_proxy_rebuild now validates the tstoken on the original tsclick URL via reconstruct_and_validate_signed_target before applying any parameter mutations. Without this an attacker could submit an unsigned tsclick and mint valid click redirects to arbitrary URLs. - Enforce settings.proxy.allowed_domains on tsurl before issuing the new signed redirect. CI fix (P2): - Replace stale TRUSTED_SERVER__SYNTHETIC__SECRET_KEY env var with TRUSTED_SERVER__EDGE_COOKIE__SECRET_KEY in the Cloudflare integration build step; the previous key was ignored, leaving the artifact built with the default config value. * Add Spin adapter to public docs and README - README Quick Start: add cargo test-spin - README Development: replace broad workspace clippy with target-matched aliases and note why; add cargo test-spin to test list - architecture.md: mention Cloudflare/Spin in the high-level overview; add trusted-server-adapter-spin section with build/test/lint commands and known MVP limits; expand Runtime Targets table to include all four adapters; add note on target-matched clippy requirement * Fix proxy_rebuild_adds_and_removes_params test after tstoken validation Test was constructing a tsclick URL without a signature. Now that handle_first_party_proxy_rebuild validates the tstoken via reconstruct_and_validate_signed_target, the test must supply a properly signed click URL. Compute the token with compute_encrypted_sha256_token over tsurl + original params before building tsclick. * Resolve PR review findings: strengthen test assertions and CI gate - Fix auth_middleware test to use protected route (/admin/keys/rotate) with 401 + WWW-Authenticate assertion; /auction is not a protected route and cannot prove AuthMiddleware ran - Add all_explicit_routes_are_registered() to Axum adapter using RouterService introspection; status != 404 is a false positive when /{*rest} catch-all is registered - Add [lints.clippy] section to integration-tests/Cargo.toml (inlined from workspace) so clippy --all-targets actually gates test binaries; drop redundant =1.52.3 exact tokio pin (Cargo.lock already enforces it) - Fix two clippy::panic violations in parity.rs (unwrap_or_else(panic!)) and pre-existing doc_markdown violations surfaced by the new lint gate - Add top-level JSON key-set comparison in discovery_route_body_is_json_parity; is_some == is_some passes even when both adapters return different objects - Tighten MAX_GROWTH_FACTOR 2.0 → 1.1; observed growth is ≤1.01× and 2× would not catch double-injection or buffer-leak regressions * Fix clippy-spin-native failures in app.rs - Collapse nested if/if-let/if-let into a single let-chain condition (collapsible_if, Rust 2024 edition let chains) - Move host_from_spin_url test module to end of file so no items follow the test module (items_after_test_module) * Bound publisher response body size and document interim buffering Add MAX_PLATFORM_RESPONSE_BODY_BYTES (10 MiB) cap in fastly_response_to_platform to prevent heap exhaustion from large origin responses. Content-Length pre-check avoids the WASM heap copy entirely for declared-size responses; post-materialization check covers chunked responses without Content-Length. Document the interim buffering regression in PublisherResponse::Stream and PassThrough doc comments, noting that both variants now hold a fully materialised body until the platform HTTP client gains streaming response support in PR 15. Resolves PR 624 round-7 review findings. * Add edge_cookie module, test helpers, and integration handle signatures Complete merge resolution of remaining unstaged files: - lib.rs: expose edge_cookie as pub(crate) module (used by proxy.rs) - platform/test_support.rs: add recorded_request_headers() on StubHttpClient and build_services_with_http_client() factory for tests with custom HTTP clients - integrations/{datadome,didomi,lockr,permutive,prebid,sourcepoint}: add _services: &RuntimeServices parameter to handle() following main branch IntegrationProxy trait change * Fix remaining code review findings from merge Address medium-priority findings surfaced during review of the main merge: - test_support.rs: record request headers in send_async (was only recorded in send), so recorded_request_headers() is consistent across sync and async HTTP test paths - test_support.rs: document StubBackend vs NoopBackend distinction on build_services_with_http_client so callers understand the tradeoff - endpoints.rs: 413 PAYLOAD_TOO_LARGE response now includes Content-Type and a client-facing message instead of an empty body with a server-internal error string - kv.rs: derive Clone for KvIdentityGraph (trivial String wrapper) - main.rs: build KvIdentityGraph once and clone for finalize_kv_graph, eliminating the double call to maybe_identity_graph * Fix cargo fmt lint * Resolve PR 626 round-2 review findings - Fix orchestrator select() Err path: add failed_backend_name to PlatformSelectResult and populate it from fastly::SendError::backend_name() in the Fastly adapter; orchestrator now uses this field directly instead of the broken diff-against-remaining logic (which always failed on Fastly because remaining entries have no backend names) - Add push_select_error() to StubHttpClient and strip backend names from remaining entries in select() to match Fastly production behavior - Add select_error_is_attributed_to_correct_provider test: two stub providers, one fails via injected select error, assert correct attribution - Remove CONTENT_LENGTH from datadome headers_to_copy (body is re-materialized so client Content-Length is wrong for the outbound request) - Replace _request_info ignored param in prebid to_openrtb with request_info and remove the duplicate internal RequestInfo::from_request call - Replace from_utf8_lossy with strict from_utf8 + fallback in google_tag_manager - Replace .expect() on HeaderValue::from_str with if let Ok in adserver_mock * Resolve PR14 round-1 review findings - Fix trusted-server.toml comment: max_buffered_body_bytes defaults to 16 MiB when omitted and responses exceeding the cap return 500, not unbounded - Remove incorrect "null (unbounded)" language from Publisher settings doc - Add parity note to resolve_geo_for_response explaining intentional divergence from legacy path (EdgeZero skips geo for all 401s, not just AuthChallenge ones) - Add comment to dispatch_fallback clarifying integration-proxy responses bypass max_buffered_body_bytes (body already consumed upstream) - Replace dispatch_with_config_handle with app.router().oneshot() in edgezero_main to avoid the fastly::Response round-trip that used set_header and silently dropped duplicate header values (e.g. multiple Set-Cookie headers) - Add finalize_handle_preserves_duplicate_set_cookie_headers regression test verifying FinalizeResponseMiddleware does not collapse duplicate headers * Update integration-tests lock file to fix CI --locked failure * Update root workspace lock file to match integration-tests versions * Update integration-tests lock file for getrandom wasm dep and tokio dev-dep move * Finish tokio removal from trusted-server-core Convert all remaining #[tokio::test] tests to futures::executor::block_on across proxy.rs, publisher.rs, auction/endpoints.rs, auction/orchestrator.rs, and integrations/testlight.rs. Drop tokio from [dev-dependencies] in trusted-server-core/Cargo.toml — tokio is no longer referenced anywhere in the crate. Also fix two stale doc comments in proxy.rs that still referenced fastly::Response and platform_response_to_fastly. * Fix stale fastly::Body doc reference in platform/mod.rs * Remove fastly dependency from core via EcKvStore platform trait The PR15 merge reintroduced fastly into trusted-server-core through ec/kv.rs, ec/rate_limiter.rs, and an orphaned backend.rs, breaking native linking for the Axum dev server. - Delete orphaned core backend.rs (adapter already owns its copy) - Keep the RateLimiter trait in core; move FastlyRateLimiter and RATE_COUNTER_NAME to the Fastly adapter - Add the EcKvStore primitive trait (lookup with generation marker, conditional insert, prefix count, delete) in ec/kv_backend.rs - Rewrite KvIdentityGraph on top of EcKvStore so CAS retry loops, tombstone semantics, and validation stay in core - Implement FastlyEcKvStore in the adapter mapping onto the Fastly KV Store API - Add in-memory and failing test backends; cover create/revive, upsert, and tombstone paths natively * Fix adapter tests broken by strict placeholder-secret validation get_settings() now rejects the placeholder secrets baked into trusted-server.toml (hardened in PR14/15) instead of only logging warnings, so adapter tests that loaded baked settings began failing. - Build test settings from explicit TOML with non-placeholder values in the Axum middleware tests and the Fastly app tests - Add TrustedServerApp::routes_with_settings() so the Axum route integration tests drive the real router without the baked settings - Accept the designed 501 Not Implemented for admin key routes on the dev server; only an unhandled 500 fails the test * Address round-4 review findings on Axum dev server PR - Restore the publisher buffered-body size cap: reinstate BoundedWriter (now in core so both adapters share it) and enforce settings.publisher.max_buffered_body_bytes in buffer_publisher_response, with unit tests covering the cap - Fix backend-name misattribution in AxumPlatformHttpClient::select: futures::future::select_all uses swap_remove and makes no ordering guarantee for remaining futures, so positional index reconstruction could pair later auction responses with the wrong bidder. Each AxumPendingHandle now implements Future and resolves to its own backend name - Reorder reqwest after regex in workspace Cargo.toml - Drop redundant dev-dependency re-declarations (edgezero-adapter-axum, edgezero-core, reqwest) in the Axum adapter manifest * Update integration-tests lock file after fastly removal from core Removing the fastly dependency from trusted-server-core dropped its transitive wasm dependencies from the integration-tests dependency graph, leaving the crate's standalone Cargo.lock stale and failing the --locked check in check-integration-dependency-versions.sh. * Address Cloudflare adapter review findings and fix axum CI wasm target CI: - Install the wasm32-wasip1 target in the test-axum job — the 'Verify Fastly WASM release build' step added by PR16 builds for that target but the toolchain setup never installed it Blocking findings: - Strip stale Content-Length from decoded Workers fetch responses and set it from the decoded body length. The origin value describes the compressed payload while the Workers runtime auto-decompresses, so pass-through responses could be sent with a truncating length - Reject multi-provider auction fan-out before any request launches: add PlatformHttpClient::supports_concurrent_fanout() (default true), report false from CloudflareHttpClient whose send_async executes eagerly, and validate in the orchestrator before the launch loop. Previously the select()-time rejection fired only after every provider request had already run sequentially and spent the auction budget. The select()-time check remains as defense-in-depth. Regression test asserts rejection happens with zero requests sent Non-blocking findings: - Outbound request headers use Headers::append instead of set so duplicate header names forward every value, matching the response path - Replace the unreachable! panic in send_async with a typed PlatformError so an edgezero behavior change cannot panic a Worker - Replace bare unwrap() with expect("should ...") in platform tests per the testing conventions * Install wasm32-wasip1 target in test-axum CI job The 'Verify Fastly WASM release build' step builds for wasm32-wasip1 but the job's toolchain setup never installed that target, failing with: can't find crate for core. * Fix parity-crate clippy violations and bump worker for worker-build 0.8 The merged [lints.clippy] block from PR18 now applies to this branch's pre-existing integration test files: - Collapse the nested if in is_ec_cookie_expired into a let-chain - Replace redundant closures with serde_json::Value::as_u64 - Replace the denied panic! in the EC scenario loop with an assert! carrying the same diagnostic message CI's freshly installed worker-build ^0.8 requires wasm-bindgen 0.2.123 or newer, but the lock files held worker 0.8.3 with wasm-bindgen 0.2.121 (js-sys exact-pins its companion wasm-bindgen, so only the worker 0.8.4 bump moves it). Update worker in both the workspace and integration-tests lock files. * Resolve PR14 round-2 review findings - Document EdgeZero parity gaps: add a "Not yet ported (legacy-only)" section to the app.rs route inventory enumerating the missing EC API routes and EC identity lifecycle, referenced from fastly.toml and tracked in issue #495 - Register legacy /admin/keys/rotate and /admin/keys/deactivate aliases in NAMED_ROUTES with an auth-gating parity test - Make publisher.max_buffered_body_bytes a plain usize so the dead unbounded mode is unrepresentable; drop the dead None arm in resolve_publisher_response_buffered - Reword apply_finalize_headers expect() messages to the "should ..." convention - Clarify legacy_main doc comment: /health was already short-circuited before routing pre-PR14; only logger init moved after the probe - Minimize integration-tests Cargo.lock churn to just the edgezero rev pin update (reverts ~30 unrelated transitive bumps) * Port EC identity lifecycle and EC API routes to the EdgeZero path Resolves the PR14 P0 parity findings: the EdgeZero path previously used EcContext::default() with no KV graph or partner registry, omitted the EC API routes, and never ran ec_finalize_response or pull sync. The EC subsystem landed on main (#621) after this branch's EdgeZero wiring was written, so the merge from PR13 stubbed the new EC-aware signatures. - Register POST /_ts/api/v1/batch-sync and GET/OPTIONS /_ts/api/v1/identify as named routes mirroring the legacy arms (identity graph, partner registry, rate limiter, CORS) - Add build_ec_request_state reproducing the legacy pre-routing prelude: device signals, bot gate, ts-eids/sharedid cookie capture, geo lookup, EcContext creation, and KV-graph gating - Wire EcContext, KvIdentityGraph, and PartnerRegistry into handle_auction and integration proxy dispatch; generate EC IDs for browser navigations before the publisher fallback - Thread EcFinalizeState through response extensions so edgezero_main runs ec_finalize_response and the pull-sync hook on the converted fastly response, mirroring legacy_main - Derive Clone on EcContext (required for http response extensions) - Document the remaining intentional deviations (401 auth challenges skip EC finalize, buffered streaming, router-level 405s) in the app.rs module docs and update the fastly.toml flag comment - Add parity tests proving the EC API routes never reach the publisher fallback and that responses carry EcFinalizeState * Align shared dependency versions in integration-tests lock file The shared integration build environment check requires direct dependencies shared between the workspace and the integration-tests crate to resolve to the same versions. Bump log (0.4.29 -> 0.4.32) and serde_json (1.0.149 -> 1.0.150) to match the workspace lock file, which picked them up with the edgezero rev update. Verified locally with scripts/check-integration-dependency-versions.sh. * Resolve PR15 re-review findings Blocking: - Restore the non-panicking '/' fallback in from_fastly_request: a URL Fastly accepts but http::Uri rejects degrades with a warning instead of aborting the Wasm instance. Adds a regression test using a >65534-byte URL (http::Uri's length limit; url::Url has none) - Deduplicate the EC ID generator: edge_cookie.rs delegates to the canonical ec::generation normalize_ip + generate_ec_id, removing the IPv6 /64 normalization divergence that minted non-correlating identity prefixes. Adds a test asserting both paths hash to the same prefix - Wire consent KV read-fallback and write-on-change into build_consent_context: loads persisted consent when the request carries no signals (jurisdiction re-derived from current geo) and persists cookie-sourced consent after the context is built. Adds three pipeline-level tests with an in-memory KV store Non-blocking: - git rm the orphaned trusted-server-core/src/backend.rs (no longer compiled since lib.rs dropped the module), collapsing the duplicated DEFAULT_FIRST_BYTE_TIMEOUT to the platform definition - Restore the two lgtm[rust/cleartext-logging] suppressions in publisher.rs — CodeQL still runs in CI (.github/workflows/codeql.yml) - Wrap prebid backend resolution errors in TrustedServerError::Auction with the endpoint, matching aps and adserver_mock attribution - Log a warning when an EC Set-Cookie header value is rejected instead of silently no-opping (set_ec_cookie and expire_ec_cookie) - Replace the inline block_on(select(vec![...])) + .ready in pull sync with the equivalent http_client().wait() helper * Document why trusted-server-core still depends on fastly The EC KV identity graph requires compare-and-swap semantics (InsertMode::Add and generation preconditions) that the EdgeZero KvStore trait does not expose, and the rate limiter uses fastly::erl, which has no EdgeZero abstraction. Removing the dependency is deferred until EdgeZero grows equivalent APIs; note this on the manifest entry so the deferral is discoverable. * Resolve PR16 round-5 review findings Blocking: - Attribute failed auction providers on the Axum select error path: failed_backend_name now carries the backend whose request failed, matching the Fastly adapter, so the orchestrator removes the provider and records BidStatus::Error instead of dropping it through the "backend not identified" branch. Adds a select-level regression test (request to a closed port) asserting the attribution - Migrate the remaining live dev-tooling files to the per-target aliases: /check-ci, /verify, /test-all, and the build-validator agent now use cargo {build,clippy,test}-{fastly,axum}. New build-* and check-* aliases added to .cargo/config.toml; CLAUDE.md's bare cargo build / cargo check examples updated to the aliases Non-blocking: - Abort dropped auction tasks: AxumPendingHandle aborts its JoinHandle on drop so deadline-dropped bidders stop instead of running up to the 30s transport timeout in the background - Log buffered upstream response sizes on both the send and send_async paths so a large upstream is visible in dev instead of silently growing the heap - Drop the redundant double error-wrapping in count_hash_prefix_keys and delete — the backend already attaches store context - apply_image_passthrough_metadata returns () instead of a bool both call sites discarded - axum and tower dev-dependencies moved to workspace pins - Admin route tests assert the fixed 501 contract instead of != 404, and CAS-conflict paths are now covered: a conflict-injecting EcKvStore test backend exercises the retry-then-succeed, concurrent-revive short-circuit, and retry-exhaustion arms of create_or_revive * Extend per-target build/check aliases to the Cloudflare adapter The build-fastly/check-fastly aliases merged from PR16 predate the Cloudflare adapter and did not exclude it, so they compiled the wasm32-unknown-unknown crate for wasm32-wasip1. Add the exclusion (matching test-fastly/clippy-fastly), add a build-cloudflare alias mirroring check-cloudflare, and update every dev-tooling doc that lists the per-target commands: /check-ci, /verify, /test-all, the build-validator agent, CLAUDE.md, AGENTS.md, and README.md now include the cloudflare clippy/test/build/check steps alongside fastly and axum. All aliases verified locally (check-fastly, check-axum, check-cloudflare, build-cloudflare). * Strip internal fastly-ssl signal from publisher origin requests The EdgeZero entry point re-injects fastly-ssl from trusted Fastly TLS metadata so in-process scheme detection works after header sanitization. The publisher fallback forwarded the client request verbatim, leaking this internal edge signal to publisher backends — the legacy path never re-added it. Strip the header after RequestInfo extracts the scheme and before the outbound send, restoring legacy outbound-header parity. Add a regression test asserting the header is not forwarded. Also clarify the max_buffered_body_bytes doc comment: the effective ceiling for a publisher page on Fastly is bounded by the platform HTTP client's 10 MiB raw-body cap, which runs before this decoded-output buffer. Raising the value only helps highly compressible pages whose decoded size exceeds the 16 MiB default while their compressed origin body stays under 10 MiB. * Fix clippy violations in integration-tests crate The new crate-wide [lints.clippy] block plus clippy --all-targets -D warnings lints pre-existing test code for the first time. Resolve the four violations the new parity CI gate surfaces: - collapsible_if in common/ec.rs - redundant_closure_for_method_calls in frameworks/scenarios.rs (x2) - panic in integration.rs (replace unwrap_or_else+panic with assert) * Address Phase 5 verification review findings Resolve the test-fidelity gaps from the round-4 review: - Register canonical /_ts/admin/keys/* routes on the Axum and Cloudflare adapters (matching Settings::ADMIN_ENDPOINTS and the Fastly adapter), and switch the route/parity tests to a production-shaped ^/_ts/admin handler regex. The auth-gating assertions now target the canonical paths that production config actually protects; legacy /admin/keys/* aliases are kept for parity and documented as reaching the handler directly. - Assert cross-adapter status equality (and non-404 routing) for /auction instead of only checking each adapter does not 401. - Strengthen the discovery JSON parity check to compare the full parsed bodies rather than mere JSON-parsability. - Use expect("should ...") in build.rs instead of unwrap_or_else+panic. * Derive trusted host and scheme from spin-full-url on the Spin adapter The Spin fallback dispatch reconstructed only the Host header from the trusted spin-full-url and left client-supplied Forwarded/X-Forwarded-* headers intact. Because build_runtime_services sets no TLS signal, detect_request_scheme had no trusted scheme source and fell back to http, or to a client-spoofed scheme/host, which publisher HTML rewriting, integration URL rewriting, and request-signing context then consumed. Strip the spoofable forwarded headers (mirroring the Fastly/Axum edge sanitization), then inject both the trusted Host and the trusted scheme parsed from spin-full-url. Extracted the logic into apply_trusted_host_and_scheme and added unit tests covering spoofed X-Forwarded-* headers and Host preservation. * Route all publisher fallback methods on the Spin adapter The Spin route table only registered GET and POST for / and /{*rest} and only each named route's primary method, so HEAD /, OPTIONS /page (CORS preflight), HEAD /first-party/proxy, and similar requests returned a router-level 405 instead of reaching the publisher/integration fallback like Fastly and Axum. Mirror the Fastly/Axum publisher_fallback_methods() pattern: register GET, POST, HEAD, OPTIONS, PUT, PATCH, and DELETE for the catch-all and for every named path's non-primary methods, all routed through the shared fallback. Dynamic /static/tsjs= handling stays GET-only (now gated inside dispatch on the request method). Added route tests for HEAD /, OPTIONS, and a non-primary method on a named path; updated the PUT /verify-signature test to assert fall-through. * Reject deleting reserved signing params during proxy-rebuild handle_first_party_proxy_rebuild validated the original tstoken but then let payload.del/add mutate any parameter, including the reserved tsexp replay bound that handle_first_party_proxy_sign attaches. A public rebuild request could take a still-valid expiring /first-party/click URL, delete tsexp, and receive a freshly signed click URL that reconstruct_and_validate_signed_target accepts indefinitely (expiration is optional). Treat tsexp, tstoken, and tsurl as reserved: reject del/add for them so the bounded expiration is preserved and re-signed. Added regression tests asserting deletion of tsexp is rejected and that a normal rebuild retains the tsexp bound. * Keep legacy digit-leading KIDs removable via the admin API The digit-leading KID rejection (added to block Spin variable-encoder aliasing) was applied through validate_kid to every caller, including handle_deactivate_key. Deployments that created operator-supplied digit-leading KIDs under the previous validation rules could no longer deactivate or delete them, since the request was rejected before storage was touched. Split validation: validate_kid_format enforces the structural constraints (length, charset) and validate_kid layers the digit-leading rejection on top for creation/rotation. Deactivation and deletion now use validate_kid_format so legacy digit-leading KIDs remain removable. Added tests covering both paths. * Pin worker-build to the locked worker version in the Cloudflare build The Cloudflare build installed worker-build with a floating `^0.8`, which began resolving to worker-build 0.8.5. That release passes `--force-enable-abort-handler` to wasm-bindgen, but worker-build downloads the wasm-bindgen CLI matching the locked `wasm-bindgen` (0.2.123, pulled by worker 0.8.4), and that CLI does not recognise the flag — so the integration-test Cloudflare bundle build failed with "unexpected argument '--force-enable-abort-handler'". Derive the worker crate version from Cargo.lock and pin worker-build to it. worker-build is released in lockstep with worker, so this keeps the build tool and the wasm-bindgen CLI it drives compatible, and the pin advances automatically when the lockfile bumps worker. Verified locally: build.sh installs worker-build 0.8.4 and produces build/index.js without error. * Reconstruct absolute request URIs for Spin first-party handlers Spin builds the core request URI from IncomingRequest::path_with_query(), so it is path-only (e.g. "/first-party/proxy?..."). The shared first-party proxy/click/sign handlers parse req.uri() with url::Url::parse, which rejected the relative path with "Invalid URL" — breaking the creative first-party proxy flow, click redirects, and GET sign query parsing on Spin. Rename apply_trusted_host_and_scheme to normalize_spin_request and have it rebuild an absolute URI from the trusted spin-full-url scheme+host, then apply it to every named first-party handler (proxy, click, sign GET+POST, proxy-rebuild) in addition to the publisher fallback. Also build the startup error router from the full publisher fallback method set so HEAD, OPTIONS, and PATCH on "/" and nested paths return the generic 503 instead of a router-level 405, matching the healthy router. Add regression tests: a signed proxy round-trip and GET sign query parse through the Spin router, plus a startup-fallback method-coverage unit test. * Enforce a portable KID contract across signing adapters validate_kid now requires a lowercase ASCII leading character, matching the strictest platform key-name encoder (the Spin variable encoder, which rejects uppercase- and punctuation-leading KIDs and aliases digit-leading ones). A KID minted on any adapter must be storable on every backend, so create/rotate validates against the strictest common denominator and returns a client- correctable 400 instead of a runtime 5xx on Spin. Deactivation keeps the looser format check so legacy KIDs stay removable. …
1 parent 8e099ec commit c412735

32 files changed

Lines changed: 6681 additions & 538 deletions

File tree

.cargo/config.toml

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,42 @@
22
# trusted-server-adapter-fastly → wasm32-wasip1 (Fastly Compute)
33
# trusted-server-adapter-axum → native (dev server)
44
# trusted-server-adapter-cloudflare → wasm32-unknown-unknown (Cloudflare Workers)
5+
# trusted-server-adapter-spin → wasm32-wasip1 (Fermyon Spin)
56
#
67
# All adapters are workspace members so `-p` resolves each.
78
# default-members = [fastly] — required so Viceroy can locate the binary via `cargo run --bin`.
89
# Use the aliases below to target each adapter with the correct toolchain.
910

1011
[alias]
1112
# Fastly adapter + shared crates (wasm32-wasip1 via Viceroy)
12-
# Excludes Axum (native-only) and Cloudflare (wasm32-unknown-unknown, separate job)
13-
test-fastly = ["test", "--workspace", "--exclude", "trusted-server-adapter-axum", "--exclude", "trusted-server-adapter-cloudflare", "--target", "wasm32-wasip1"]
13+
# Excludes Axum (native-only), Cloudflare (wasm32-unknown-unknown), and Spin (separate wasm32-wasip1 job)
14+
test-fastly = ["test", "--workspace", "--exclude", "trusted-server-adapter-axum", "--exclude", "trusted-server-adapter-cloudflare", "--exclude", "trusted-server-adapter-spin", "--target", "wasm32-wasip1"]
1415
# Axum dev server adapter (native)
1516
test-axum = ["test", "-p", "trusted-server-adapter-axum"]
1617
# Cloudflare adapter (native host; WASM target checked separately in CI)
1718
test-cloudflare = ["test", "-p", "trusted-server-adapter-cloudflare"]
1819
# Cloudflare adapter WASM target check (wasm32-unknown-unknown requires --features cloudflare)
1920
check-cloudflare = ["check", "-p", "trusted-server-adapter-cloudflare", "--target", "wasm32-unknown-unknown", "--features", "cloudflare"]
21+
# Spin adapter (native host tests; WASM target checked separately with --features spin)
22+
test-spin = ["test", "-p", "trusted-server-adapter-spin"]
23+
check-spin = ["check", "-p", "trusted-server-adapter-spin", "--target", "wasm32-wasip1", "--features", "spin"]
2024

2125
# Clippy — target-matched to avoid cross-target compile failures
22-
clippy-fastly = ["clippy", "--workspace", "--exclude", "trusted-server-adapter-axum", "--exclude", "trusted-server-adapter-cloudflare", "--all-targets", "--all-features", "--target", "wasm32-wasip1", "--", "-D", "warnings"]
26+
clippy-fastly = ["clippy", "--workspace", "--exclude", "trusted-server-adapter-axum", "--exclude", "trusted-server-adapter-cloudflare", "--exclude", "trusted-server-adapter-spin", "--all-targets", "--all-features", "--target", "wasm32-wasip1", "--", "-D", "warnings"]
2327
clippy-axum = ["clippy", "-p", "trusted-server-adapter-axum", "--all-targets", "--all-features", "--", "-D", "warnings"]
2428
# No --all-features: the `cloudflare` feature has a compile_error! guard on
2529
# non-wasm32 targets. WASM-feature coverage comes from `cargo check-cloudflare`.
2630
clippy-cloudflare = ["clippy", "-p", "trusted-server-adapter-cloudflare", "--all-targets", "--", "-D", "warnings"]
31+
clippy-spin-native = ["clippy", "-p", "trusted-server-adapter-spin", "--all-targets", "--", "-D", "warnings"]
32+
clippy-spin-wasm = ["clippy", "-p", "trusted-server-adapter-spin", "--target", "wasm32-wasip1", "--features", "spin", "--lib", "--", "-D", "warnings"]
2733

2834
# Build — target-matched, same split as test/clippy
29-
build-fastly = ["build", "--workspace", "--exclude", "trusted-server-adapter-axum", "--exclude", "trusted-server-adapter-cloudflare", "--target", "wasm32-wasip1"]
35+
build-fastly = ["build", "--workspace", "--exclude", "trusted-server-adapter-axum", "--exclude", "trusted-server-adapter-cloudflare", "--exclude", "trusted-server-adapter-spin", "--target", "wasm32-wasip1"]
3036
build-axum = ["build", "-p", "trusted-server-adapter-axum"]
3137
build-cloudflare = ["build", "-p", "trusted-server-adapter-cloudflare", "--target", "wasm32-unknown-unknown", "--features", "cloudflare"]
3238

3339
# Check — fast compile validation, same split
34-
check-fastly = ["check", "--workspace", "--exclude", "trusted-server-adapter-axum", "--exclude", "trusted-server-adapter-cloudflare", "--target", "wasm32-wasip1"]
40+
check-fastly = ["check", "--workspace", "--exclude", "trusted-server-adapter-axum", "--exclude", "trusted-server-adapter-cloudflare", "--exclude", "trusted-server-adapter-spin", "--target", "wasm32-wasip1"]
3541
check-axum = ["check", "-p", "trusted-server-adapter-axum"]
3642

3743
[target.'cfg(all(target_arch = "wasm32"))']

.github/actions/setup-integration-test-env/action.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ runs:
9898
env:
9999
TRUSTED_SERVER__PUBLISHER__ORIGIN_URL: http://127.0.0.1:${{ inputs.origin-port }}
100100
TRUSTED_SERVER__PUBLISHER__PROXY_SECRET: integration-test-proxy-secret
101-
TRUSTED_SERVER__EDGE_COOKIE__SECRET_KEY: integration-test-secret-key
101+
TRUSTED_SERVER__EC__PASSPHRASE: integration-test-ec-secret-padded-32
102102
TRUSTED_SERVER__PROXY__CERTIFICATE_CHECK: "false"
103103
run: cargo build -p trusted-server-adapter-axum
104104

@@ -129,6 +129,6 @@ runs:
129129
env:
130130
TRUSTED_SERVER__PUBLISHER__ORIGIN_URL: http://127.0.0.1:${{ inputs.origin-port }}
131131
TRUSTED_SERVER__PUBLISHER__PROXY_SECRET: integration-test-proxy-secret
132-
TRUSTED_SERVER__SYNTHETIC__SECRET_KEY: integration-test-secret-key
132+
TRUSTED_SERVER__EC__PASSPHRASE: integration-test-ec-secret-padded-32
133133
TRUSTED_SERVER__PROXY__CERTIFICATE_CHECK: "false"
134134
run: bash crates/trusted-server-adapter-cloudflare/build.sh

.github/workflows/format.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ jobs:
4141
- name: Run cargo clippy (Cloudflare — native)
4242
run: cargo clippy-cloudflare
4343

44+
- name: Run cargo clippy (Spin — native)
45+
run: cargo clippy-spin-native
46+
47+
- name: Run cargo clippy (Spin — wasm32-wasip1)
48+
run: cargo clippy-spin-wasm
49+
4450
format-typescript:
4551
runs-on: ubuntu-latest
4652
defaults:

.github/workflows/test.yml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,45 @@ jobs:
114114
- name: Run Cloudflare adapter tests (native host)
115115
run: cargo test-cloudflare
116116

117+
test-spin:
118+
name: cargo check/build/test (spin native + wasm32-wasip1)
119+
runs-on: ubuntu-latest
120+
steps:
121+
- uses: actions/checkout@v4
122+
123+
- name: Retrieve Rust version
124+
id: rust-version
125+
run: echo "rust-version=$(grep '^rust ' .tool-versions | awk '{print $2}')" >> $GITHUB_OUTPUT
126+
shell: bash
127+
128+
- name: Set up Rust toolchain (native + wasm32-wasip1)
129+
uses: actions-rust-lang/setup-rust-toolchain@v1
130+
with:
131+
toolchain: ${{ steps.rust-version.outputs.rust-version }}
132+
target: wasm32-wasip1
133+
cache-shared-key: cargo-${{ runner.os }}
134+
135+
- name: Check Spin adapter (native host)
136+
run: cargo check -p trusted-server-adapter-spin
137+
138+
- name: Check Spin adapter (wasm32-wasip1)
139+
run: cargo check-spin
140+
141+
- name: Build Spin adapter release WASM
142+
# Mirror the Fastly release-build overrides so the artifact embeds usable
143+
# settings instead of the trusted-server.toml placeholders, which startup
144+
# rejects (falling back to the generic 503 router). This proves the
145+
# documented Spin artifact can boot, not just compile.
146+
env:
147+
TRUSTED_SERVER__PUBLISHER__ORIGIN_URL: http://127.0.0.1:8080
148+
TRUSTED_SERVER__PUBLISHER__PROXY_SECRET: integration-test-proxy-secret
149+
TRUSTED_SERVER__EC__PASSPHRASE: integration-test-ec-secret-padded-32
150+
TRUSTED_SERVER__PROXY__CERTIFICATE_CHECK: "false"
151+
run: cargo build --package trusted-server-adapter-spin --target wasm32-wasip1 --features spin --release
152+
153+
- name: Run Spin adapter tests (native host)
154+
run: cargo test-spin
155+
117156
test-parity:
118157
name: cargo test (cross-adapter parity)
119158
runs-on: ubuntu-latest

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
# Auto-generated JS module index
1313
**/src/generated-modules.ts
1414

15+
# local Spin CLI downloads
16+
/spin
17+
/spin.sig
18+
1519
# EdgeZero local KV store (created by edgezero-adapter-axum framework)
1620
.edgezero/
1721

CLAUDE.md

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ crates/
1717
trusted-server-adapter-fastly/ # Fastly Compute entry point (wasm32-wasip1 binary)
1818
trusted-server-adapter-axum/ # Axum dev server entry point (native binary)
1919
trusted-server-adapter-cloudflare/ # Cloudflare Workers entry point (wasm32-unknown-unknown binary)
20+
trusted-server-adapter-spin/ # Fermyon Spin entry point (wasm32-wasip1 component)
2021
trusted-server-js/ # TypeScript/JS build — per-integration IIFE bundles
2122
lib/ # TS source, Vitest tests, esbuild pipeline
2223
```
@@ -68,6 +69,21 @@ cargo check-cloudflare
6869

6970
# Test Cloudflare adapter (native host)
7071
cargo test-cloudflare
72+
73+
# Check Spin adapter (native)
74+
cargo check -p trusted-server-adapter-spin
75+
76+
# Check Spin adapter (WASM target)
77+
cargo check-spin
78+
79+
# Test Spin adapter (native host)
80+
cargo test-spin
81+
82+
# Production-style Spin WASM artifact used by crates/trusted-server-adapter-spin/spin.toml
83+
cargo build --package trusted-server-adapter-spin --target wasm32-wasip1 --features spin --release
84+
85+
# Optional local Spin runtime smoke, if the Spin CLI is installed
86+
spin up --from crates/trusted-server-adapter-spin
7187
```
7288

7389
### Testing & Quality
@@ -78,12 +94,20 @@ cargo test-cloudflare
7894
cargo test-fastly # Fastly adapter + core (wasm32-wasip1 via Viceroy)
7995
cargo test-axum # Axum dev server adapter (native)
8096
cargo test-cloudflare # Cloudflare Workers adapter (native host)
97+
cargo test-spin # Spin adapter route tests (native host)
8198

8299
# Format
83100
cargo fmt --all -- --check
84101

85-
# Lint
86-
cargo clippy-fastly && cargo clippy-axum && cargo clippy-cloudflare
102+
# Lint by adapter target — target-matched clippy is the blocking gate because
103+
# the workspace has multiple wasm runtimes and runtime-specific SDKs. A plain
104+
# `cargo clippy --workspace --all-features` would trip the cloudflare
105+
# feature's non-wasm32 compile_error! guard.
106+
cargo clippy-fastly
107+
cargo clippy-axum
108+
cargo clippy-cloudflare
109+
cargo clippy-spin-native
110+
cargo clippy-spin-wasm
87111

88112
# Check compilation (per-target aliases — bare `cargo check` fails at the workspace root)
89113
cargo check-fastly && cargo check-axum && cargo check-cloudflare
@@ -175,6 +199,7 @@ pub struct UserId(Uuid);
175199
## Error Handling
176200

177201
- Use `error-stack` (`Report<MyError>`) — not anyhow or eyre.
202+
- **Exception**: `crates/trusted-server-adapter-spin/src/lib.rs` entry point returns `anyhow::Result` because `edgezero_adapter_spin::run_app` forces this type at the WASM FFI boundary. Do not use `anyhow` anywhere else.
178203
- Use `Box<dyn Error>` only in tests or prototyping.
179204
- Use concrete error types with `Report<E>`.
180205
- Use `ensure!()` / `bail!()` macros for early returns.
@@ -298,11 +323,12 @@ IntegrationRegistration::builder(ID)
298323
Every PR must pass:
299324

300325
1. `cargo fmt --all -- --check`
301-
2. `cargo clippy-fastly && cargo clippy-axum && cargo clippy-cloudflare`
302-
3. `cargo test-fastly && cargo test-axum && cargo test-cloudflare`
303-
4. JS build and test (`cd crates/trusted-server-js/lib && npx vitest run`)
304-
5. JS format (`cd crates/trusted-server-js/lib && npm run format`)
305-
6. Docs format (`cd docs && npm run format`)
326+
2. `cargo clippy-fastly && cargo clippy-axum && cargo clippy-cloudflare && cargo clippy-spin-native && cargo clippy-spin-wasm`
327+
3. `cargo test-fastly && cargo test-axum && cargo test-cloudflare && cargo test-spin`
328+
4. `cargo test --manifest-path crates/trusted-server-integration-tests/Cargo.toml --test parity`
329+
5. JS build and test (`cd crates/trusted-server-js/lib && npx vitest run`)
330+
6. JS format (`cd crates/trusted-server-js/lib && npm run format`)
331+
7. Docs format (`cd docs && npm run format`)
306332

307333
---
308334

@@ -312,7 +338,8 @@ Every PR must pass:
312338
2. **Get approval** — for non-trivial changes, present a plan first.
313339
3. **Implement incrementally** — small, testable changes. Every change should
314340
impact as little code as possible.
315-
4. **Test after every change**`cargo test-fastly && cargo test-axum`.
341+
4. **Test after every change** — run the target-matched adapter test for the
342+
code you changed; before PR handoff, run the full CI gate list above.
316343
5. **Explain as you go** — describe what you changed and why.
317344
6. **If blocked** — explain what's blocking and why.
318345

0 commit comments

Comments
 (0)