Files
2026-05-27 23:29:53 +01:00

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 HttpDispatchEnvelope is serialised
  • THEN the structured-clone payload contains no Request instance AND JSON.stringify of 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 no Request prototype 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 headers map has no authorization key
  • WHEN a route attempts to allowlist cookie
  • THEN the bridge throws BRIDGE_HEADER_NOT_ALLOWED at 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_INVALID AND 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 transferList was 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 SuaveplanError with code PLUGIN.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 SuaveplanError with code BRIDGE_HANDLER_THROW AND details.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"