Wire request signing to RuntimeServices store primitives (PR 9)#609
Wire request signing to RuntimeServices store primitives (PR 9)#609
Conversation
Rename crates/common → crates/trusted-server-core and crates/fastly → crates/trusted-server-adapter-fastly following the EdgeZero naming convention. Add EdgeZero workspace dependencies pinned to rev 170b74b. Update all references across docs, CI workflows, scripts, agent files, and configuration.
Introduces trusted-server-core::platform with PlatformConfigStore, PlatformSecretStore, PlatformKvStore, PlatformBackend, PlatformHttpClient, and PlatformGeo traits alongside ClientInfo, PlatformError, and RuntimeServices. Wires the Fastly adapter implementations and threads RuntimeServices into route_request. Moves GeoInfo to platform/types as platform-neutral data and adds geo_from_fastly for field mapping.
…o-pr2-platform-traits
- Defer KV store opening: replace early error return with a local UnavailableKvStore fallback so routes that do not need synthetic ID access succeed when the KV store is missing or temporarily unavailable - Use ConfigStore::try_open + try_get and SecretStore::try_get throughout FastlyPlatformConfigStore and FastlyPlatformSecretStore to honour the Result contract instead of panicking on open/lookup failure - Encapsulate RuntimeServices service fields as pub(crate) with public getter methods (config_store, secret_store, backend, http_client, geo) and a pub new() constructor; adapter updated to use new() - Reference #487 in FastlyPlatformHttpClient stub (PR 6 implements it) - Remove unused KvPage re-export from platform/mod.rs - Use super::KvHandle shorthand in RuntimeServices::kv_handle()
- Split fastly_storage.rs into storage/{config_store,secret_store,api_client,mod}.rs
- Add PlatformConfigStore read path via FastlyPlatformConfigStore::get using ConfigStore::try_open/try_get
- Add PlatformError::NotImplemented variant; stub write methods on FastlyPlatformConfigStore and FastlyPlatformSecretStore
- Add StoreName/StoreId newtypes with From<String>, From<&str>, AsRef<str>
- Add UnavailableKvStore to core platform module
- Add RuntimeServicesBuilder replacing 7-arg constructor
- Migrate get_active_jwks and handle_trusted_server_discovery to use &RuntimeServices
- Update call sites in signing.rs, rotation.rs, main.rs
- Add success-path test for handle_trusted_server_discovery using StubJwksConfigStore
- Fix test_parse_cookies_to_jar_empty typo (was emtpy)
- Make StoreName and StoreId inner fields private; From/AsRef provide all needed construction and access - Add #[deprecated] to GeoInfo::from_request with #[allow(deprecated)] at the three legacy call sites to track migration progress - Enumerate the six platform traits in the platform module doc comment - Extract backend_config_from_spec helper to remove duplicate BackendConfig construction in predict_name and ensure - Replace .into_iter().collect() with .to_vec() on secret plaintext bytes - Remove unused bytes dependency from trusted-server-adapter-fastly - Add comment on SecretStore::open clarifying it already returns Result (unlike ConfigStore::open which panics)
ChristianPavilonis
left a comment
There was a problem hiding this comment.
Overall this is a well-architected PR — the core crate is now genuinely platform-agnostic for signing, and the management API isolation to the adapter is the right layering. A few correctness and structural items below, none blocking.
Note: One finding could not be placed as an inline comment because the relevant line is not part of the diff:
🤔 Module doc still references Fastly-specific store semantics (crates/trusted-server-core/src/request_signing/mod.rs, line 8)
The module doc still says "Fastly stores have two identifiers" and references ConfigStore::open / SecretStore::open. Since the whole point of this PR is platform-agnostic signing, this doc should use platform-neutral language (e.g. "Platform stores have two identifiers" and reference the trait methods).
aram356
left a comment
There was a problem hiding this comment.
Summary
This PR cleanly separates the Fastly management API client from trusted-server-core into the adapter layer, replacing direct FastlyConfigStore/FastlySecretStore usage with RuntimeServices platform traits. The architecture improvement is solid. Two items need clarification before merge.
Blocking
🔧 wrench
- Secret storage format change: the new
FastlyManagementApiClient::create_secretbase64-encodes thesecretfield at the transport layer (and uses PUT), while the oldFastlyApiClient::create_secretsent it as raw text (POST). Sincerotation.rs::store_private_keyalready base64-encodes the raw key, the effective payload format has changed. Need confirmation on Fastly API contract and backward compatibility with existing keys. (management_api.rs:68)
❓ question
- Broadened status check: old code checked for specific status codes (
OK,NO_CONTENT), new code usesis_success()(any 2xx). Was this intentional? (management_api.rs:90)
Non-blocking
🤔 thinking
- Per-call client construction: each write call constructs a new
FastlyManagementApiClient(4x duringrotate_key). SDK caching mitigates this, but worth considering caching at a higher level. (platform.rs:69) parse_active_kidsuntested directly: edge cases like",",""," kid-a , , kid-b "are not covered by focused unit tests. (mod.rs:56)
♻️ refactor
signing_store_idsreturns unnamed tuple: fine for two values, but a named struct would improve readability if more settings are added. (endpoints.rs:168)
⛏ nitpick
JWKS_STORE_NAME/SIGNING_STORE_NAMEvisibility:pubbut only used within the crate —pub(crate)would be more precise. (mod.rs:49)- PR checklist says "tracing": actual code and CLAUDE.md use
logmacros.
CI Status
- integration tests: PASS
- browser integration tests: PASS
- prepare integration artifacts: PASS
aram356
left a comment
There was a problem hiding this comment.
Summary
The architectural move — extracting FastlyApiClient from trusted-server-core into trusted-server-adapter-fastly::management_api and threading RuntimeServices through signing and auction — is exactly the right direction for EdgeZero's platform-agnostic core. Local fmt/clippy/cargo test --workspace all pass, and integration checks on GitHub are green.
Four blocking items prevent merge as-is, all clustered around the rotate/deactivate lifecycle. Fixing (1) first will likely surface (2) and (3) via real tests without any further changes.
Blocking
🔧 wrench
1. Handler tests in request_signing/endpoints.rs assert nothing (crates/trusted-server-core/src/request_signing/endpoints.rs:542-658)
test_handle_rotate_key_with_empty_body, test_handle_rotate_key_with_custom_kid, test_handle_deactivate_key_request, test_handle_deactivate_key_with_delete, and test_handle_trusted_server_discovery all use this pattern:
match result {
Ok(mut resp) => { /* log … no asserts */ }
Err(e) => log::debug!("Expected error in test environment: {}", e),
}They pass whether the handler succeeds or fails, so the new rotate/deactivate/discovery handler paths are effectively untested even though the PR checklist claims "New code has tests."
Fix: assert on response status and on the success/error fields of the parsed JSON body. Minimum:
let mut resp = handle_rotate_key(&settings, &noop_services(), req)
.expect("should return a response even when stores are unavailable");
assert_eq!(resp.get_status(), StatusCode::INTERNAL_SERVER_ERROR);
let body: RotateKeyResponse = serde_json::from_str(&resp.take_body_str())
.expect("should deserialize rotate response");
assert!(!body.success);
assert!(body.error.is_some());2. SECRET_UPSERT_METHOD = "PUT" on the Fastly secret-store collection endpoint is not a documented API verb (crates/trusted-server-adapter-fastly/src/management_api.rs:31, 257-289)
The previous api_client.rs used POST (the documented create verb); upsert semantics in Fastly's secret-store API are expressed by posting with method: "create_or_recreate" in the JSON body, not by switching the verb.
The only guards here are secret_upsert_method_uses_put (asserts the constant against itself) and create_secret_uses_secret_store_error_for_transport_failures (uses a bogus backend). Neither touches the real API contract. As written, the second rotation of any kid — or a same-day re-rotation via generate_date_based_kid — will likely return 405/400 from the management API in production.
Fix:
const SECRET_UPSERT_METHOD: &str = "POST";
fn build_secret_payload(secret_name: &str, secret_value: &str) -> String {
serde_json::json!({
"name": secret_name,
"secret": general_purpose::STANDARD.encode(secret_value.as_bytes()),
"method": "create_or_recreate",
})
.to_string()
}This is distinct from the earlier base64 finding — it's about the verb itself.
3. KeyRotationManager::rotate_key is non-atomic across four management-API writes with no rollback (crates/trusted-server-core/src/request_signing/rotation.rs:61-97)
Sequence:
store_private_key(secret store)store_public_jwk(config store)update_current_kid(config store)update_active_kids(config store)
Failure modes that leave the store inconsistent:
- Fail at (2): private key lives in the secret store with no corresponding JWK — orphaned key material.
- Fail at (4):
current-kidadvances to a kid that isn't inactive-kids. The edge starts signing with a kid whose JWK is not advertised in the JWKS discovery document, so downstream verifiers that refresh viaactive-kidswill reject every signed request until the store is manually repaired. This is a signing outage.
Fix: on failure after (1) or (2), attempt best-effort deletion of the freshly written artifacts and surface the rollback status in the returned Report. Ideally, reorder so current-kid only flips after both artifacts AND active-kids are confirmed written:
store_private_key → store_public_jwk → update_active_kids → update_current_kid
That ordering ensures any partial failure leaves the old kid still active and the new kid visible in JWKS but unused — a recoverable state.
4. delete_key has a symmetric partial-failure hole that leaks private key material (crates/trusted-server-core/src/request_signing/rotation.rs:205-227)
If deactivate_key and the JWK delete both succeed but secret_store.delete fails, the private key remains in the secret store while its JWK is gone. A retry then fails at the JWK delete step because check_response rejects 404 (see ❓ below), so the operator can't converge by re-running the command.
Fix: order the secret-store delete before the JWK delete so a partial failure leaves a verifiable-but-not-signable orphan JWK (safer than orphaned key material), and treat 404 from both deletes as success so retries converge.
❓ question
5. Are management-API deletes meant to be idempotent? (crates/trusted-server-adapter-fastly/src/management_api.rs:224-322)
check_response rejects any non-2xx, so a 404 on delete_config_item/delete_secret propagates as an error. For key-lifecycle use, idempotent deletes are usually desirable so a retried delete_key converges after a partial failure. Intentional? A minimal change:
if response.get_status() == StatusCode::NOT_FOUND {
return Ok(());
}This interacts directly with finding (4).
6. Status check broadened from specific codes to is_success() (crates/trusted-server-adapter-fastly/src/management_api.rs:90)
Old api_client.rs checked specific status codes (OK/NO_CONTENT); now any 2xx passes. Usually a good thing, but worth confirming it's deliberate.
Non-blocking
🤔 thinking
- TOCTOU in
rotate_key(request_signing/rotation.rs:70-89): readscurrent-kidandactive-kids, then writes unconditionally. Two concurrent rotations can lose one side's update. Rotation is admin-gated and rare, so probably acceptable — but a read-before-write invariant check would be cheap insurance. parse_ed25519_signing_keylength heuristic is fragile (request_signing/signing.rs:38-56):len > 32→ base64, else raw. The production path always writes base64 viarotation.rs::store_private_key, so a raw value >32 bytes in production means data corruption — and this code hides it behind a misleading "failed to decode base64" error. Consider requiring base64 unconditionally or gating the raw branch behind#[cfg(test)].- Per-call client construction (
adapter-fastly/src/platform.rs:68-76,129-142): everyput/deleteconstructs a freshFastlyManagementApiClient, which re-opensapi-keysand re-registers the management backend. Doc acknowledges it, but for a singlerotate_keycall that's four back-to-back opens to read the same entry. Follow-up could cache anArc<FastlyManagementApiClient>on the stores behind aOnceLock.
♻️ refactor
- Inconsistent admin-endpoint response semantics (
request_signing/endpoints.rs:237,:345):handle_verify_signaturereturns 200 with opaqueverified: falseon internal errors, whilehandle_rotate_key/handle_deactivate_keyreturn 500 witherror: Some(format!("{}", e))stringifying the fullerror-stackreport. These are admin endpoints and presumably auth-gated upstream, but either the error detail should be opt-in behind a debug flag or the contract should be documented. KeyRotationManager::newtakesimpl Into<String>(request_signing/rotation.rs:49): CLAUDE.md's "preferFromoverInto" guidance suggests&strwould be clearer. Minor.
🏕 camp site
SpySecretStore::createunconditionally returnsOk(request_signing/rotation.rs:329-342): a failing-create variant would exercise the rollback path requested in (3). A minimalSpySecretStore::with_create_failure_after(n)would unlock real tests for partial-failure recovery.
⛏ nitpick
Cargo.tomldep ordering (crates/trusted-server-adapter-fastly/Cargo.toml):trusted-server-jsnow sits betweenurlencodingand the alphabetical dependencies.api_key: String(management_api.rs:109): no zeroize-on-drop. Client is short-lived so low-risk, but the previousVec<u8>is gone. Flagging for awareness only.
👍 praise
- Extracting
FastlyApiClientinto the adapter layer is the right layering move —trusted-server-coreis now genuinely platform-agnostic for request signing. parse_active_kidsnow has focused unit tests covering trimming, empty segments, and whitespace-only segments (request_signing/mod.rs:86-128) — directly addresses a prior review comment.SigningPayloadusing structured JSON instead of delimiter-based formats is a real security improvement (prevents signature confusion attacks), and the round-trip tests insigning.rsvalidate it end-to-end.
CI Status
cargo fmt --all -- --check— PASS (local)cargo clippy --workspace --all-targets --all-features -- -D warnings— PASS (local)cargo test --workspace— PASS (local)- integration tests — PASS (GitHub)
- browser integration tests — PASS (GitHub)
- prepare integration artifacts — PASS (GitHub)
aram356
left a comment
There was a problem hiding this comment.
Follow-up with inline comments properly pinned to diff lines. Two corrections to the previous review:
-
Retraction on the "handler tests assert nothing" 🔧: those weak
match { Ok => log / Err => log }tests are pre-existing infeature/edgezero-pr8-content-rewriting; this PR only touched them mechanically to passservicesthrough. Not blocking for PR 9. Reclassifying as a 🏕 for a follow-up: while you're in here, please replace them with real assertions —noop_services()returnsErrdeterministically, so an assertion likeassert_eq!(resp.get_status(), StatusCode::INTERNAL_SERVER_ERROR)on theOk(resp)branch would pin the contract. -
Remaining blockers stand (see inline):
SECRET_UPSERT_METHOD = "PUT"is not a documented Fastly verb — second same-kid rotation will hit the wire and likely 405.rotate_key/delete_keyare non-atomic across four management-API writes with no rollback — partial failures cause signing outages or orphaned key material.
The architectural direction remains the right one; these are lifecycle-correctness holes sitting on top of a clean refactor.
aram356
left a comment
There was a problem hiding this comment.
Summary
Prior review findings on rotation atomicity, POST-based secret upsert, 404-idempotent deletes, base64 contract hardening, and endpoint-test assertions are all resolved cleanly. Blocking concerns on this round are merge-conflict leftovers rather than signing-subsystem bugs.
Blocking
🔧 wrench
- Out-of-scope
proxy.rsregression: per-requestallowed_domainsis silently ignored —redirect_is_permittednow reads fromsettings.proxy.allowed_domainsat both call sites, and the now-deadProxyRequestHeaders::allowed_domainsfield is hidden behind#[allow(dead_code)]. Doc and behavior disagree; integration proxies that defaulted to open mode are now bounded by the global allowlist. (crates/trusted-server-core/src/proxy.rs:440,:604,:731) - Unused
trusted-server-jsdep re-introduced: PR #615 removed it from the adapter as unused; this branch's merge kept it back.base64/serde/serde_json/urlencodingare justified bymanagement_api.rs, buttrusted-server-jsis not referenced anywhere undercrates/trusted-server-adapter-fastly/. (crates/trusted-server-adapter-fastly/Cargo.toml:24)
Non-blocking
🤔 thinking
FastlyManagementApiClientreconstructed on every write — a singlerotate_keypays four opens ofapi-keys+ four backend re-registrations. (platform.rs:68-76,:129-142)- TOCTOU in
rotate_key'sactive-kidsRMW — admin-gated so concurrency is unlikely, but worth a cheap check-and-set. (rotation.rs:94-102) - Write-cost doc leads with the wrong concern — handle-open caching is a footnote; the outbound HTTPS round-trip to
api.fastly.comis the headline. (platform.rs:37-45)
♻️ refactor
make_requestdispatches HTTP method via string literals — typo-prone; takingfastly::http::Methodeliminates the_ =>unreachable branch. (management_api.rs:156-165)signing_store_idsreturns an unnamed(&str, &str)— flagged previously; a named struct would be clearer at both call sites. (endpoints.rs:168-184)
⛏ nitpick
check_responsereads response body on success paths — moveread_to_stringinto the error branch. (management_api.rs:86-94)SECRET_UPSERT_METHODtest is self-referential — asserts a const against its own literal; inline the verb or drop the test. (management_api.rs:366-372)
📌 out of scope
- Plan doc bundled in the diff (+1835 lines) —
docs/superpowers/plans/2026-03-31-pr9-wire-signing-to-store-primitives.mddoubles the reviewer surface area. Plans belong in their own PR or outside review scope.
CI Status
- fmt: PASS
- clippy: PASS
- cargo test: PASS
- vitest: PASS
- format-docs / format-typescript: PASS
- integration tests / browser integration tests: PASS
- CodeQL: PASS
- 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
aram356
left a comment
There was a problem hiding this comment.
Summary
The architectural rewrite is clean — RequestSigner::from_services(&RuntimeServices), KeyRotationManager is infallible, management API writes are adapter-only, and test stubs exercise real crypto. Prior review rounds have addressed the HTTP status handling and Box::leak test-helper issues.
This pass surfaces four correctness/operational concerns around key rotation and the admin surface, plus a test-coverage gap for the rollback paths. None are data-loss bugs, but each can wedge request signing for operators using the admin endpoints.
Blocking
🔧 wrench
- Same-day re-rotation silently invalidates prior signatures (
rotation.rs:66) — inline. - No validation on operator-supplied
kid(endpoints.rs:146, applies to bothrotateanddeactivatehandlers) — inline. - Rollback paths are documented but untested (
rotation.rs:80-129) — inline. - Deactivating or deleting the current-kid bricks signing (
rotation.rs:227-275) — inline. Pre-existing, but this PR finalizes the admin surface.
❓ question
- Rotation resurrects a previously deactivated
previous_kid(rotation.rs:95-99) — inline.
Non-blocking
🌱 seedling
PlatformSecretStore::createtaking&strforces double-base64 on binary keys (crates/trusted-server-core/src/platform/traits.rs:79). The signing key is base64-encoded inrotation::store_private_key, then base64-encoded again bymanagement_api::build_secret_payloadwhen serialized to the Fastly management API JSON payload. Documented and correct, but the root cause isvalue: &strin the trait signature — there's no way to pass opaque bytes. Not for this PR, but worth consideringvalue: &[u8]when the trait is next revved. The Fastly adapter would base64-encode internally, other adapters can choose their encoding, and callers stop double-encoding.
⛏ nitpick
- Transport-failure test reaches into
FastlyManagementApiClientstruct fields (management_api.rs:387-402) — inline.
CI Status
- fmt: PASS
- clippy: PASS
- rust tests: PASS
- js tests: PASS
- browser integration: PASS
- format-typescript / format-docs: PASS
Move logging initialization into Fastly adapter (PR 10) (#610) - 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 ::.
… and Move logging initialization into Fastly adapter (#610) * Rename crates to trusted-server-core and trusted-server-adapter-fastly Rename crates/common → crates/trusted-server-core and crates/fastly → crates/trusted-server-adapter-fastly following the EdgeZero naming convention. Add EdgeZero workspace dependencies pinned to rev 170b74b. Update all references across docs, CI workflows, scripts, agent files, and configuration. * Add platform abstraction layer with traits and RuntimeServices Introduces trusted-server-core::platform with PlatformConfigStore, PlatformSecretStore, PlatformKvStore, PlatformBackend, PlatformHttpClient, and PlatformGeo traits alongside ClientInfo, PlatformError, and RuntimeServices. Wires the Fastly adapter implementations and threads RuntimeServices into route_request. Moves GeoInfo to platform/types as platform-neutral data and adds geo_from_fastly for field mapping. * Address platform layer review feedback - Defer KV store opening: replace early error return with a local UnavailableKvStore fallback so routes that do not need synthetic ID access succeed when the KV store is missing or temporarily unavailable - Use ConfigStore::try_open + try_get and SecretStore::try_get throughout FastlyPlatformConfigStore and FastlyPlatformSecretStore to honour the Result contract instead of panicking on open/lookup failure - Encapsulate RuntimeServices service fields as pub(crate) with public getter methods (config_store, secret_store, backend, http_client, geo) and a pub new() constructor; adapter updated to use new() - Reference #487 in FastlyPlatformHttpClient stub (PR 6 implements it) - Remove unused KvPage re-export from platform/mod.rs - Use super::KvHandle shorthand in RuntimeServices::kv_handle() * Reject host strings containing control characters in BackendConfig * Fix clippy error * Validate scheme and host for control characters in BackendConfig * Address review findings on platform abstraction layer * Address review findings on platform abstraction layer * Add config store read path and storage module split - Split fastly_storage.rs into storage/{config_store,secret_store,api_client,mod}.rs - Add PlatformConfigStore read path via FastlyPlatformConfigStore::get using ConfigStore::try_open/try_get - Add PlatformError::NotImplemented variant; stub write methods on FastlyPlatformConfigStore and FastlyPlatformSecretStore - Add StoreName/StoreId newtypes with From<String>, From<&str>, AsRef<str> - Add UnavailableKvStore to core platform module - Add RuntimeServicesBuilder replacing 7-arg constructor - Migrate get_active_jwks and handle_trusted_server_discovery to use &RuntimeServices - Update call sites in signing.rs, rotation.rs, main.rs - Add success-path test for handle_trusted_server_discovery using StubJwksConfigStore - Fix test_parse_cookies_to_jar_empty typo (was emtpy) * Harden legacy config-store reads and align Fastly adapter stubs * Address storage review feedback * Resolved github-advanced-security bot problems * Address PR review feedback on platform abstraction layer - Make StoreName and StoreId inner fields private; From/AsRef provide all needed construction and access - Add #[deprecated] to GeoInfo::from_request with #[allow(deprecated)] at the three legacy call sites to track migration progress - Enumerate the six platform traits in the platform module doc comment - Extract backend_config_from_spec helper to remove duplicate BackendConfig construction in predict_name and ensure - Replace .into_iter().collect() with .to_vec() on secret plaintext bytes - Remove unused bytes dependency from trusted-server-adapter-fastly - Add comment on SecretStore::open clarifying it already returns Result (unlike ConfigStore::open which panics) * Add PR 4 design spec for secret store trait (read-only) * Clarify test scope and deferred branches in PR 4 spec * Add implementation plan for PR 4 secret store trait * Add test for get_secret_bytes open-failure path * Add NotImplemented tests for FastlyPlatformSecretStore write stubs * Inline StoreId binding and add section comment in write-stub tests * Remove plan * Add PR 6 design spec for backend and HTTP client traits * Address spec review findings on PR 6 design * Implement PlatformHttpClient and thread RuntimeServices through proxy layer - Add PlatformHttpClient trait with send(), send_async(), and select() methods - Add PlatformBackend trait with predict_name() and ensure() methods - Add PlatformResponse wrapper around EdgeZero HTTP responses - Add PlatformPendingRequest and PlatformSelectResult for auction fan-out - Thread RuntimeServices through IntegrationProxy::handle(), IntegrationRegistry::handle_proxy(), and all first-party proxy endpoints so handlers can reach the HTTP client - Add StubHttpClient and StubBackend test stubs with build_services_with_http_client helper - Add proxy_request_calls_platform_http_client_send integration test - Fix proxy_with_redirects to stay within 7-arg clippy limit via ProxyRequestHeaders struct - Document Body::Stream limitation in edge_request_to_fastly with warning log - Document intentional duplication of platform_response_to_fastly across proxy and orchestrator - Remove spec file (promoted to plan + implementation) * Address pr review findings * Resolve pr review findings * Add PR7 design spec for geo lookup + client info extract-once Documents the call site migration plan: five Fastly SDK extraction points in trusted-server-core replaced by RuntimeServices::client_info reads, following Phase 1 injection pattern from the EdgeZero migration design. * Fix spec review issues in PR7 design doc - Correct erroneous claim about generate_synthetic_id being called twice via DeviceInfo; it is called once (line 91 for fresh_id), DeviceInfo.ip is a separate req.get_client_ip_addr() call fixed independently - Add before/after snippet for handle_publisher_request call site in main.rs - Add noop_services import instruction for http_util.rs test module - Clarify _services rename (drop underscore, not add new param) in didomi.rs - Clarify nextjs #[allow(deprecated)] annotations are out of scope (different function) * Update PR7 spec to address all five agent review findings - Change RequestInfo::from_request signature to &ClientInfo (not &RuntimeServices) so prebid can call it with context.client_info - Scope SDK-call acceptance criteria to active non-deprecated code only - List all six AuctionContext construction sites including two production sites in orchestrator.rs and three test helpers in orchestrator/prebid - Add explicit warn-and-continue pattern for publisher.rs geo lookup - Correct testing table: formats.rs and endpoints.rs have no test modules; add orchestrator.rs and prebid.rs test helper update rows * Add PR7 implementation plan and address plan review findings Plan covers 6 tasks in compilation-safe order: AuctionContext struct change first, then from_request signature, then synthetic.rs cascade, then publisher geo, then didomi. Includes two new copy_headers unit tests (Some/None). Spec fixes: clarify injection pattern exceptions for &ClientInfo and Option<IpAddr>; reword acceptance criterion to reflect that provider-layer reads flow through AuctionContext.client_info. * Fix three plan review findings and two open questions - Finding 1 (High): Add missing publisher.rs test call site at line ~695 for get_or_generate_synthetic_id — was omitted from Task 3 Step 6 - Finding 2 (Medium): Remove crate::geo::GeoInfo import from endpoints.rs rather than replacing it — type is not used by name after the change, keeping any import fails clippy -D warnings - Finding 3 (Low): Replace interactive git add -p in Task 6 with explicit file staging instruction - Open Q1: Add Task 2 step to update stale handle_publisher_request signature in auction/README.md - Open Q2: Add Task 2 step to update from_request doc comment to reflect ClientInfo-based TLS detection instead of Fastly SDK calls * Broaden two low-severity doc cleanup steps in PR7 plan - Step 7: cover all four stale Fastly-SDK-specific locations in http_util.rs (SPOOFABLE_FORWARDED_HEADERS doc, RequestInfo struct doc, from_request doc, detect_request_scheme doc) - Step 8: replace the whole routing snippet in auction/README.md, not just the one handle_publisher_request line — handle_auction and integration_registry.handle_proxy are also stale in that snippet * Fix two remaining low findings in PR7 plan - Add missing Location 2 (RequestInfo.scheme field doc, line ~67) to Step 7; renumber subsequent locations 3-5 - Replace &runtime_services with runtime_services in Step 5 and README snippet — runtime_services is already &RuntimeServices in route_request * Fix count drift in Step 7: four → five locations * Add client_info field to AuctionContext and fix all construction sites * Change RequestInfo::from_request to take &ClientInfo, thread services into handle_publisher_request * Add Task 2 follow-up coverage and README route fixes * 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. * 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 rotate/delete atomicity, HTTP verb, idempotent deletes, and weak tests - 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 * Move logging initialization into Fastly adapter (PR 10) (#610) Move logging initialization into Fastly adapter (PR 10) (#610) - 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 ::.
… and Move logging initialization into Fastly adapter (#610) * Rename crates to trusted-server-core and trusted-server-adapter-fastly Rename crates/common → crates/trusted-server-core and crates/fastly → crates/trusted-server-adapter-fastly following the EdgeZero naming convention. Add EdgeZero workspace dependencies pinned to rev 170b74b. Update all references across docs, CI workflows, scripts, agent files, and configuration. * Add platform abstraction layer with traits and RuntimeServices Introduces trusted-server-core::platform with PlatformConfigStore, PlatformSecretStore, PlatformKvStore, PlatformBackend, PlatformHttpClient, and PlatformGeo traits alongside ClientInfo, PlatformError, and RuntimeServices. Wires the Fastly adapter implementations and threads RuntimeServices into route_request. Moves GeoInfo to platform/types as platform-neutral data and adds geo_from_fastly for field mapping. * Address platform layer review feedback - Defer KV store opening: replace early error return with a local UnavailableKvStore fallback so routes that do not need synthetic ID access succeed when the KV store is missing or temporarily unavailable - Use ConfigStore::try_open + try_get and SecretStore::try_get throughout FastlyPlatformConfigStore and FastlyPlatformSecretStore to honour the Result contract instead of panicking on open/lookup failure - Encapsulate RuntimeServices service fields as pub(crate) with public getter methods (config_store, secret_store, backend, http_client, geo) and a pub new() constructor; adapter updated to use new() - Reference #487 in FastlyPlatformHttpClient stub (PR 6 implements it) - Remove unused KvPage re-export from platform/mod.rs - Use super::KvHandle shorthand in RuntimeServices::kv_handle() * Reject host strings containing control characters in BackendConfig * Fix clippy error * Validate scheme and host for control characters in BackendConfig * Address review findings on platform abstraction layer * Address review findings on platform abstraction layer * Add config store read path and storage module split - Split fastly_storage.rs into storage/{config_store,secret_store,api_client,mod}.rs - Add PlatformConfigStore read path via FastlyPlatformConfigStore::get using ConfigStore::try_open/try_get - Add PlatformError::NotImplemented variant; stub write methods on FastlyPlatformConfigStore and FastlyPlatformSecretStore - Add StoreName/StoreId newtypes with From<String>, From<&str>, AsRef<str> - Add UnavailableKvStore to core platform module - Add RuntimeServicesBuilder replacing 7-arg constructor - Migrate get_active_jwks and handle_trusted_server_discovery to use &RuntimeServices - Update call sites in signing.rs, rotation.rs, main.rs - Add success-path test for handle_trusted_server_discovery using StubJwksConfigStore - Fix test_parse_cookies_to_jar_empty typo (was emtpy) * Harden legacy config-store reads and align Fastly adapter stubs * Address storage review feedback * Resolved github-advanced-security bot problems * Address PR review feedback on platform abstraction layer - Make StoreName and StoreId inner fields private; From/AsRef provide all needed construction and access - Add #[deprecated] to GeoInfo::from_request with #[allow(deprecated)] at the three legacy call sites to track migration progress - Enumerate the six platform traits in the platform module doc comment - Extract backend_config_from_spec helper to remove duplicate BackendConfig construction in predict_name and ensure - Replace .into_iter().collect() with .to_vec() on secret plaintext bytes - Remove unused bytes dependency from trusted-server-adapter-fastly - Add comment on SecretStore::open clarifying it already returns Result (unlike ConfigStore::open which panics) * Add PR 4 design spec for secret store trait (read-only) * Clarify test scope and deferred branches in PR 4 spec * Add implementation plan for PR 4 secret store trait * Add test for get_secret_bytes open-failure path * Add NotImplemented tests for FastlyPlatformSecretStore write stubs * Inline StoreId binding and add section comment in write-stub tests * Remove plan * Add PR 6 design spec for backend and HTTP client traits * Address spec review findings on PR 6 design * Implement PlatformHttpClient and thread RuntimeServices through proxy layer - Add PlatformHttpClient trait with send(), send_async(), and select() methods - Add PlatformBackend trait with predict_name() and ensure() methods - Add PlatformResponse wrapper around EdgeZero HTTP responses - Add PlatformPendingRequest and PlatformSelectResult for auction fan-out - Thread RuntimeServices through IntegrationProxy::handle(), IntegrationRegistry::handle_proxy(), and all first-party proxy endpoints so handlers can reach the HTTP client - Add StubHttpClient and StubBackend test stubs with build_services_with_http_client helper - Add proxy_request_calls_platform_http_client_send integration test - Fix proxy_with_redirects to stay within 7-arg clippy limit via ProxyRequestHeaders struct - Document Body::Stream limitation in edge_request_to_fastly with warning log - Document intentional duplication of platform_response_to_fastly across proxy and orchestrator - Remove spec file (promoted to plan + implementation) * Address pr review findings * Resolve pr review findings * Add PR7 design spec for geo lookup + client info extract-once Documents the call site migration plan: five Fastly SDK extraction points in trusted-server-core replaced by RuntimeServices::client_info reads, following Phase 1 injection pattern from the EdgeZero migration design. * Fix spec review issues in PR7 design doc - Correct erroneous claim about generate_synthetic_id being called twice via DeviceInfo; it is called once (line 91 for fresh_id), DeviceInfo.ip is a separate req.get_client_ip_addr() call fixed independently - Add before/after snippet for handle_publisher_request call site in main.rs - Add noop_services import instruction for http_util.rs test module - Clarify _services rename (drop underscore, not add new param) in didomi.rs - Clarify nextjs #[allow(deprecated)] annotations are out of scope (different function) * Update PR7 spec to address all five agent review findings - Change RequestInfo::from_request signature to &ClientInfo (not &RuntimeServices) so prebid can call it with context.client_info - Scope SDK-call acceptance criteria to active non-deprecated code only - List all six AuctionContext construction sites including two production sites in orchestrator.rs and three test helpers in orchestrator/prebid - Add explicit warn-and-continue pattern for publisher.rs geo lookup - Correct testing table: formats.rs and endpoints.rs have no test modules; add orchestrator.rs and prebid.rs test helper update rows * Add PR7 implementation plan and address plan review findings Plan covers 6 tasks in compilation-safe order: AuctionContext struct change first, then from_request signature, then synthetic.rs cascade, then publisher geo, then didomi. Includes two new copy_headers unit tests (Some/None). Spec fixes: clarify injection pattern exceptions for &ClientInfo and Option<IpAddr>; reword acceptance criterion to reflect that provider-layer reads flow through AuctionContext.client_info. * Fix three plan review findings and two open questions - Finding 1 (High): Add missing publisher.rs test call site at line ~695 for get_or_generate_synthetic_id — was omitted from Task 3 Step 6 - Finding 2 (Medium): Remove crate::geo::GeoInfo import from endpoints.rs rather than replacing it — type is not used by name after the change, keeping any import fails clippy -D warnings - Finding 3 (Low): Replace interactive git add -p in Task 6 with explicit file staging instruction - Open Q1: Add Task 2 step to update stale handle_publisher_request signature in auction/README.md - Open Q2: Add Task 2 step to update from_request doc comment to reflect ClientInfo-based TLS detection instead of Fastly SDK calls * Broaden two low-severity doc cleanup steps in PR7 plan - Step 7: cover all four stale Fastly-SDK-specific locations in http_util.rs (SPOOFABLE_FORWARDED_HEADERS doc, RequestInfo struct doc, from_request doc, detect_request_scheme doc) - Step 8: replace the whole routing snippet in auction/README.md, not just the one handle_publisher_request line — handle_auction and integration_registry.handle_proxy are also stale in that snippet * Fix two remaining low findings in PR7 plan - Add missing Location 2 (RequestInfo.scheme field doc, line ~67) to Step 7; renumber subsequent locations 3-5 - Replace &runtime_services with runtime_services in Step 5 and README snippet — runtime_services is already &RuntimeServices in route_request * Fix count drift in Step 7: four → five locations * Add client_info field to AuctionContext and fix all construction sites * Change RequestInfo::from_request to take &ClientInfo, thread services into handle_publisher_request * Add Task 2 follow-up coverage and README route fixes * 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. * 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 rotate/delete atomicity, HTTP verb, idempotent deletes, and weak tests - 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 * Move logging initialization into Fastly adapter (PR 10) (#610) Move logging initialization into Fastly adapter (PR 10) (#610) - 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 ::.
Summary
FastlyConfigStore/FastlySecretStoreconstruction inrequest_signing/withRuntimeServicesplatform traits, making the signing subsystem platform-agnostictrusted-server-coreinto an adapter-onlyFastlyManagementApiClient, enforcing the EdgeZero layering rule that only the adapter may call Fastly-specific APIsRuntimeServicesintoAuctionContextsoPrebidAuctionProvider(and future providers) can callRequestSigner::from_serviceswithout a deprecatedFastlyConfigStoreshimChanges
crates/trusted-server-adapter-fastly/src/management_api.rsFastlyManagementApiClientwithupdate_config_item,delete_config_item,create_secret,delete_secretvia Fastly management APIcrates/trusted-server-adapter-fastly/src/platform.rsFastlyPlatformConfigStore::put/deleteandFastlyPlatformSecretStore::create/deletedelegate toFastlyManagementApiClientcrates/trusted-server-adapter-fastly/src/main.rsservicesinto/verify-signaturehandler; addmod management_apicrates/trusted-server-core/src/request_signing/signing.rsRequestSigner::from_services(&RuntimeServices)replacesfrom_config();verify_signatureaccepts&RuntimeServices; deprecated shim removedcrates/trusted-server-core/src/request_signing/rotation.rsKeyRotationManager::new()now infallible; all methods acceptservices: &RuntimeServicescrates/trusted-server-core/src/request_signing/endpoints.rsservices: &RuntimeServices; tests use in-memory stub storescrates/trusted-server-core/src/auction/types.rsAuctionContextgainspub services: &'a RuntimeServicescrates/trusted-server-core/src/auction/orchestrator.rsservicesthrough derivedAuctionContextconstruction sitescrates/trusted-server-core/src/auction/endpoints.rsserviceswhen constructingAuctionContextcrates/trusted-server-core/src/integrations/prebid.rsRequestSigner::from_services(context.services)— no more#[allow(deprecated)]crates/trusted-server-core/src/platform/test_support.rsbuild_services_with_config_and_secrettest helpercrates/trusted-server-core/src/storage/api_client.rsFastlyApiClientmoved to adapter-onlymanagement_api.rscrates/trusted-server-core/src/storage/mod.rsapi_clientmodule andFastlyApiClientre-exportdocs/superpowers/plans/2026-03-31-pr9-wire-signing-to-store-primitives.mdCloses
Closes #490
Test plan
cargo test --workspacecargo clippy --workspace --all-targets --all-features -- -D warningscargo fmt --all -- --checkcd crates/js/lib && npx vitest runcd crates/js/lib && npm run formatcd docs && npm run formatcargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1fastly compute serveChecklist
unwrap()in production code — useexpect("should ...")tracingmacros (notprintln!)