6.2 KiB
Capability Spec Delta: api-router-worker-bridge (api-router-worker-bridge)
Package: @suaveplan/api-router-worker-bridge
Status: PROPOSED
Every requirement below carries a
**Verified by:**line at authoring time.
ADDED Requirements
Requirement: Raw Request object never crosses the worker boundary
The package SHALL marshal only the already-validated body, params, query, plus a filtered headers map, the resolved principal, tenantId, requestId, and W3C trace context across the worker port. The raw Request object MUST NOT appear in the wire envelope under any circumstance. Worker-side reconstruction SHALL produce a ctx.request shape that exposes ONLY these fields — there is no way for worker code to read the original socket, the original Headers instance, or any unfiltered header.
Scenario: envelope wire shape contains no Request reference
- WHEN an
HttpDispatchEnvelopeis serialised - THEN the structured-clone payload contains no
Requestinstance ANDJSON.stringifyof the envelope succeeds (proving plain-data shape)
Scenario: worker ctx.request lacks underlying Request access
- WHEN a handler running under the bridge inspects
ctx.request - THEN the available keys are exactly
["body", "params", "query", "headers", "method", "url"]AND noRequestprototype is reachable
Verified by: tests/no-raw-request.test.ts::"envelope contains no Request reference" AND src/worker-side/worker-side.test.ts::"ctx.request exposes only marshalled fields"
Requirement: Default header allowlist denies credentials-bearing headers
The package SHALL apply a default deny list to outgoing envelopes that includes authorization, cookie, set-cookie, x-csrf-token, and proxy-authorization. Headers on the deny list MUST NOT appear in envelope.headers regardless of route configuration. Routes MAY EXTEND the allowlist via configuration to include arbitrary headers but MUST NOT REMOVE any default-denied header from the deny list.
Scenario: Authorization is stripped from envelope by default
- WHEN a route hit carries
Authorization: Bearer abc - THEN the resulting envelope's
headersmap has noauthorizationkey
Scenario: Cookie cannot be allowlisted via config
- WHEN a route attempts to allowlist
cookie - THEN the bridge throws
BRIDGE_HEADER_NOT_ALLOWEDat configuration time
Verified by: src/header-allowlist/header-allowlist.spec-conformance.test.ts::"Authorization stripped by default" AND src/header-allowlist/header-allowlist.spec-conformance.test.ts::"cookie cannot be allowlisted"
Requirement: Envelope schemas are Zod-validated on both sides
The package SHALL define Zod schemas for HttpDispatchEnvelope and HttpResponseEnvelope. Main-side dispatch() MUST validate before sending. Worker-side serve() MUST validate on receipt. Validation failure on either side MUST throw BRIDGE_ENVELOPE_INVALID with the Zod issue tree in details.
Scenario: malformed envelope is rejected
- WHEN a non-object value is sent across the port
- THEN the receiving side throws
BRIDGE_ENVELOPE_INVALIDAND the request promise rejects
Verified by: src/envelope-schemas/envelope-schemas.test.ts::"malformed envelope throws BRIDGE_ENVELOPE_INVALID"
Requirement: Request and response streams transfer via Transferable
The package SHALL marshal request-body ReadableStream and response-body ReadableStream via Transferable so chunks transfer zero-copy. Back-pressure SHALL be propagated end-to-end via api-worker-rpc's streaming ack protocol — the consumer's slow read MUST throttle the producer.
Scenario: 16 MiB request stream round-trips
- WHEN the main side dispatches with a 16 MiB request body stream
- THEN the worker handler observes the full byte sequence AND
transferListwas used (verified by absence of allocation in main process after transfer)
Scenario: response stream backpressure throttles producer
- WHEN the main side reads slowly from a worker-produced response stream
- THEN the worker-side producer stalls on missing acks
Verified by: src/streaming-marshal/streaming-marshal.test.ts::"16 MiB request stream round-trips" AND src/streaming-marshal/streaming-marshal.test.ts::"response stream backpressure throttles producer"
Requirement: Bridge passes the router conformance suite under the worker-proxy surface
The package SHALL be runnable as the surface argument to runRouterConformance from @suaveplan/api-router/testing. Every router invariant (route match, param extraction, middleware order, error propagation, etc.) MUST hold across the bridge identically to a direct in-process router. This is the Stage-3 gate that lets Stage 4 declare router conformance "both surfaces".
Scenario: router conformance suite passes against bridge
- WHEN
runRouterConformance("worker-bridge", makeBridgeFactory)runs - THEN every conformance scenario passes
Verified by: tests/conformance/router-conformance.test.ts::"runRouterConformance passes against bridge"
Requirement: Handler throw propagates as SuaveplanError without leaking handler internals
The package SHALL forward worker-side handler throws back across the bridge via api-worker-rpc's error-reconstruction. SuaveplanError instances MUST round-trip with code + cause. Plain Error throws MUST be wrapped in SuaveplanError with code BRIDGE_HANDLER_THROW. The wire envelope MUST NOT carry the raw Error.stack as a live property — only as a details.remoteStack string.
Scenario: SuaveplanError round-trips with code preserved
- WHEN a worker-side handler throws
SuaveplanError("PLUGIN.X.NOT_FOUND") - THEN the main side receives a
SuaveplanErrorwith codePLUGIN.X.NOT_FOUND
Scenario: plain Error wraps as BRIDGE_HANDLER_THROW
- WHEN a worker-side handler throws
new RangeError("boom") - THEN the main side receives
SuaveplanErrorwith codeBRIDGE_HANDLER_THROWANDdetails.remoteName === "RangeError"
Verified by: src/worker-side/worker-side.test.ts::"SuaveplanError round-trips with code" AND src/worker-side/worker-side.test.ts::"plain Error wraps as BRIDGE_HANDLER_THROW"