Skip to content

Add Fermyon Spin adapter (wasm32-wasip1)#735

Open
prk-Jr wants to merge 12 commits into
feature/edgezero-pr18-phase5-verificationfrom
feature/edgezero-pr19-spin-adapter
Open

Add Fermyon Spin adapter (wasm32-wasip1)#735
prk-Jr wants to merge 12 commits into
feature/edgezero-pr18-phase5-verificationfrom
feature/edgezero-pr19-spin-adapter

Conversation

@prk-Jr
Copy link
Copy Markdown
Collaborator

@prk-Jr prk-Jr commented May 25, 2026

Summary

  • Introduces trusted-server-adapter-spin — the Fermyon Spin entry point (wasm32-wasip1 component) using edgezero_adapter_spin::run_app with Spin-backed RuntimeServices (config store, KV, secrets, buffered proxy client with gzip/br decompression)
  • Extends parity tests to cover Spin as a third in-process adapter alongside Fastly and Cloudflare
  • Bumps EdgeZero rev to ce6bcf74 (adds edgezero-adapter-spin), Fastly SDK to 0.12, Cloudflare worker to 0.8, and Viceroy to 0.16.5 to resolve bot-analysis hostcall breakage introduced by the SDK bump

Changes

File Change
crates/trusted-server-adapter-spin/ New crate: Spin entry point, edgezero.toml, spin.toml, platform runtime services, route/auth smoke tests, EdgeZero manifest validation test
Cargo.toml / Cargo.lock Add trusted-server-adapter-spin; bump EdgeZero rev, fastly 0.12, log-fastly 0.12, worker 0.8
crates/integration-tests/Cargo.toml / tests/parity.rs Extend parity suite to assert Spin adapter route behaviour matches Fastly and Cloudflare
.cargo/config.toml Add test-spin, check-spin, clippy-spin-native, clippy-spin-wasm aliases
.github/workflows/test.yml Add Spin native and wasm32-wasip1 CI jobs
.github/workflows/format.yml Add Spin clippy job
.github/actions/setup-integration-test-env/action.yml Install Viceroy 0.16.5
.tool-versions Pin Viceroy to 0.16.5
CLAUDE.md Document Spin commands and target-matched lint guidance
crates/trusted-server-adapter-fastly/src/{main,platform,route_tests}.rs Minor updates required by fastly 0.12 API surface
crates/trusted-server-adapter-cloudflare/Cargo.toml Bump worker to 0.8
docs/superpowers/specs/2026-03-19-edgezero-migration-design.md Update migration design notes for Spin phase

Closes

Closes #732

Known MVP limits

  • Spin component variables don't support hyphens, dots, or uppercase in key names — some Trusted Server config keys require manual mapping; authenticated key rotation is not claimed
  • Spin KV TTL is unavailable in the current EdgeZero Spin KV adapter
  • cargo build --workspace intentionally fails (mixed wasm32-wasip1 / wasm32-unknown-unknown targets); target-matched aliases are the correct verification path

Test plan

  • cargo fmt --all -- --check
  • cargo check (native)
  • cargo check-spin (wasm32-wasip1)
  • cargo build --package trusted-server-adapter-spin --target wasm32-wasip1 --features spin --release
  • cargo test-spin (route/auth smoke tests, native host)
  • cargo test-fastly (Viceroy 0.16.5)
  • cargo test-axum
  • cargo test-cloudflare
  • cargo test --manifest-path crates/integration-tests/Cargo.toml --test parity (Fastly + Cloudflare + Spin)
  • cargo clippy-fastly, clippy-axum, clippy-cloudflare, clippy-spin-native, clippy-spin-wasm
  • cargo clippy --manifest-path crates/integration-tests/Cargo.toml -- -D warnings
  • cargo check -p trusted-server-adapter-fastly --target wasm32-wasip1
  • cargo check -p trusted-server-adapter-cloudflare --target wasm32-unknown-unknown --features cloudflare
  • git diff --check
  • Other: spin up --from crates/trusted-server-adapter-spin (requires local Spin CLI install)

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

prk-Jr added 3 commits May 22, 2026 19:57
Adds trusted-server-adapter-spin: Spin entrypoint via
edgezero_adapter_spin::run_app, EdgeZero and Spin runtime manifests,
platform runtime services (null geo, sync Spin variable secret adapter,
conservative buffered HTTP client with gzip/br decompression policy),
route/auth smoke tests, and EdgeZero manifest validation test.

Bumps EdgeZero deps to ce6bcf74b529d9066d08ba87b2971af8379eb29e to
access edgezero-adapter-spin. The new rev requires fastly = "0.12"
(edgezero-adapter-fastly workspace dependency) and worker = "0.8"
(Cloudflare adapter type compatibility). Pins viceroy 0.16.5 to
resolve the Fastly SDK 0.12 bot-analysis hostcall issue.

Extends parity tests to include Spin as a third in-process adapter.
Adds target-matched cargo aliases and CI jobs for Spin native and
wasm32-wasip1 targets. Updates CLAUDE.md lint and build guidance for
the mixed-runtime workspace.

Known MVP limits: Spin component variables do not map cleanly to all
Trusted Server config keys; authenticated key rotation success is not
claimed. Spin KV TTL is unavailable in the current EdgeZero Spin KV
adapter.
@prk-Jr prk-Jr self-assigned this May 25, 2026
@prk-Jr prk-Jr requested review from ChristianPavilonis and aram356 and removed request for aram356 May 25, 2026 13:08
prk-Jr added 4 commits May 27, 2026 17:12
KIDs starting with a digit would alias in the Spin variable encoder
(both "1foo" and "n1foo" map to "v_n1foo"). Blocking them at validation
eliminates the risk without changing the encoding scheme.
- Filter WASI HTTP P2 forbidden outbound headers (host, connection,
  keep-alive, transfer-encoding, upgrade, proxy-connection) before
  building the Spin outbound request; the host header caused every
  proxy request to fail with HeaderError::Forbidden
- Fix push_spin_variable_escape to emit _xhh not _hh; corrects all
  variable names in spin.toml and encoded test expectations
- Prepend n to digit-leading keys in spin_variable_name so Spin's
  letter-led segment requirement is met; aliasing now unreachable
  because validate_kid rejects digit-leading KIDs
- Fix with_capacity to account for the optional n prefix
- Remove redundant localhost entries from allowed_outbound_hosts
  (http://*:* already covers them)
- Update spin.toml encoding comment: collision-free → collision-resistant
Resolve conflict in parity.rs: preserve axum_www_auth binding from base
branch (required by assert_eq! below) and move spin WWW-Authenticate
presence check to end of function, consistent with deactivate test pattern.
Spin's WASI HTTP bridge does not surface the incoming Host header via
IncomingRequest::headers() — the authority is only accessible through
Spin's spin-full-url synthetic header. EdgeZero's into_core_request
forwards all headers including spin-full-url, but the Host header is
absent. extract_request_host() returns "" which causes
classify_response_route to fall back to BufferedUnmodified, skipping
the HTML processor entirely: no TSJS injection, no URL rewriting, no
GTM proxying.

Inject Host from spin-full-url in dispatch before routing, via a
simple host_from_spin_url helper that strips the scheme and path. The
fix is Spin-specific and does not touch shared publisher code.

Also addresses PR review findings: remove build_per_request_services
passthrough wrapper, reduce MAX_DECOMPRESSED_SIZE to 8 MiB, annotate
streaming body limitation, expand validate_kid digit test, upgrade
parity WWW-Authenticate assertions from presence to value equality,
and document the anyhow exception at the WASM FFI boundary.
Copy link
Copy Markdown
Collaborator

@aram356 aram356 left a comment

Choose a reason for hiding this comment

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

Summary

Adds a Fermyon Spin entry-point crate (wasm32-wasip1 component) with an EdgeZero manifest, platform runtime services, conservative response policy (hop-by-hop sanitization, gzip/br decompression with a 64 MiB cap), route/auth smoke tests, and a three-way parity test extension. Bumps the EdgeZero rev plus fastly 0.12, worker 0.8, and Viceroy 0.16.5 to track the new SDK surface.

Overall the adapter is well-tested with documented MVP limits, but the Spin variable encoder makes safety claims it doesn't enforce, and outbound HTTP has no timeout. Requesting changes on those two items; the rest are non-blocking suggestions.

Blocking

🔧 wrench

  • spin_variable_name contract is broader than its safety claim: the comment + test name anchor on validate_kid, but the encoder is called by config and secret stores with arbitrary keys. Two reachable foot-guns — digit-leading aliasing and adjacent-underscore invalidity for uppercase/_/-/./: -leading keys. Fix by rejecting at the encoder boundary. (crates/trusted-server-adapter-spin/src/platform.rs:138-176)
  • No outbound HTTP timeout on Spin: spin_sdk::http::send is called with no timeout; NoopBackend discards PlatformBackendSpec::first_byte_timeout. Either honor it via a race-with-delay future or document the limitation explicitly. (crates/trusted-server-adapter-spin/src/platform.rs:399-444)

Non-blocking

♻️ refactor

  • startup_error_router skips AuthMiddleware and methods beyond GET/POST — the fallback router leaks startup error text on admin routes without a 401 challenge, and non-GET/POST methods bypass the error message entirely. (crates/trusted-server-adapter-spin/src/app.rs:128-154)
  • Duplicate /first-party/sign handlersfp_sign_get_handler and fp_sign_post_handler are byte-identical closures. (crates/trusted-server-adapter-spin/src/app.rs:264-288)

🤔 thinking

  • Multi-provider fan-out rejected at request time onlyselect returns an error when 2+ providers are submitted, but operators won't discover this until /auction traffic hits. A startup-time warning when adapter + provider count are incompatible would surface the limit sooner. (crates/trusted-server-adapter-spin/src/platform.rs:507-547)
  • SpinSecretStoreAdapter UTF-8 and plaintext constraints — Spin variables are UTF-8 strings and unencrypted in the manifest by default. Today's JSON-encoded signing keys work; binary secrets would silently break, and production deployments must back the variables with a real secret-provider source. Worth a # Limitations rustdoc section. (crates/trusted-server-adapter-spin/src/platform.rs:570-598)

⛏ nitpick

  • with_capacity under-allocates — escapes are 4 bytes per char; current bound assumes 1:1. (platform.rs:150)
  • Test name overpromisesspin_variable_name_digit_prefix_aliasing_is_unreachable_for_valid_kids is true only for callers that pre-validate. Pair with an encoder-level test or rename. (platform.rs:980)

🌱 seedling

  • No live Spin-runtime CI gate — the PR test plan's spin up --from ... is intentionally unchecked. Today's CI builds and runs native-host route tests; a follow-up could exercise the WASM artifact under a real Spin runtime (the analog of Viceroy for Fastly).

📌 out of scope

  • clippy-cloudflare added to format.yml — closes an existing gap but is orthogonal to the Spin adapter. Calling it out so it isn't lost.

CI Status

  • fmt: PASS
  • clippy (Fastly / Axum / Cloudflare / Spin native + wasm32-wasip1): PASS
  • cargo test (Fastly via Viceroy): PASS
  • cargo test (Axum native): PASS
  • cargo test (Cloudflare native): PASS
  • cargo check/build (Spin native + wasm32-wasip1): PASS
  • cargo test (cross-adapter parity): PASS
  • vitest: PASS
  • format-docs / format-typescript: PASS
  • integration + browser tests: PASS

Comment on lines +138 to +176
fn spin_variable_name(
key: &str,
error_context: PlatformError,
) -> Result<String, Report<PlatformError>> {
if key.is_empty() {
return Err(Report::new(error_context).attach("Spin variable key must not be empty"));
}

let mut chars = key.chars().peekable();
let digit_leading = chars.peek().is_some_and(char::is_ascii_digit);

// `v_` prefix + optional `n` for digit-leading keys + encoded body.
let mut out = String::with_capacity(key.len() + 2 + usize::from(digit_leading));
out.push_str("v_");

// Spin requires each _-separated word to start with an ASCII letter.
// If the key starts with a digit, prefix with 'n' so the first word is letter-led.
// `validate_kid` rejects digit-leading KIDs, so this branch is unreachable for
// all operator-supplied and system-generated keys; it is kept as a safety net.
if digit_leading {
out.push('n');
}

for ch in chars {
match ch {
'a'..='z' | '0'..='9' => out.push(ch),
'A'..='Z' | '-' | '_' | '.' | ':' => {
push_spin_variable_escape(&mut out, ch as u8);
}
_ => {
return Err(Report::new(error_context).attach(format!(
"Spin variable key `{key}` contains unsupported character `{ch}`"
)));
}
}
}

Ok(out)
}
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.

🔧 wrenchspin_variable_name contract is broader than the validation that protects it, and the doc comment + test name advertise safety that doesn't extend to non-KID callers.

Two concrete problems:

  1. Digit-leading aliasing is reachable. The comment at L156 anchors the safety claim on validate_kid, but this function is also called by ConfigStoreHandleAdapter::get and spin_secret_variable_name with arbitrary keys. Any caller with a digit-leading key collides with its n-prefixed sibling (1foo and n1foo both → v_n1foo). Today's keys are safe; one operator-defined config key starting with a digit silently aliases.

  2. Uppercase/leading-_/-/./: keys produce names this code itself says are invalid. The function's own comment (L153) states "Spin requires each _-separated word to start with an ASCII letter." But escaping the first char produces adjacent underscores: "Foo""v__x46oo", segments ["v", "", "x46oo"] — empty middle segment. The encoder never rejects these.

Fix — reject at the encoder boundary (mirror the empty-key rejection and validate_kid's defense):

if key.is_empty() {
    return Err(Report::new(error_context).attach("Spin variable key must not be empty"));
}
if !key.starts_with(|c: char| c.is_ascii_lowercase()) {
    return Err(Report::new(error_context).attach(format!(
        "Spin variable key `{key}` must start with a lowercase ASCII letter"
    )));
}

Then drop the n-prefix branch (now unreachable) and remove the aliasing test — the contract is enforced at the type boundary rather than relying on every caller to pre-validate.

Comment on lines +440 to +444
let spin_response: spin_sdk::http::Response =
spin_sdk::http::send(spin_request).await.map_err(|e| {
Report::new(PlatformError::HttpClient)
.attach(format!("outbound request to {uri} failed: {e}"))
})?;
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.

🔧 wrench — No outbound HTTP timeout.

spin_sdk::http::send is called with no timeout configuration. PlatformBackendSpec::first_byte_timeout flows to the Fastly adapter, but NoopBackend here discards it. A slow or hung origin will block the Spin invocation for whatever default the runtime imposes, with no way for operators to tune it.

Fix options:

  1. Honor first_byte_timeout by racing the spin_sdk::http::send future against a delay (using whatever async timer Spin provides), returning a typed PlatformError::HttpClient on expiry. Even a single hard-coded timeout (e.g., 15s) is materially better than "whatever Spin defaults to".
  2. If Spin's outbound HTTP truly has no cancellation primitive, document this explicitly in the SpinPlatformHttpClient rustdoc and the PR's "Known MVP limits" so operators don't expect Fastly-like timeout semantics.

Comment on lines +128 to +154
fn startup_error_router(e: &Report<TrustedServerError>) -> RouterService {
let message = Arc::new(format!("{}\n", e.current_context().user_message()));
let status = e.current_context().status_code();

let make = move |msg: Arc<String>| {
move |_ctx: RequestContext| {
let body = edgezero_core::body::Body::from((*msg).clone());
let mut resp = Response::new(body);
*resp.status_mut() = status;
resp.headers_mut().insert(
header::CONTENT_TYPE,
HeaderValue::from_static("text/plain; charset=utf-8"),
);
async move { Ok::<Response, EdgeError>(resp) }
}
};

RouterService::builder()
.middleware(FinalizeResponseMiddleware::new(Arc::new(
Settings::default(),
)))
.get("/", make(Arc::clone(&message)))
.post("/", make(Arc::clone(&message)))
.get("/{*rest}", make(Arc::clone(&message)))
.post("/{*rest}", make(Arc::clone(&message)))
.build()
}
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.

♻️ refactorstartup_error_router diverges from the main router in two ways that have user-visible consequences.

  1. No AuthMiddleware. When startup fails, anonymous callers hitting /admin/keys/rotate get the startup error body (e.g. "missing signing storage configuration") without a 401 challenge. The main router gates this; the fallback should too. Either add AuthMiddleware::new(...) to the builder, or return a generic body like "service unavailable" so the deployment state isn't leaked.

  2. Only GET/POST registered. PUT/DELETE/PATCH on /{*rest} will return 405 (or 404) instead of the startup error, so a misbehaving client probing with non-GET/POST methods sees inconsistent behaviour between healthy and degraded states. Either bind those methods too, or use a method-agnostic catch-all if RouterService supports one.

This is non-blocking, but the fallback router is exactly the surface that gets exercised when something's already wrong — the bar for its consistency with the main router is higher than usual.

Comment on lines +264 to +288
// GET /first-party/sign
let s = Arc::clone(&state);
let fp_sign_get_handler = move |ctx: RequestContext| {
let s = Arc::clone(&s);
async move {
let services = build_per_request_services(&ctx);
let req = ctx.into_request();
Ok(handle_first_party_proxy_sign(&s.settings, &services, req)
.await
.unwrap_or_else(|e| http_error(&e)))
}
};

// POST /first-party/sign
let s = Arc::clone(&state);
let fp_sign_post_handler = move |ctx: RequestContext| {
let s = Arc::clone(&s);
async move {
let services = build_per_request_services(&ctx);
let req = ctx.into_request();
Ok(handle_first_party_proxy_sign(&s.settings, &services, req)
.await
.unwrap_or_else(|e| http_error(&e)))
}
};
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.

♻️ refactorfp_sign_get_handler and fp_sign_post_handler are byte-identical closures both calling handle_first_party_proxy_sign. Build one closure and .clone() it for both route bindings (same pattern used for get_fallback/post_fallback later in the file).

Comment on lines +507 to +547
async fn select(
&self,
mut pending_requests: Vec<PlatformPendingRequest>,
) -> Result<PlatformSelectResult, Report<PlatformError>> {
if pending_requests.is_empty() {
return Err(Report::new(PlatformError::HttpClient)
.attach("select called with an empty pending_requests list"));
}

if pending_requests.len() >= 2 {
return Err(Report::new(PlatformError::HttpClient).attach(format!(
"SpinPlatformHttpClient: multi-provider fan-out is not supported \
({} providers submitted). Configure a single auction provider \
or use the Fastly adapter for parallel DSP fan-out.",
pending_requests.len()
)));
}

let ready_platform = pending_requests.remove(0);
let pending = ready_platform
.downcast::<SpinPendingResponse>()
.map_err(|_| {
Report::new(PlatformError::HttpClient)
.attach("unexpected inner type in SpinPlatformHttpClient::select")
})?;

let mut builder = edgezero_core::http::response_builder().status(pending.status);
for (name, value) in &pending.headers {
builder = builder.header(name.as_str(), value.as_slice());
}
let edge_resp = builder
.body(edgezero_core::body::Body::from(pending.body))
.change_context(PlatformError::HttpClient)?;

let ready = Ok(PlatformResponse::new(edge_resp).with_backend_name(pending.backend_name));
Ok(PlatformSelectResult {
ready,
remaining: pending_requests,
})
}
}
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 — Rejecting fan-out at request time is fine as a safety net, but operators won't discover the constraint until a /auction request fails in production.

Consider an additional startup-time check (e.g. in IntegrationRegistry::new or build_orchestrator) that warns when the active adapter doesn't support fan-out and more than one provider is configured. Not a blocker, but the failure mode is currently "silent until traffic hits".

Comment on lines +570 to +598
#[cfg(all(feature = "spin", target_arch = "wasm32"))]
struct SpinSecretStoreAdapter;

#[cfg(all(feature = "spin", target_arch = "wasm32"))]
impl PlatformSecretStore for SpinSecretStoreAdapter {
fn get_bytes(
&self,
store_name: &StoreName,
key: &str,
) -> Result<Vec<u8>, Report<PlatformError>> {
let variable_name = spin_secret_variable_name(store_name, key)?;
match spin_sdk::variables::get(&variable_name) {
Ok(value) => Ok(value.into_bytes()),
Err(error) => Err(Report::new(PlatformError::SecretStore).attach(format!(
"secret lookup failed for key `{key}` as Spin variable `{variable_name}`: {error}"
))),
}
}

fn create(&self, _: &StoreId, _: &str, _: &str) -> Result<(), Report<PlatformError>> {
Err(Report::new(PlatformError::SecretStore)
.attach("secret store writes are not supported on Spin"))
}

fn delete(&self, _: &StoreId, _: &str) -> Result<(), Report<PlatformError>> {
Err(Report::new(PlatformError::SecretStore)
.attach("secret store deletes are not supported on Spin"))
}
}
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.

🤔 thinkingSpinSecretStoreAdapter reads via spin_sdk::variables::get which returns a String. Two implications worth documenting in the rustdoc:

  1. UTF-8 only. value.into_bytes() works fine for today's JSON-encoded signing keys, but any future binary secret (e.g., raw Ed25519 bytes) will silently fail at the Spin runtime layer (variables are UTF-8 strings).
  2. Plaintext by default. Component variables aren't encrypted at rest in the manifest. Production deployments must back them with a real secret-provider source. The PR body's "authenticated key rotation is not claimed" line is the right disclaimer but doesn't cover the at-rest concern.

Adding a short # Limitations section to the adapter's rustdoc would make these constraints discoverable without reading the EdgeZero source.

let digit_leading = chars.peek().is_some_and(char::is_ascii_digit);

// `v_` prefix + optional `n` for digit-leading keys + encoded body.
let mut out = String::with_capacity(key.len() + 2 + usize::from(digit_leading));
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.

nitpickwith_capacity under-allocates whenever any char needs an _xhh escape (4 bytes per char). A current-kid-style key reallocates mid-build. key.len() * 4 + 3 covers the worst case.

}

#[test]
fn spin_variable_name_digit_prefix_aliasing_is_unreachable_for_valid_kids() {
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.

nitpick — Test name overpromises. spin_variable_name is reachable from non-KID callers (config and secret stores), so "unreachable for valid kids" leaves the actual safety conditional on every caller. Either rename to ..._is_unreachable_for_kids_passing_validate_kid, or pair this with a test asserting that the encoder itself rejects digit-leading input — see the encoder finding above.

Copy link
Copy Markdown
Collaborator

@ChristianPavilonis ChristianPavilonis left a comment

Choose a reason for hiding this comment

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

Summary

Submitting the review findings as a comment review. CI is passing. I did not duplicate the existing inline review comments for the Spin variable encoder, missing outbound timeout, or startup fallback because those are already covered by an existing changes-requested review on this PR.

The additional findings below are folded into the review body because their referenced lines are outside this stacked PR's current GitHub diff, so GitHub cannot attach them as inline comments on PR #735.

Additional findings

P1 — proxy-rebuild can mint valid click redirects without validating the original token

crates/trusted-server-core/src/proxy.rs:1107

handle_first_party_proxy_rebuild extracts tsurl, ignores any supplied tstoken, then signs a new /first-party/click URL. Because this route is public, an attacker can create valid click redirects to arbitrary URLs and, when a victim follows them, handle_first_party_click may append the victim's EC ID to the attacker-controlled redirect target.

Suggestion: Validate payload.tsclick with reconstruct_and_validate_signed_target before applying changes, reject missing/invalid tstoken, and enforce settings.proxy.allowed_domains in the click redirect path before returning Location.

P2 — Cloudflare integration build uses stale SYNTHETIC env var

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

The Cloudflare build step sets TRUSTED_SERVER__SYNTHETIC__SECRET_KEY, but the current settings field is edge_cookie.secret_key. The override is ignored, so the Cloudflare integration artifact is built with the default config value instead of the intended integration-test secret.

Suggestion: Replace it with:

TRUSTED_SERVER__EDGE_COOKIE__SECRET_KEY: integration-test-secret-key

P3 — Public docs/README do not reflect Spin or target-matched commands

docs/guide/architecture.md:7, README.md:51

The public docs still describe only Fastly/Axum runtimes and recommend broad workspace clippy commands that the project now documents as problematic for mixed runtimes.

Suggestion: Update README and guide pages to include Spin commands (cargo test-spin, cargo check-spin, the Spin build command) and target-matched clippy aliases.

Existing findings already covered by prior review

  • P1 — Spin variable encoder accepts unsafe key shapes (crates/trusted-server-adapter-spin/src/platform.rs).
  • P1 — Spin outbound HTTP ignores configured timeouts (crates/trusted-server-adapter-spin/src/platform.rs).
  • P2 — Spin startup fallback bypasses admin auth and only handles GET/POST (crates/trusted-server-adapter-spin/src/app.rs).

prk-Jr added 4 commits May 29, 2026 15:24
Blocking:
- Reject keys not starting with a lowercase ASCII letter at the
  spin_variable_name encoder boundary; removes the aliasing-prone
  digit-leading n-prefix branch and fixes with_capacity to worst-case
  (key.len() * 4 + 3). Updates affected tests to assert rejection.
- Document the no-configurable-outbound-timeout limitation in
  SpinPlatformHttpClient rustdoc; PlatformBackendSpec::first_byte_timeout
  is ignored by NoopBackend and Spin's http::send has no per-request
  timeout API.

Non-blocking:
- startup_error_router now logs the error and returns a generic
  "Service Unavailable" body so deployment state is not leaked to
  anonymous callers; adds PUT/DELETE handlers for all catch-all routes.
- Collapse duplicate fp_sign_get_handler/fp_sign_post_handler into one
  closure + clone, matching the get_fallback/post_fallback pattern.
- Add # Limitations rustdoc section to SpinSecretStoreAdapter covering
  UTF-8-only variables and plaintext-at-rest in the default manifest.

Security (P1 — proxy-rebuild):
- handle_first_party_proxy_rebuild now validates the tstoken on the
  original tsclick URL via reconstruct_and_validate_signed_target before
  applying any parameter mutations. Without this an attacker could submit
  an unsigned tsclick and mint valid click redirects to arbitrary URLs.
- Enforce settings.proxy.allowed_domains on tsurl before issuing the
  new signed redirect.

CI fix (P2):
- Replace stale TRUSTED_SERVER__SYNTHETIC__SECRET_KEY env var with
  TRUSTED_SERVER__EDGE_COOKIE__SECRET_KEY in the Cloudflare integration
  build step; the previous key was ignored, leaving the artifact built
  with the default config value.
- README Quick Start: add cargo test-spin
- README Development: replace broad workspace clippy with target-matched
  aliases and note why; add cargo test-spin to test list
- architecture.md: mention Cloudflare/Spin in the high-level overview;
  add trusted-server-adapter-spin section with build/test/lint commands
  and known MVP limits; expand Runtime Targets table to include all four
  adapters; add note on target-matched clippy requirement
Test was constructing a tsclick URL without a signature. Now that
handle_first_party_proxy_rebuild validates the tstoken via
reconstruct_and_validate_signed_target, the test must supply a properly
signed click URL. Compute the token with compute_encrypted_sha256_token
over tsurl + original params before building tsclick.
- Collapse nested if/if-let/if-let into a single let-chain condition
  (collapsible_if, Rust 2024 edition let chains)
- Move host_from_spin_url test module to end of file so no items follow
  the test module (items_after_test_module)
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.

3 participants