Skip to content

Add trusted-server-adapter-cloudflare crate (PR 17) - #644

Merged
prk-Jr merged 30 commits into
feature/edgezero-pr16-axum-dev-serverfrom
feature/edgezero-pr17-cloudflare-adapter
Jun 17, 2026
Merged

Add trusted-server-adapter-cloudflare crate (PR 17)#644
prk-Jr merged 30 commits into
feature/edgezero-pr16-axum-dev-serverfrom
feature/edgezero-pr17-cloudflare-adapter

Conversation

@prk-Jr

@prk-Jr prk-Jr commented Apr 18, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Add `trusted-server-adapter-cloudflare` crate — a Cloudflare Workers entry point using `#[event(fetch)]` and `edgezero_adapter_cloudflare::run_app`. Implements `Hooks` with full route parity to the Fastly and Axum adapters including admin key routes, auction, first-party proxy, and publisher fallback.
  • Add `CloudflareHttpClient` (wasm32 only) backed by `worker::Fetch` so outbound proxy requests actually reach the origin on the Workers runtime. Strips `content-encoding`/`transfer-encoding` headers since the Workers runtime auto-decompresses responses, preventing a double-decompress error in the proxy layer.
  • Wire real Cloudflare store implementations in `platform.rs` via edgezero handles injected by `run_app` — reducing `#[cfg(target_arch = "wasm32")]` blocks from 5 to 2:
    • Config: `ConfigStoreHandleAdapter` bridges `ctx.config_store()` to `PlatformConfigStore` — reuses the already-parsed `TRUSTED_SERVER_CONFIG` JSON handle.
    • KV: `KvHandleAdapter` bridges `ctx.kv_handle()` to `PlatformKvStore` — reuses the `env.kv()` handle opened by `run_app`.
    • Secrets: `CloudflareSecretStoreAdapter` calls `env.secret()` synchronously (still `#[cfg]` — `SecretHandle::get_bytes` is async while `PlatformSecretStore::get_bytes` is sync).
    • Geo: `CloudflareGeo` reads `cf-ipcountry`, `cf-ipcity`, `cf-ipcontinent`, `cf-iplatitude`, `cf-iplongitude` — no `#[cfg]` needed, degrades gracefully on native.
  • Replace `std::time::Instant` with `web_time::Instant` in the auction orchestrator and audit remaining `std::time::SystemTime` usages in `proxy.rs` and `signing.rs` — `std::time` panics on `wasm32-unknown-unknown` (Cloudflare), while `web_time` works on all three targets.

Changes

File Change
`crates/trusted-server-adapter-cloudflare/src/lib.rs` New — `#[event(fetch)]` entry point, cfg-gated for wasm32
`crates/trusted-server-adapter-cloudflare/src/app.rs` New — `TrustedServerApp` implementing `Hooks`, full route table
`crates/trusted-server-adapter-cloudflare/src/platform.rs` New — real store adapters (`ConfigStoreHandleAdapter`, `KvHandleAdapter`, `CloudflareSecretStoreAdapter`, `CloudflareGeo`), `CloudflareHttpClient` (wasm32), `extract_client_ip` + 7 unit tests
`crates/trusted-server-adapter-cloudflare/build.sh` New — NVM + `.env` sourcing, `cd SCRIPT_DIR` guard, worker-build 0.7
`crates/trusted-server-adapter-cloudflare/Cargo.toml` New — crate manifest with wasm32 target deps
`crates/trusted-server-adapter-cloudflare/wrangler.toml` New — wrangler config; uses `--cwd` DX so `wrangler dev` runs from workspace root
`crates/trusted-server-adapter-cloudflare/cloudflare.toml` New — edgezero manifest declaring `TRUSTED_SERVER_CONFIG`, `TRUSTED_SERVER_KV`, secrets bindings
`crates/trusted-server-adapter-cloudflare/.gitignore` Add `build/` and `.dev.vars` (wrangler convention for local secrets)
`crates/trusted-server-adapter-cloudflare/tests/routes.rs` New — host-target smoke test: `routes()` builds without panic
`crates/trusted-server-core/src/auction/orchestrator.rs` `std::time::Instant` → `web_time::Instant`
`crates/trusted-server-core/src/proxy.rs` `std::time::SystemTime` → `web_time::SystemTime` (import + test)
`crates/trusted-server-core/src/request_signing/signing.rs` `std::time::SystemTime` → `web_time::SystemTime` (production + test)
`crates/trusted-server-core/src/consent/mod.rs` `std::time::SystemTime` → `web_time::SystemTime`
`crates/trusted-server-core/src/platform/http.rs` Add `UnavailableHttpClient` (shared stub for platforms without outbound HTTP)
`Cargo.toml` Add `web-time`, `js-sys`, `bytes` to workspace deps
`.github/workflows/test.yml` Add CI jobs: native check + wasm32-unknown-unknown check for Cloudflare adapter
`crates/integration-tests/tests/environments/cloudflare.rs` New — `CloudflareWorkers` integration test environment
`crates/integration-tests/tests/environments/mod.rs` Export `cloudflare` environment module
`crates/integration-tests/tests/integration.rs` Add Cloudflare integration test cases (ignored pending wrangler dev)
`.vscode/tasks.json` Add "cloudflare dev server" task using `wrangler dev --cwd` from workspace root
`CLAUDE.md` Document Cloudflare adapter build/check/test commands

Closes

Closes #498

Test plan

  • `cargo test --workspace` (excluding Fastly adapter — Fastly WASM ABI not linkable on native host, pre-existing)
  • `cargo clippy --workspace --all-targets --all-features -- -D warnings`
  • `cargo fmt --all -- --check`
  • JS tests: `cd crates/js/lib && npx vitest run`
  • JS format: `cd crates/js/lib && npm run format`
  • Docs format: `cd docs && npm run format`
  • WASM build: `cargo check -p trusted-server-adapter-cloudflare --target wasm32-unknown-unknown`
  • Manual testing via `wrangler dev` — proxy reaches origin, JS static routes serve correctly, no double-decompress errors
  • Native host-target tests: `cargo test -p trusted-server-adapter-cloudflare` (7 unit + 1 smoke test pass)

Checklist

  • Changes follow CLAUDE.md conventions
  • No `unwrap()` in production code — use `expect("should ...")`
  • Uses `log` macros (not `println!`)
  • New code has tests
  • No secrets or credentials committed

Known deferred items

  • Secret store writes (`create`/`delete`) are not supported on Cloudflare Workers — `/admin/keys/rotate` and `/admin/keys/deactivate` read and write keys via `PlatformConfigStore`/`PlatformSecretStore`. Reads work via `CloudflareSecretStoreAdapter`; write-back would require a Cloudflare management API call outside the Workers runtime.

prk-Jr added 5 commits April 17, 2026 13:58
…ator

wasm32-unknown-unknown (Cloudflare Workers) does not support
std::time::Instant — it panics at runtime. web_time::Instant is a
zero-cost drop-in on native and JS-backed on WASM.
Implements the Cloudflare Workers adapter following the same pattern as
trusted-server-adapter-axum: TrustedServerApp implements the Hooks trait,
platform services use noop stubs on native (CI-compilable), and the
#[event(fetch)] entry point delegates to edgezero_adapter_cloudflare::run_app.

Also adds UnavailableHttpClient to trusted-server-core platform module,
parallel to the existing UnavailableKvStore.
CI: test-cloudflare job checks native host compile, wasm32-unknown-unknown
compile (with cloudflare feature), and runs host-target unit tests.
CLAUDE.md: add cloudflare crate to workspace layout and build commands.
…un_app

The rev (38198f9) of edgezero used in this workspace requires worker 0.7
(not 0.6) and run_app() takes a manifest_src: &str as first argument.
Updated Cargo.toml and lib.rs accordingly.
- Implement CloudflareHttpClient (wasm32 only) using worker::Fetch for real
  outbound proxy requests; strip content-encoding/transfer-encoding headers
  since the Workers runtime auto-decompresses responses
- Add build.sh with cd-to-SCRIPT_DIR guard so worker-build always runs from
  the correct crate root regardless of invocation directory
- Switch wrangler dev task to use --cwd from workspace root (same DX as Fastly)
- Add js-sys to workspace dependencies; reference it via { workspace = true }
- Fix #[ignore] messages on Cloudflare integration tests
- Replace std::time::{SystemTime,UNIX_EPOCH} with web_time in test code for
  signing.rs and proxy.rs (consistency with production paths)
- Add NoopConfigStore/NoopSecretStore TODO comment tracking the gap
- Add extract_client_ip unit tests (parses cf-connecting-ip, absent, invalid)
- Remove empty crate_compiles_on_host_target test
- Add CloudflareHttpClient timeout doc noting Workers CPU-budget tradeoff
@prk-Jr prk-Jr self-assigned this Apr 18, 2026
@prk-Jr
prk-Jr marked this pull request as draft April 18, 2026 13:08
prk-Jr added 3 commits April 18, 2026 19:25
Replace direct worker::Env store construction with edgezero handles
already injected by run_app, reducing #[cfg(target_arch = "wasm32")]
blocks from 5 to 2.

- ConfigStoreHandleAdapter: bridges ctx.config_store() to
  PlatformConfigStore — reuses the already-parsed TRUSTED_SERVER_CONFIG
  JSON handle rather than re-parsing it on every request
- KvHandleAdapter: bridges ctx.kv_handle() to PlatformKvStore — reuses
  the env.kv() handle opened by run_app rather than opening a new one
- CloudflareGeo: moved outside #[cfg]; reads cf-ipcountry and related
  headers via ctx.request().headers() which needs no platform import
- CloudflareSecretStoreAdapter: kept under #[cfg] — env.secret() is
  synchronous at the JS level but PlatformSecretStore::get_bytes is sync
  while SecretHandle::get_bytes is async; direct env access is required
- Add .dev.vars to .gitignore (wrangler convention for local secrets)
- Add bytes workspace dep for KvStore impl
- Implement CloudflareWorkers::spawn() to start wrangler dev; in CI uses
  wrangler.ci.toml (no build step, uses pre-built bundle); locally uses
  wrangler.toml (triggers build.sh rebuild)
- Add wrangler.ci.toml: no [build] section so wrangler dev skips the
  worker-build step when the bundle is pre-built as a CI artifact
- Add build-cloudflare input to setup-integration-test-env: adds
  wasm32-unknown-unknown target and runs build.sh with integration test env vars
- In prepare-artifacts: enable build-cloudflare and upload build/ dir as artifact
- In integration-tests: restore CF build dir, install wrangler, set
  CLOUDFLARE_WRANGLER_DIR, and remove --skip flags for cloudflare tests
@prk-Jr prk-Jr linked an issue Apr 20, 2026 that may be closed by this pull request
@prk-Jr
prk-Jr marked this pull request as ready for review April 20, 2026 12:01
- Add GeoInfo happy-path test: build_geo_lookup_returns_some_with_populated_country
  verifies country, city, continent, latitude, and longitude are correctly
  populated when Cloudflare headers are present
- Simplify CloudflareSecretStoreAdapter::get_bytes: collapse brittle JsError
  string-matching guards into a single error arm with contextual message
- Document sequential fan-out latency in CloudflareHttpClient: explain
  sum(DSP_i) vs max(DSP_i) implication and why true parallelism is blocked
  by the ?Send bound on PlatformHttpClient
- Fix stale wrangler.toml comment: update to reflect ConfigStoreHandleAdapter
  wiring rather than the now-fallback NoopConfigStore
- Extend CI triggers to feature branches: format.yml and test.yml now run
  fmt/clippy/tests on PRs targeting feature/** so stacking PRs are gated
- Fix fmt violations caught during pre-review verification: platform.rs
  two-line Err wrapping and plan doc Prettier formatting
@prk-Jr
prk-Jr marked this pull request as draft April 20, 2026 15:37
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.
@prk-Jr
prk-Jr marked this pull request as ready for review April 20, 2026 16:07

@ChristianPavilonis ChristianPavilonis left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Adds the Cloudflare adapter, runtime wiring, and CI coverage. CI is green, but I found two correctness issues that should be addressed before merge, plus two smaller follow-ups.

Comment thread crates/trusted-server-adapter-cloudflare/src/app.rs Outdated
Comment thread crates/trusted-server-adapter-cloudflare/src/platform.rs
Comment thread crates/trusted-server-adapter-cloudflare/src/platform.rs Outdated
Comment thread crates/integration-tests/tests/integration.rs Outdated
prk-Jr added 6 commits April 28, 2026 17:31
Resolved conflicts:
- .cargo/config.toml: extend test-fastly alias to also exclude trusted-server-adapter-cloudflare
- .github/workflows/test.yml: use cargo test-fastly alias in CI (from PR16) and keep test-cloudflare job (from PR17)
- CLAUDE.md: keep Cloudflare workspace entry and cargo test-axum alias; merge both adapters' commands
- crates/trusted-server-core/src/proxy.rs: use #[tokio::test] + std::time pattern (consistent with PR16 tests)
- Cargo.lock: regenerated to reflect merged workspace state
Brings in the default-members fix: restricts workspace default-members
to `trusted-server-adapter-fastly` so Viceroy can locate the binary
via `cargo run --bin` during `cargo test-fastly`.

@aram356 aram356 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Adds the Cloudflare Workers adapter, with web_time migration in core and a new CI job. The middleware-bypass and backend-naming concerns from the prior review are addressed and have regression tests, but the second prior 🔧 (sequential fan-out + missing per-request deadline) is mitigated only by documentation and a noisy log warning — the underlying behavior is unchanged. CI has three required gates failing.

Blocking

🔧 wrench

  • CI is red on three required gatescargo fmt --all -- --check, format-docs, and integration tests are failing on the latest run. The integration-tests failure is especially concerning because this PR is the one adding the CloudflareWorkers runtime to that matrix and the new env-var/wrangler wiring (integration-tests.yml:74-105). Either the new Cloudflare integration test broke, or it isn't actually running and another regression slipped in. Run cargo fmt --all, cd docs && npm run format, and reproduce the integration-tests failure locally with CLOUDFLARE_WRANGLER_DIR set; address whatever surfaces. Don't merge with red required checks.
  • Per-provider auction timeout is silently dropped on Cloudflare (crates/trusted-server-adapter-cloudflare/src/platform.rs:289-315) — see inline.
  • select() warning fires n-1 times per auction → log noise on every multi-provider request (crates/trusted-server-adapter-cloudflare/src/platform.rs:329-337) — see inline.

Non-blocking

🤔 thinking

  • send_async always materializes the body, then re-serializes it for select (platform.rs:289-315) — see inline.
  • Body::Stream silently dropped in outbound request body (platform.rs:228-235) — see inline.

♻️ refactor

  • get_fallback / post_fallback near-duplicates (app.rs:307-363) — see inline.
  • auth_middleware_rejects_with_401_when_credentials_required doesn't actually test 401 (tests/routes.rs:43-75) — see inline.

📌 out of scope

  • wrangler.toml ships a placeholder KV id (wrangler.toml:11) — id = "REPLACE_WITH_YOUR_KV_NAMESPACE_ID" is fine for wrangler dev --local but will fail at deploy. Either call this out in getting-started.md next to the deploy instructions, or use a kv_namespaces[].preview_id so dev still works while the prod id stays empty/required. Follow-up issue is fine.

📝 note

  • worker::Env::clone() per request (platform.rs:444-451) — see inline.

Prior-review status

  • ✅ Cloudflare routes skip middleware → fixed (new src/middleware.rs + regression tests in tests/routes.rs:23-75).
  • ⚠️ send_async serializes / no deadline → only documentation added; the underlying serialization and missing per-request timeout remain (see blocking findings above).
  • ✅ Backend names too coarse → fixed (platform.rs:62-77 includes scheme, host, port, timeout_ms, cert flag).
  • ✅ Stale ignore message → fixed (integration.rs:139,147).

CI Status

  • cargo fmt: FAIL
  • format-docs: FAIL
  • integration tests: FAIL
  • cargo check (cloudflare native + wasm32-unknown-unknown): PASS
  • cargo test: PASS
  • cargo test (axum native): PASS
  • vitest: PASS
  • format-typescript: PASS
  • browser integration tests: PASS

Comment thread crates/trusted-server-adapter-cloudflare/src/platform.rs
Comment thread crates/trusted-server-adapter-cloudflare/src/platform.rs Outdated
Comment thread crates/trusted-server-adapter-cloudflare/src/platform.rs
Comment thread crates/trusted-server-adapter-cloudflare/src/platform.rs Outdated
Comment thread crates/trusted-server-adapter-cloudflare/src/app.rs
Comment thread crates/trusted-server-adapter-cloudflare/tests/routes.rs Outdated
Comment thread crates/trusted-server-adapter-cloudflare/src/platform.rs

@aram356 aram356 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Adds the trusted-server-adapter-cloudflare crate (edgezero handle adapters for Config/KV/secrets, CloudflareHttpClient, geo extraction from cf-* headers), an integration test environment, and a web_time migration in core. Crate scaffolding and middleware wiring are sound after the prior two review rounds, but CI is red on three jobs (one introduced by this PR) and several correctness/quality concerns from the earlier reviews remain open.

Note: the PR base is feature/edgezero-pr16-axum-dev-server, not main. Two of the four CI failures listed below are inherited from the base — but they still block merge to main.

Blocking

🔧 wrench

  • cargo fmt fails on crates/integration-tests/tests/environments/cloudflare.rs — introduced by this PR. The integration-tests crate is in the workspace exclude list, so cargo fmt --all from the repo root passes; the CI rustfmt action checks every Rust file. See inline comment.
  • use worker::*; violates CLAUDE.mdcrates/trusted-server-adapter-cloudflare/src/lib.rs:7. See inline.
  • Integration tests fail with orphaned workerd processescrates/integration-tests/tests/environments/cloudflare.rs:104-109 only kills the wrangler parent; workerd grandchildren leak. Likely root cause of the test_all_combinations / test_nextjs_axum failures observed in CI. See inline.
  • Per-provider auction timeout silently dropped on Cloudflare (still open from prior review) — platform.rs:277. See inline.
  • select() warning logs n-1 times per multi-provider auction (still open from prior review) — platform.rs:337. See inline.
  • CI: clippy::type_complexity failurecrates/trusted-server-adapter-axum/src/platform.rs:194. Inherited from PR-16 base; cannot merge to main until fixed in the base chain. Suggested: type ResponseTriplet = (u16, Vec<(String, Vec<u8>)>, Vec<u8>);.
  • CI: format-docs failuredocs/guide/architecture.md and docs/guide/getting-started.md. Inherited from PR-15 (6d1184d6). Same merge-to-main concern.

Non-blocking

🤔 thinking

  • send_async eagerly awaits → multi-provider auctions degrade to sum(latencies) — see inline at platform.rs:315.
  • Body::Stream silently dropped on outbound — see inline at platform.rs:235.
  • Top-level worker dispatch error: {e} may leak internals to clients — see inline at lib.rs:20.
  • Eager full-response buffering with no cap — see inline at app.rs:96.
  • Confirm worker::Error::Display does not leak the secret value — see inline at platform.rs:391.
  • Hardcoded port 8787 conflicts with local wrangler dev — see inline at cloudflare.rs:23.

♻️ refactor

  • get_fallback and post_fallback are near-duplicates — see inline at app.rs:363.
  • auth_middleware_rejects_with_401_when_credentials_required doesn't actually test 401 — see inline at tests/routes.rs:75.

📝 note

  • Stale #[ignore] reason on Cloudflare integration tests — see inline at integration.rs:139.
  • The PR base also has red CI (format-docs, clippy::type_complexity). Recommend rebasing on the merged base before re-requesting review so CI status reflects only this PR's deltas.

⛏ nitpick

  • Method::from(... .to_ascii_uppercase()) — see inline at platform.rs:216.
  • wrangler.toml ships id = "REPLACE_WITH_YOUR_KV_NAMESPACE_ID"wrangler dev refuses to start until edited. Worth a one-line note in CLAUDE.md or atop build.sh.
  • The cloudflare feature in this crate's Cargo.toml is effectively redundant — [target.'cfg(target_arch = "wasm32")'.dependencies] already pulls worker and the adapter feature unconditionally on wasm32, so --features cloudflare only matters for cargo check on native. The doc comment names the use case but a one-line "why it isn't default" note would help future readers.

CI Status

  • cargo fmt: FAIL (introduced by this PR — crates/integration-tests/tests/environments/cloudflare.rs)
  • clippy: FAIL (inherited from base — axum type_complexity)
  • rust tests (fastly, axum, cloudflare): PASS
  • vitest: PASS
  • format-docs: FAIL (inherited from base)
  • format-typescript: PASS
  • integration tests: FAIL (test_all_combinations, test_nextjs_axum — likely the workerd cleanup issue)
  • browser integration tests: PASS
  • cargo check (cloudflare native + wasm32-unknown-unknown): PASS

Comment thread crates/integration-tests/tests/environments/cloudflare.rs Outdated
Comment thread crates/integration-tests/tests/environments/cloudflare.rs Outdated
Comment thread crates/integration-tests/tests/environments/cloudflare.rs
Comment thread crates/integration-tests/tests/integration.rs Outdated
Comment thread crates/trusted-server-adapter-cloudflare/src/lib.rs Outdated
Comment thread crates/trusted-server-adapter-cloudflare/src/platform.rs Outdated
Comment thread crates/trusted-server-adapter-cloudflare/src/platform.rs
Comment thread crates/trusted-server-adapter-cloudflare/src/app.rs Outdated
Comment thread crates/trusted-server-adapter-cloudflare/src/app.rs Outdated
Comment thread crates/trusted-server-adapter-cloudflare/tests/routes.rs Outdated
prk-Jr added 3 commits May 13, 2026 16:18
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
- Merge latest PR16 base (bff1adc, 314575b): removes unused backend.rs
  and fixes .gitignore comment
- Use SpawnedRequestResult type alias from PR16 (includes Result wrapper)
- Add check-cloudflare alias for wasm32-unknown-unknown target check
- Add clippy-cloudflare alias to complete the per-adapter clippy pattern

@aram356 aram356 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Round 4 review. The three round-3 🔧 blockers (workerd cleanup, cargo fmt, wildcard import) are resolved and CI is fully green on 15206b6a (10/10 jobs). The fail-loud multi-provider design replaces the silent degradation cleanly. Two new blocking items: the critical multi-provider rejection has no test coverage despite being #[cfg]-gateable into a testable helper, and the deferred Secret::to_string() audit needs either inline confirmation or a tracking issue before merge. Everything else is non-blocking — cleanup, defense-in-depth, and follow-ups.

Blocking

🔧 wrench

  • Multi-provider select() rejection has zero test coverageplatform.rs:327. Extract the length check as a target-agnostic free function and add 3-4 unit tests. See inline.

❓ question

  • Confirm worker::Secret::to_string() returns only the raw valueplatform.rs:384. Either inline-confirm against worker 0.7 source or open a tracking issue and link it. See inline.

Non-blocking

🤔 thinking

  • from_utf8_lossy corrupts non-ASCII outbound header valuesplatform.rs:215.
  • worker::Env cloned per request for secret adapterplatform.rs:444.
  • compatibility_date = "2024-09-23" is 18+ months oldcloudflare.toml:3.
  • wrangler.toml placeholder KV ID needs a heads-up notewrangler.toml:11.

♻️ refactor

  • 13 near-identical per-route handler closuresapp.rs:178-303. ~150 LOC reducible via a macro_rules! or generic helper. Same pattern in the Axum adapter — candidate for a shared core helper.
  • apply_finalize_headers silently skips invalid operator-config headersmiddleware.rs:117. CLAUDE.md says invalid enabled config should surface as startup/config errors. Same issue in Axum.

🌱 seedling

  • CloudflareGeo missing regionplatform.rs:500. CF Enterprise injects cf-region / cf-region-code.
  • cloudflare feature is wasm32-only-effective but doesn't say soCargo.toml:18. Latent broken combo for cargo check --features cloudflare on native.

⛏ nitpick

  • Method::from(...uppercase()) still deferred from prior reviewplatform.rs:210.
  • to_ascii_lowercase() allocation per response headerplatform.rs:251. Use eq_ignore_ascii_case.
  • Set Content-Length but don't strip Transfer-Encodingapp.rs:90. Defense-in-depth.
  • cargo install runs unconditionally in build.shbuild.sh:20. Gate with command -v.

CI Status

  • cargo fmt: PASS
  • cargo test (fastly): PASS
  • cargo test (axum native): PASS
  • cargo check (cloudflare native + wasm32-unknown-unknown): PASS
  • vitest: PASS
  • format-docs: PASS
  • format-typescript: PASS
  • integration tests: PASS
  • browser integration tests: PASS
  • prepare integration artifacts: PASS

Open from prior round (acknowledged)

  • 🤔 Eager full-response buffering with no cap (app.rs:88) — deferred to follow-up issue.
  • Secret::to_string() audit — now blocking (see above).

Comment thread crates/trusted-server-adapter-cloudflare/src/platform.rs Outdated
Comment thread crates/trusted-server-adapter-cloudflare/src/platform.rs
Comment thread crates/trusted-server-adapter-cloudflare/src/platform.rs Outdated
Comment thread crates/trusted-server-adapter-cloudflare/src/platform.rs
Comment thread crates/trusted-server-adapter-cloudflare/cloudflare.toml
Comment thread crates/trusted-server-adapter-cloudflare/Cargo.toml
Comment thread crates/trusted-server-adapter-cloudflare/src/platform.rs Outdated
Comment thread crates/trusted-server-adapter-cloudflare/src/platform.rs Outdated
Comment thread crates/trusted-server-adapter-cloudflare/src/app.rs Outdated
Comment thread crates/trusted-server-adapter-cloudflare/build.sh Outdated
prk-Jr added 4 commits May 28, 2026 14:54
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
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.
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.
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.
@prk-Jr
prk-Jr requested a review from aram356 May 28, 2026 09:42

@aram356 aram356 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Round-5 re-review of the Cloudflare Workers adapter. Every blocker from the prior CHANGES_REQUESTED review is resolved and all CI gates are green, including the integration tests job this PR adds the Cloudflare runtime to. No correctness, security, WASM, or convention issues remain in the changed code. Approving; the findings below are non-blocking and left to the author's discretion.

Note: the base is feature/edgezero-pr16-axum-dev-server, not main — approving this greenlights merging into that feature branch; the full edgezero chain still has to land to reach production.

Non-blocking

🤔 thinking

  • Multi-provider rejection fires after the budget is already spentplatform.rs:547-567 (see inline). The supported single-provider path is unaffected.
  • Geo lat/long default to 0.0 (Null Island) when absentplatform.rs:520-529 (see inline).

♻️ refactor

  • Duplicate outbound request headers silently collapsed (headers.set vs append) — platform.rs:213-223 (see inline).

📝 note

  • unreachable! panic in send_asyncplatform.rs:303-305 (see inline).
  • Stacked base / stale compatibility_date — base is a feature branch; compatibility_date = "2024-09-23" is ~20 months old (the comment already invites a bump).

⛏ nitpick

  • Test code uses unwrap() instead of expect("should …")platform.rs test module (see inline).

📌 out of scope

  • wrangler.toml placeholder KV idwrangler.toml:13 (see inline).

Prior-review status

  • ✅ CI red (fmt / format-docs / integration) → all green
  • ✅ Per-provider timeout silently dropped → now rejects loudly (with the timing caveat above)
  • select() warning loop → removed
  • use worker::* → explicit imports
  • workerd process leak → killpg(pgid, SIGTERM) on Drop
  • ✅ Auth test didn't assert behavior → renamed + honest doc
  • get/post_fallback duplication → dispatch() helper
  • Body::Stream silently dropped → now returns Err

The web_time migration is correctly scoped: a no-op std re-export on wasm32-wasip1, so Fastly's production path is unchanged.

CI Status

  • cargo fmt: PASS
  • clippy (cloudflare native + wasm32-unknown-unknown): PASS
  • cargo test: PASS
  • cargo test (axum native): PASS
  • vitest: PASS
  • format-typescript: PASS
  • format-docs: PASS
  • integration tests: PASS
  • browser integration tests: PASS

Comment thread crates/trusted-server-adapter-cloudflare/src/platform.rs Outdated
Comment thread crates/trusted-server-adapter-cloudflare/src/platform.rs Outdated
Comment thread crates/trusted-server-adapter-cloudflare/src/platform.rs
Comment thread crates/trusted-server-adapter-cloudflare/src/platform.rs Outdated
Comment thread crates/trusted-server-adapter-cloudflare/src/platform.rs Outdated
Comment thread crates/trusted-server-adapter-cloudflare/wrangler.toml

@ChristianPavilonis ChristianPavilonis left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review by Yesman.

I found two blocking correctness issues in the new Cloudflare adapter: compressed origin responses can be returned with a stale Content-Length after decoding, and the unsupported multi-provider auction path is rejected only after all provider requests have already been sent and awaited. Please address these before merging.

Comment thread crates/trusted-server-adapter-cloudflare/src/platform.rs
Comment thread crates/trusted-server-adapter-cloudflare/src/platform.rs
prk-Jr added 5 commits June 10, 2026 18:52
Conflict resolutions:
- .github/workflows/test.yml: keep both PR16's Fastly WASM release
  build verification step and PR17's test-cloudflare job
- crates/integration-tests/Cargo.toml: union of libc (PR17) and
  urlencoding (PR16) dev-dependencies; lock file regenerated
- crates/trusted-server-core/src/consent/mod.rs: keep PR17's web_time
  clock (wasm32-unknown-unknown support) plus PR16's AtomicBool import
- crates/js/lib/package-lock.json: regenerated from merged package.json

Semantic fixes for PR16 API changes in the Cloudflare adapter:
- handle_proxy now takes ProxyDispatchInput
- handle_auction gained kv, partner registry, and EC context parameters
- GeoInfo gained the asn field
- PlatformSelectResult gained failed_backend_name
- resolve_publisher_response delegates to the shared
  buffer_publisher_response so the max_buffered_body_bytes cap applies
  on Workers too
- Cloudflare middleware tests build explicit test settings instead of
  loading the baked placeholder secrets that get_settings() rejects
- clippy-cloudflare alias drops --all-features, which always tripped
  the cloudflare feature's non-wasm32 compile_error! guard
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
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).

@ChristianPavilonis ChristianPavilonis left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Approving per request, with the findings below recorded for follow-up. CI is currently green on the latest head.

Findings folded into body

  • P1 — EC timestamp helper still uses std::time::SystemTimecrates/trusted-server-core/src/ec/mod.rs:480: this core path can be exercised by Cloudflare EC generation/sync/tombstone flows, and std::time is not safe on wasm32-unknown-unknown. Suggested fix: migrate current_timestamp() to web_time::{SystemTime, UNIX_EPOCH} and update the stale wasm32-wasip1-only comment.
  • P2 — Cloudflare clippy alias is documented but not run in CI.github/workflows/format.yml:35: CI runs Fastly/Axum clippy but not cargo clippy-cloudflare, so Cloudflare adapter lint regressions can merge unnoticed. Suggested fix: add a Cloudflare clippy workflow step.
  • P2 — clean-checkout Cloudflare build target is not installed by rust-toolchain.tomlrust-toolchain.toml:3: cargo build-cloudflare targets wasm32-unknown-unknown, but the pinned toolchain only installs wasm32-wasip1; CI installs the target explicitly, but local documented commands fail on a clean checkout. Suggested fix: add wasm32-unknown-unknown to the toolchain targets or document/run rustup target add wasm32-unknown-unknown.

CI

Latest gh pr checks 644 shows all current checks passing.

#[cfg(target_arch = "wasm32")]
#[event(fetch)]
pub async fn main(req: Request, env: Env, ctx: Context) -> Result<Response> {
match edgezero_adapter_cloudflare::run_app::<app::TrustedServerApp>(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 — Cloudflare response conversion drops duplicate response headers

The adapter delegates final response conversion to the pinned edgezero-adapter-cloudflare. At the pinned rev in Cargo.toml, from_core_response iterates parts.headers and calls worker::Headers::set(...), which overwrites prior values for repeated header names. The Cloudflare outbound fetch bridge also uses resp.headers().entries(), which is not sufficient for preserving multi-value Set-Cookie.

Why it matters: Trusted Server publisher proxy responses may contain multiple Set-Cookie headers from the origin plus Trusted Server EC/consent cookies. Collapsing to the last value breaks publisher sessions and identity/privacy cookie behavior.

Suggested fix: Update/pin EdgeZero to use append for response headers, and explicitly preserve multi-value Set-Cookie from Workers fetch responses via the appropriate multi-value API. Add a Cloudflare integration/regression test with multiple Set-Cookie headers.

handle_first_party_proxy_rebuild(&s.settings, &services, req).await
}),
)
.get("/", get_fallback.clone())

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 — Cloudflare publisher fallback only supports GET/POST

The Cloudflare router registers only .get/.post for / and /{*rest}. Fastly/Axum route publisher fallback for GET, POST, HEAD, OPTIONS, PUT, PATCH, and DELETE.

Why it matters: Cloudflare will reject or miss normal publisher-origin traffic such as HEAD requests, CORS OPTIONS preflights, and non-GET/POST API calls instead of proxying them. That is a behavioral compatibility break for a transparent edge proxy.

Suggested fix: Mirror Axum/Fastly: add a shared publisher_fallback_methods() list and register both root and wildcard fallback routes for all supported methods. Also apply this to the startup-error router.

// platform executes `send_async` eagerly (e.g. Cloudflare Workers):
// sequential execution would accrue the sum of provider latencies and
// blow the auction budget before a later `select` could reject it.
if provider_names.len() > 1 && !context.services.http_client().supports_concurrent_fanout()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 — Sequential-fanout guard rejects based on configured provider count before filtering launchable providers

The new Cloudflare safety guard checks provider_names.len() > 1 before the existing skip logic for unknown, disabled, timed-out, or no-backend providers.

Why it matters: A Cloudflare deployment with two configured names but only one actually launchable provider now fails the entire auction, whereas the prior logic would safely skip the inactive provider and run the single valid one.

Suggested fix: Build/filter launch candidates first without sending requests, then reject only when more than one request would actually be launched on a non-concurrent platform.


# Allow cloudflare-specific overrides on top (not committed).
# Copy .env.cloudflare.dev.example → .env.cloudflare.dev to customise.
[ -f "$SCRIPT_DIR/.env.cloudflare.dev" ] && . "$SCRIPT_DIR/.env.cloudflare.dev"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 — Cloudflare-specific env overrides are sourced but not exported

.env.cloudflare.dev is sourced without set -a, unlike the root .env.

Why it matters: Local override variables may not be exported to worker-build, so the local Cloudflare bundle can ignore intended overrides.

Suggested fix: Source it with set -a && source ... && set +a.

ChristianPavilonis

This comment was marked as duplicate.

@aram356 aram356 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Round 5 review. All round-4 blocking items resolved — including the two 🔧/❓ blockers I escalated. The author went beyond what was asked: extracted reject_multi_provider_fanout as a testable free function (4 unit tests), AND added a full-pipeline test at the orchestrator level (rejects_multi_provider_fanout_before_launch_on_sequential_platform) that asserts no provider requests launch under the rejection path. The Secret::to_string() audit was done inline with a citation of the worker-rs source. Bonus correctness fix on the response path strips stale Content-Length (which described the compressed payload) and re-emits an accurate value after bytes() decoding — caught a real bug I missed in round 4.

CI is fully green on fe9951dd (10/10 jobs). Approving with non-blocking notes only; the open items are forward-looking (KV-backed EC on Cloudflare prod, shared adapter helpers, missing geo region field) or documentation polish.

Non-blocking

🤔 thinking

  • /auction runs with EcContext::default() on Cloudflare — auction partners never receive a TS EC ID. Matches Axum dev-mode. See inline at app.rs:280.
  • handle_proxy passes kv: None even though Cloudflare has a KV store — integration proxies on Cloudflare run without KV identity-graph augmentation. See inline at app.rs:215.

🌱 seedling

  • make_handler is a candidate to lift into core and share with Axum — both adapters reimplement the same boilerplate. See inline at app.rs:83.
  • CloudflareGeo still missing region — carried from round 4 (asn: None was added for the new trait field, but region is still hardcoded). See inline at platform.rs:522.

📌 out of scope

  • Round-4 deferrals carried forward: worker::Env cloned per request (acknowledged as cheap Rc<JsValue> clone), apply_finalize_headers silently logs invalid operator-config headers (cross-adapter — belongs in core Settings validation), and the PR's own "Known deferred items" (secret store writes).

📝 note

  • Cloudflare integration tests run via --include-ignored in CI only — see inline at integration.rs:141.

CI Status

  • cargo fmt: PASS
  • cargo test (fastly): PASS
  • cargo test (axum native): PASS
  • cargo check (cloudflare native + wasm32-unknown-unknown): PASS
  • vitest: PASS
  • format-docs: PASS
  • format-typescript: PASS
  • integration tests: PASS
  • browser integration tests: PASS
  • prepare integration artifacts: PASS

.post(
"/auction",
make_handler(Arc::clone(&state), |s, services, req| async move {
let ec_context = EcContext::default();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 thinking/auction runs with EcContext::default() on Cloudflare.

This matches the Axum dev-mode pattern (axum/app.rs:276), but the practical effect on Cloudflare prod traffic is that auction partners never receive a TS EC ID — bidders see anonymous requests, EID resolution returns empty (kv = None, registry = None), and consent context is empty.

Likely a tracked follow-up before Cloudflare ships prod traffic. Worth either calling out in the PR description's "Known deferred items" or filing a follow-up issue and linking it from this line, so a future contributor doesn't deploy Cloudflare to production assuming feature parity with Fastly.

method: &method,
path: &path,
settings: &state.settings,
kv: None,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 thinkinghandle_proxy passes kv: None even though Cloudflare has a KV store.

services.kv_store() returns the KvHandleAdapter (real Cloudflare KV), but the KvIdentityGraph type that handle_proxy wants isn't constructed here. Same as Axum dev-mode (axum/app.rs:139) — so integration-proxy responses on Cloudflare won't be augmented with identity-graph data.

Not a regression vs. Axum, but Cloudflare prod will need the KV identity graph wired before it has parity with Fastly. Worth a follow-up issue link from this line.

/// (`|s, svc, req| async move { ... }`) closures.
type BoxedHandlerFuture = Pin<Box<dyn Future<Output = Result<Response, EdgeError>>>>;

fn make_handler<F, Fut>(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🌱 seedlingmake_handler is a candidate to lift into core and share with Axum.

The Axum adapter has an equivalent helper at axum/app.rs:159 (execute_handler). Both adapters need the same "build services, call handler, convert error to response" boilerplate. A core::handler_helpers::wrap would let both adapters share one definition and one test surface — and the eventual Fastly adapter would inherit it for free.

Not for this PR; flag for a future refactor.

latitude: self.latitude,
longitude: self.longitude,
metro_code: 0,
region: None,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🌱 seedlingCloudflareGeo still missing region.

Carried from round 4. Cloudflare Workers Enterprise plans inject cf-region (e.g. "California") and cf-region-code (e.g. "CA"). One header read away from completeness; the asn: None next to it handles the new trait field, but region is still always None. Follow-up issue is fine — not blocking.


#[test]
#[ignore = "requires Docker, the `wrangler` CLI in $PATH, and a prebuilt Cloudflare Workers bundle (run build.sh first); the test starts `wrangler dev` automatically"]
fn test_wordpress_cloudflare() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 note — Cloudflare integration tests run via --include-ignored in CI only.

Local devs need wrangler + bash crates/trusted-server-adapter-cloudflare/build.sh per the updated #[ignore] reasons. The CI workflow (integration-tests.yml) installs wrangler, restores the pre-built CF bundle, and runs --include-ignored — that wiring looks correct end-to-end.

No action — just acknowledging the dev-loop story. A line in CONTRIBUTING.md (or the existing testing docs) pointing devs at bash build.sh before running the ignored tests would close the loop.

- EC current_timestamp(): use web_time::SystemTime instead of std::time so
  the EC path does not panic on wasm32-unknown-unknown (Cloudflare Workers),
  matching the rest of core (proxy, orchestrator, signing).
- Cloudflare router: proxy the publisher fallback for all 7 methods
  (GET/POST/HEAD/OPTIONS/PUT/PATCH/DELETE) via publisher_fallback_methods(),
  mirroring the Axum/Fastly adapters; tsjs stays GET-only. Applies to both
  the main and startup-error routers.
- build.sh: export .env.cloudflare.dev overrides with set -a so worker-build
  sees them.
- rust-toolchain.toml: install wasm32-unknown-unknown so documented
  cargo build-cloudflare works on a clean checkout.
- CI: run cargo clippy-cloudflare in the format workflow.
@prk-Jr
prk-Jr merged commit 4a4b4b9 into feature/edgezero-pr16-axum-dev-server Jun 17, 2026
8 checks passed
@aram356
aram356 deleted the feature/edgezero-pr17-cloudflare-adapter branch June 22, 2026 19:08
prk-Jr added a commit that referenced this pull request Jun 29, 2026
* Add services param to generate_synthetic_id, remove Fastly IP/geo calls in formats and endpoints

* Revert premature publisher geo change from Task 3

* Replace deprecated GeoInfo::from_request in publisher.rs with services.geo().lookup()

* Remove Fastly IP extraction from Didomi copy_headers, use ClientInfo instead

* Move IpAddr import to test module level in didomi.rs

* Apply rustfmt formatting to didomi.rs, publisher.rs, and synthetic.rs

Fix multi-line function call style in didomi.rs, line-break wrapping in
publisher.rs test, and import ordering in synthetic.rs test module.

* Add test coverage for generate_synthetic_id with concrete client IP

Adds noop_services_with_client_ip helper to test_support and a new
test that verifies the client_ip path through generate_synthetic_id
by asserting the HMAC differs when the IP changes.

* Align geo lookup warn log format with codebase convention ({e} not {e:?})

* Apply Prettier formatting to PR7 plan and spec docs

* Document content rewriting as platform-agnostic in platform module

* Document html_processor as platform-agnostic

* Document streaming_processor as platform-agnostic

* Fix unresolved doc link: replace EdgeRequest with edgezero_core::http::Request

* Add plan for content rewriting

* Add plan for PR9: wire signing to store primitives

* Add build_services_with_config_and_secret to test_support

* Add FastlyManagementApiClient to adapter

* Implement FastlyPlatformConfigStore and FastlyPlatformSecretStore write methods via management API

Replace FastlyApiClient with FastlyManagementApiClient in the put/delete
methods of FastlyPlatformConfigStore and the create/delete methods of
FastlyPlatformSecretStore. Remove the now-unused FastlyApiClient import.

* Migrate KeyRotationManager from FastlyApiClient to RuntimeServices store primitives

* Migrate signing.rs from FastlyConfigStore/FastlySecretStore to RuntimeServices

* Delete storage/api_client.rs from core; remove FastlyApiClient

* Fix formatting after CI gate check

* Add services to AuctionContext; remove deprecated from_config shim

Thread RuntimeServices into AuctionContext so auction providers can
access platform stores directly. Update PrebidAuctionProvider to use
RequestSigner::from_services(context.services) instead of the now-
removed from_config() shim. All construction sites and test helpers
updated accordingly.

This satisfies the final acceptance criterion of #490: no
FastlyConfigStore/FastlySecretStore construction remains in the
request_signing/ modules.

* Fix prettier formatting in PR9 plan document

* Add PR 10 logging initialization design

* Add PR 10 logging initialization plan

* Fix PR 10 logging plan to avoid per-log allocation

* Extract Fastly logging initialization into adapter module

* Wire Fastly main.rs to adapter-local logging module

* Remove log-fastly from core dependencies

* Format Fastly logging module declaration

* format plan docs

* Address PR findings

* Restore idiomatic fern logging and improve target label extraction

- Reverted gratuitous _message rename and record.args() usage in logging.rs, returning to the idiomatic message parameter inside the fern format closure.
- Refactored target_label to use .rsplit_once("::") rather than .split("::").last(). This provides a more explicit and robust way to extract the final module segment.
- Expanded target_label test coverage to explicitly test edge cases such as inputs without :: separators, empty strings, and inputs with trailing ::.

* Migrate utility layer to HTTP types

* Migrate handler layer to HTTP types

* Address PR review findings

* Address review findings

* Migrate integration and provider HTTP types

* Address review findings

* Resolve review findings

* Resolve PR review findings

* Address review findings

* Removed unused import

* Resolve PR findings

* Resolve PR findings

* Add dual-path entry point with feature-flag dispatch and legacy_main extraction

* Fix clippy doc_markdown, fmt, and add warn log on flag-read failure

* Add FinalizeResponseMiddleware and AuthMiddleware with golden header-precedence test

* Simplify EdgeError::internal call and fix comment accuracy in middleware

* Revert anyhow direct dependency; use std::io::Error::other for EdgeError::internal

* Implement TrustedServerApp with all routes via Hooks trait

Replace the app.rs stub with the full EdgeZero application wiring:
- AppState struct holding Settings, AuctionOrchestrator,
  IntegrationRegistry, and PlatformKvStore
- build_per_request_services() builds RuntimeServices per request using
  FastlyRequestContext for client IP extraction
- http_error() mirrors legacy http_error_response() from main.rs
- All 12 routes from legacy route_request() registered on RouterService
- Catch-all GET/POST handlers using matchit {*rest} wildcard dispatch
  to integration proxy or publisher origin fallback
- FinalizeResponseMiddleware (outermost) and AuthMiddleware registered

* Fix app.rs spec gaps: remove double-Arc, move tsjs into catch-all, fix handler pattern

- Remove Arc::new() wrapper around build_state() which already returns Arc<AppState>
- Remove dedicated GET /static/{*rest} route and its tsjs_handler closure
- Move tsjs handling into GET /{*rest} catch-all: check path.starts_with("/static/tsjs=") first
- Extract path/method from ctx.request() before ctx.into_request() to keep &req valid
- Replace .map_err(|e| EdgeError::internal(...)) with .unwrap_or_else(|e| http_error(&e)) in all named-route handlers
- Remove configure() method from TrustedServerApp (not part of spec)
- Remove unused App import

* Clean up app.rs: remove gratuitous allocation, Box::pin inconsistency, unused turbofish, and overly-broad field visibility

- Drop `.into_bytes()` in `http_error`; `Body` implements `From<String>` directly
- Remove `Box::pin` wrapper from `get_fallback` closure; plain `async move` matches all other handlers
- Remove `Ok::<Response, EdgeError>` turbofish in `post_fallback`; type is now inferred
- Drop now-unused `EdgeError` import that was only needed for the turbofish
- Narrow `AppState` field visibility from `pub` to `pub(crate)`; struct is internal to this crate

* Reference legacy-cleanup issue in legacy_main comment

* Fix lint issues

* Pin edgezero dependencies to branch=main and bump toml to 1.1

Switches all four edgezero workspace dependencies from rev=170b74b to
branch=main so the adapter can use dispatch_with_config, the non-deprecated
public dispatch path. The main branch requires toml ^1.1, so the workspace
pin is bumped from "1.0" to "1.1" to resolve the version conflict.

* Switch EdgeZero dispatch to dispatch_with_config, add routing log lines

Replaces the deprecated dispatch() call with dispatch_with_config(), which
injects the named config store into request extensions without initialising
the logger a second time (a second set_logger call would panic because the
custom fern logger is already initialised above). Adds log::info lines for
both the EdgeZero and legacy routing paths.

* Register explicit GET / and POST / routes to cover matchit root path gap

matchit's /{*rest} catch-all does not match the bare root path /. Add
explicit .get("/", ...) and .post("/", ...) routes that clone the fallback
closures so requests to / reach the publisher origin fallback rather than
returning a 404.

* Add trusted_server_config config store for local dev

Registers the trusted_server_config config store in fastly.toml with
edgezero_enabled = "true" so that fastly compute serve routes requests
through the EdgeZero path without needing a deployed service.

* Address PR review findings in app.rs

- Normalise get_fallback to extract path/method from req after consuming
  the context, consistent with post_fallback and avoiding a double borrow
  on ctx
- Add comment to http_error documenting the intentional duplication with
  http_error_response in main.rs (different HTTP type systems; removable
  in PR 15)
- Add comment above route handlers explaining why the explicit per-handler
  pattern is kept over a macro abstraction

* Add PR15 implementation plan: remove fastly from core crate

* Move compat conversion fns to adapter, delete core compat.rs

* Move geo_from_fastly from core to adapter platform

* Move BackendConfig from core to adapter backend module

BackendConfig uses fastly::backend::Backend directly, making it
incompatible with the platform-portability goal. This commit:

- Copies backend.rs verbatim into trusted-server-adapter-fastly,
  updating the one crate-internal import path
- Adds url dependency to the adapter Cargo.toml
- Rewires platform.rs and management_api.rs to use crate::backend
- Removes pub mod backend from trusted-server-core/lib.rs
- Migrates aps.rs, adserver_mock.rs, and prebid.rs off the deleted
  BackendConfig, using context.services.backend().ensure() for
  registration and a new predict_backend_name_for_url free function
  in integrations/mod.rs for stateless name prediction

cargo check --workspace passes with zero errors.

* Delete dead backend_name_for_url from adapter backend

All callers were migrated to predict_backend_name_for_url in core's
integrations module. Remove the now-unused method and update the
parse_origin doc comment that referenced it.

* Delete legacy FastlyConfigStore and FastlySecretStore from core

Remove crates/trusted-server-core/src/storage/ entirely (config_store.rs,
secret_store.rs, mod.rs). All call sites migrated to PlatformConfigStore /
PlatformSecretStore traits. Also drop the now-broken intra-doc links in
trusted-server-adapter-fastly/src/platform.rs that referenced the deleted types.

* Remove fastly::kv_store from core consent module

Replace direct fastly::kv_store::KVStore usage in consent/kv.rs with a
platform-neutral ConsentKvOps trait. The trait has four sync methods
(load_entry, save_entry_with_ttl, fingerprint_unchanged, delete_entry)
keeping the consent pipeline synchronous.

- consent/kv.rs: add ConsentKvOps trait; replace load_consent_from_kv /
  save_consent_to_kv / delete_consent_from_kv with load_consent /
  save_consent (trait-based); remove open_store / fingerprint_unchanged
  private fns; drop ConsentKvMetadata / metadata_from_context (metadata
  API was Fastly-specific; fingerprint now lives in the body fp field);
  add StubKvOps + integration tests
- consent/mod.rs: add kv_ops field to ConsentPipelineInput; update
  try_kv_fallback and try_kv_write to consume kv_ops instead of
  store_name; update all struct literal sites
- publisher.rs: add kv_ops: Option<&dyn ConsentKvOps> parameter to
  handle_publisher_request; wire it into ConsentPipelineInput and use it
  for the SSC-revocation delete
- adapter/platform.rs: add FastlyConsentKvStore wrapping
  fastly::kv_store::KVStore implementing ConsentKvOps via the sync API
- adapter/app.rs + main.rs: open FastlyConsentKvStore from
  settings.consent.consent_store and pass as kv_ops to
  handle_publisher_request

* Fix consent KV trait design and formatting

Remove `fingerprint_unchanged` from `ConsentKvOps` trait so the common
case (consent unchanged) costs one KV read instead of two. `save_consent`
now loads the entry once and compares the fingerprint inline. Extract
`open_consent_kv` helper in `app.rs` to deduplicate the two identical
KV-open blocks. Replace bare `unwrap()` with descriptive `expect` messages
in consent KV tests. Run `cargo fmt --all` to fix all whitespace issues.

* Move tokio to dev-dependencies in core (test-only usage)

* Remove fastly dependency from trusted-server-core

* Remove stale fastly:: references from core doc comments

* Apply cargo fmt formatting fixes

* Wire consent KV into auction path and remove tokio from core tests

* Fix rotate/delete atomicity, HTTP verb, idempotent deletes, and weak tests

* Resolve PR review feedback on logging module

* Address review findings

* Resolve PR review findings

* Address review findings: safe body reads and bounded inbound forwarding

- Add collect_body_bounded helper with INTEGRATION_MAX_BODY_BYTES (256 KiB)
  cap to prevent unbounded memory use on streaming bodies
- Replace all into_bytes() calls (panic on Body::Stream) with collect_body
  or collect_body_bounded across integrations and auction endpoint
- Make AuctionProvider::parse_response async so implementations can safely
  drain response bodies without panicking on the Stream variant
- Add .await to both parse_response call sites in the orchestrator
- Cap inbound request bodies in lockr, permutive, datadome, and didomi
  proxy handlers using collect_body_bounded before forwarding upstream
- Fix lockr Origin header: replace expect() with a warn-and-skip fallback
  so an invalid user-supplied origin cannot crash the edge worker
- Add PayloadSizeError::StreamRead variant to google_tag_manager and return
  502 (not 413) when a stream transport error occurs while reading the body
- Remove extra blank line before closing brace in google_tag_manager impl block

* Resolve PR review findings and format lint

* Add trusted-server-adapter-axum crate skeleton with lib + bin targets

* Add .gitignore to exclude target/ from axum adapter crate

* Add Axum platform trait implementations (config/secret/backend/geo/http)

* Add Axum middleware: FinalizeResponseMiddleware + AuthMiddleware

* Add Axum app wiring: TrustedServerApp Hooks implementation

* Add Axum adapter integration tests: route parity + middleware

* Add CI job for Axum adapter native build and test

* Update CLAUDE.md: add Axum adapter to workspace layout and build commands

* Fix clippy warnings: add #[must_use], Panics doc, replace eprintln with log::error

* Promote axum adapter to workspace member, remove global wasm32 target

Move trusted-server-adapter-axum from workspace exclude to members list.
Remove the global `target = "wasm32-wasip1"` build override from
.cargo/config.toml (which forced the axum crate out of the workspace)
and pass --target wasm32-wasip1 explicitly only for Fastly CI commands.
Delete the now-redundant crate-local .cargo/config.toml.

Update CI test-rust job to exclude the axum crate and pass the explicit
target; test-axum job runs from the workspace root with -p flag.
Add .edgezero/ to .gitignore to exclude the local KV store file.

* Add Axum runtime environment to integration test matrix

Register AxumDevServer alongside FastlyViceroy in RUNTIME_ENVIRONMENTS so
the full framework x runtime scenario matrix (WordPress, Next.js) runs
against both platforms.

AxumDevServer spawns the native trusted-server-axum binary (no WASM or
Viceroy), binds to the fixed port 8787 (baked into axum.toml at compile
time), and polls for any HTTP response as readiness (root returns 403 in
test env). Binary path defaults to target/debug/trusted-server-axum,
overridable via AXUM_BINARY_PATH.

Settings are baked in at build time via TRUSTED_SERVER__* env vars, same
as Fastly. The integration-tests.sh script now builds both the WASM and
the native Axum binary with test-specific overrides (origin=127.0.0.1:8888).

Add test_wordpress_axum and test_nextjs_axum individual test functions.
Ignore .edgezero/ at workspace root (local KV store from the dev server).

* Remove unused StatusCode import from routes integration test

* Update docs to cover Axum dev server alongside Fastly

- README: update Quick Start and Development commands for both runtimes
- getting-started: add Axum as Option A (no Fastly CLI needed)
- architecture: add trusted-server-adapter-axum to Core Components;
  add Runtime Targets table
- testing: fix cargo test commands; add Axum adapter section; split
  Local Server Testing into Axum vs Fastly; restore clippy step in
  CI/CD workflow example alongside new test-axum job

* Reorder local dev options: Fastly first, Axum second

* Fix CI: build and pass Axum binary to integration test job

The integration test matrix includes AxumDevServer which requires the
native trusted-server-axum binary. Add build-axum step to the shared
setup action, package the binary alongside the WASM artifact, and pass
AXUM_BINARY_PATH to the integration test run step.

* fix integration test

* 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

* 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

* Fix integration test failure

* 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.

* 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.

* 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)

* 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.

* 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.

* 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

* 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.

* Enforce max_buffered_body_bytes on the buffered publisher post-processing path

The buffering cap was only applied when the EdgeZero entry point converted a
streaming publisher response into a buffered one. The shared
handle_publisher_request BufferedProcessed arm — taken for HTML responses with
a registered post-processor such as NextJS — decoded and re-wrote the body into
an unbounded Vec, so a highly-compressible origin response under the platform
raw-body cap could expand past max_buffered_body_bytes and exhaust the Wasm
heap.

Move BoundedWriter into trusted-server-core and apply it in the
BufferedProcessed arm so both that path and the EdgeZero stream-to-buffer
conversion enforce the same configured limit. Add a regression test driving a
registered HTML post-processor with a response that exceeds a small cap and
asserting the request errors instead of allocating past the limit.

* Migrate auction format tests to edgezero http types after merge

Merge 6e191e9 combined the edgezero http::Request/Response migration with
new auction tests from main that still used the Fastly SDK API, and added
the RequestTooLarge error variant. Update the auction format tests to build
Request<EdgeBody> via the http builder, read response bodies through
EdgeBody::into_bytes, pass the new services and geo arguments to
convert_tsjs_to_auction_request, and use http accessors. Cover the new
RequestTooLarge variant in the error status-code guard and mapping test.

* Give auction transport failures a consistent error envelope

Provider transport failures (select() errors) pushed a bare
AuctionResponse::error with no metadata, while parse and launch failures
attach error_type/message. Route transport failures through a shared
helper with a new ERROR_TYPE_TRANSPORT classifier and a static,
upstream-safe message, so the public /auction response shape no longer
depends on how a provider failed. Add unit coverage for the new helper.

* Bound the HTML post-processing accumulator by max_buffered_body_bytes

The publisher.max_buffered_body_bytes cap was only enforced by BoundedWriter
on the bytes emitted from process_response_streaming. The HTML path with
registered post-processors first accumulates every rewritten chunk in
HtmlWithPostProcessing::accumulated_output and emits nothing until the final
chunk, so a highly compressible document could grow that buffer far past the
cap before the writer ever saw it — the exact Wasm-heap OOM the setting is
meant to prevent.

Thread the configured limit into the accumulator and reject before extending
it, and re-check the post-processed output (post-processors may append
content). Errors use the same message and propagate through the same
process_chunk path that maps to a 5xx proxy error. Add a regression test
proving the accumulator itself cannot grow past the cap mid-stream rather
than only failing on the final write.

* Correct max_buffered_body_bytes failure status in sample config

BoundedWriter errors map through TrustedServerError::Proxy, whose status is
502 Bad Gateway, not 500. Operators may build runbooks from this sample, so
document the actual 502 proxy-error status.

* Sync integration-tests lockfile after dropping core wasm js deps

* Fix EdgeZero fallback asset-route and EC partner parity

Address two EdgeZero-path regressions and one doc mismatch from PR 628 review:

- Asset routes: dispatch_fallback now checks settings.asset_route_for_path for
  GET/HEAD before the publisher fallback and dispatches matches through
  handle_asset_proxy_request, mirroring legacy route_request. Asset responses
  skip EC finalization (no EcFinalizeState) and thread AssetProxyCachePolicy out
  so edgezero_main reapplies protected cache directives after finalization.

- EC partner config: build PartnerRegistry in build_state_from_settings so an
  invalid ec.partners config fails closed at startup (routes() falls back to
  startup_error_router) instead of serving publisher fallback responses with the
  partner-registry error only logged at finalize. Per-request rebuilds in
  identify/auction/batch-sync now reuse the shared registry.

- Docs: max_buffered_body_bytes cap applies to any buffered post-processed
  publisher response on both the legacy and EdgeZero paths; broaden the
  settings.rs and trusted-server.toml doc strings to match the shared code path.

Add regression tests for the asset-route fallback and fail-closed partner config.

* Capture EdgeZero device signals before request conversion

The EdgeZero path derived device signals from a synthetic fastly request
reconstructed from the EdgeZero HTTP request. That request cannot expose
Fastly-only TLS values (`get_tls_ja4`, `get_client_h2_fingerprint`), so the
JA4/H2 class the EC bot gate relies on was lost and real browsers were
classified as non-browser traffic — silently suppressing EC generation,
finalize KV writes, and pull-sync.

Derive `DeviceSignals` in `edgezero_main` from the original `FastlyRequest`
before `into_core_request` consumes it, store them in the request extensions,
and read them back in `build_ec_request_state` (falling back to the
reconstructed request when absent, e.g. in tests). Add EdgeZero-path tests
proving a browser-shaped request reaches the EC finalize path and that a
request without captured signals is treated as non-browser.

* Preserve origin Content-Length for bodiless EdgeZero asset responses

The EdgeZero asset-route fallback unconditionally rewrote Content-Length to the
buffered byte count whenever the asset origin returned a streaming body. For
HEAD responses and bodiless statuses (204, 304) — which advertise the origin
representation length while carrying no body — that rewrote a meaningful
Content-Length to 0, corrupting the metadata.

Only recompute Content-Length and attach the buffered body for body-bearing
responses; HEAD/204/304 keep the origin Content-Length untouched. Add a unit
test for the body-presence guard.

Also document the interim asset buffering behavior: the EdgeZero path buffers
asset bodies (legacy streams them uncapped) bounded by the publisher OOM guard.
Restoring streaming and deciding whether assets warrant a dedicated cap are
deferred to the streaming cutover (issue #495).

* Align Publisher Default with the config buffer-cap default

Publisher derived Default, so Publisher::default() / Settings::default() set
max_buffered_body_bytes to usize's 0 while deserializing TOML without the key
applied the intended 16 MiB default. Programmatic defaults are used in tests
and helper code, where any buffered post-processing would fail immediately at a
zero-byte cap.

Replace the derived Default with a manual impl that sources
default_max_buffered_body_bytes(), keeping the other fields at their derived
defaults. Add a unit test asserting the manual default and the TOML default
stay aligned.

* Add trusted-server-adapter-cloudflare crate (PR 17) (#644)

* Replace std::time::Instant with web_time::Instant in auction orchestrator

wasm32-unknown-unknown (Cloudflare Workers) does not support
std::time::Instant — it panics at runtime. web_time::Instant is a
zero-cost drop-in on native and JS-backed on WASM.

* Add trusted-server-adapter-cloudflare crate with host-target smoke tests

Implements the Cloudflare Workers adapter following the same pattern as
trusted-server-adapter-axum: TrustedServerApp implements the Hooks trait,
platform services use noop stubs on native (CI-compilable), and the
#[event(fetch)] entry point delegates to edgezero_adapter_cloudflare::run_app.

Also adds UnavailableHttpClient to trusted-server-core platform module,
parallel to the existing UnavailableKvStore.

* Add CI job for Cloudflare adapter and update CLAUDE.md

CI: test-cloudflare job checks native host compile, wasm32-unknown-unknown
compile (with cloudflare feature), and runs host-target unit tests.
CLAUDE.md: add cloudflare crate to workspace layout and build commands.

* Fix Cloudflare entry point: use worker 0.7 and pass manifest_src to run_app

The rev (38198f9) of edgezero used in this workspace requires worker 0.7
(not 0.6) and run_app() takes a manifest_src: &str as first argument.
Updated Cargo.toml and lib.rs accordingly.

* Add CloudflareHttpClient, build.sh, wrangler DX, and review fixes

- Implement CloudflareHttpClient (wasm32 only) using worker::Fetch for real
  outbound proxy requests; strip content-encoding/transfer-encoding headers
  since the Workers runtime auto-decompresses responses
- Add build.sh with cd-to-SCRIPT_DIR guard so worker-build always runs from
  the correct crate root regardless of invocation directory
- Switch wrangler dev task to use --cwd from workspace root (same DX as Fastly)
- Add js-sys to workspace dependencies; reference it via { workspace = true }
- Fix #[ignore] messages on Cloudflare integration tests
- Replace std::time::{SystemTime,UNIX_EPOCH} with web_time in test code for
  signing.rs and proxy.rs (consistency with production paths)
- Add NoopConfigStore/NoopSecretStore TODO comment tracking the gap
- Add extract_client_ip unit tests (parses cf-connecting-ip, absent, invalid)
- Remove empty crate_compiles_on_host_target test
- Add CloudflareHttpClient timeout doc noting Workers CPU-budget tradeoff

* Wire Cloudflare platform stores via edgezero handles

Replace direct worker::Env store construction with edgezero handles
already injected by run_app, reducing #[cfg(target_arch = "wasm32")]
blocks from 5 to 2.

- ConfigStoreHandleAdapter: bridges ctx.config_store() to
  PlatformConfigStore — reuses the already-parsed TRUSTED_SERVER_CONFIG
  JSON handle rather than re-parsing it on every request
- KvHandleAdapter: bridges ctx.kv_handle() to PlatformKvStore — reuses
  the env.kv() handle opened by run_app rather than opening a new one
- CloudflareGeo: moved outside #[cfg]; reads cf-ipcountry and related
  headers via ctx.request().headers() which needs no platform import
- CloudflareSecretStoreAdapter: kept under #[cfg] — env.secret() is
  synchronous at the JS level but PlatformSecretStore::get_bytes is sync
  while SecretHandle::get_bytes is async; direct env access is required
- Add .dev.vars to .gitignore (wrangler convention for local secrets)
- Add bytes workspace dep for KvStore impl

* Add Cloudflare Workers to CI integration tests

- Implement CloudflareWorkers::spawn() to start wrangler dev; in CI uses
  wrangler.ci.toml (no build step, uses pre-built bundle); locally uses
  wrangler.toml (triggers build.sh rebuild)
- Add wrangler.ci.toml: no [build] section so wrangler dev skips the
  worker-build step when the bundle is pre-built as a CI artifact
- Add build-cloudflare input to setup-integration-test-env: adds
  wasm32-unknown-unknown target and runs build.sh with integration test env vars
- In prepare-artifacts: enable build-cloudflare and upload build/ dir as artifact
- In integration-tests: restore CF build dir, install wrangler, set
  CLOUDFLARE_WRANGLER_DIR, and remove --skip flags for cloudflare tests

* Resolve PR review findings in Cloudflare adapter

- Add GeoInfo happy-path test: build_geo_lookup_returns_some_with_populated_country
  verifies country, city, continent, latitude, and longitude are correctly
  populated when Cloudflar…
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cloudflare Workers entry point

3 participants