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

4.4 KiB
Raw Permalink Blame History

@suaveplan/api-router-worker-bridge — request/response marshalling across the worker boundary

Package: @suaveplan/api-router-worker-bridge Status: PROPOSED Type: new-package Stage: 3 (Worker isolation infrastructure)

Why

Per design doc §6: when a plugin runs in isolated or sandboxed tier, the per-request pipeline executes steps 1–7 in the main process (intake, route match, auth, tenant, rate-limit, RBAC, validation, capability scoping), then marshals the result across MessagePort to the worker for step 8 (the user-code handler). Critically: the raw Request object is NEVER marshalled. Only the already-validated body / params / query cross the boundary. This is what lets worker-tier plugins gain isolation without losing any of the host-enforced security guarantees.

api-router-worker-bridge is the pair of half-bridges (main-side + worker-side) that implement that envelope. It sits between the main-process router (api-router-hono in Stage 4) and the worker-side request dispatcher (in api-worker-host).

What changes

Net-new package. Two halves:

ADDED exports:

  • ./main-side — BridgeMain: accepts a route hit from api-host's pipeline; constructs an HttpDispatchEnvelope (validated body, params, query, principal, tenantId, requestId, trace context); sends across the worker port via api-worker-rpc; awaits the response envelope; rehydrates the response into a Response object for the main-side response-validation + compose steps.
  • ./worker-side — BridgeWorker: receives HttpDispatchEnvelope on the worker port; reconstructs a RequestContext (capability proxies already wired by api-worker-host); invokes the route handler; serialises the return value as an HttpResponseEnvelope; supports streaming via Transferable ReadableStream.
  • ./envelope-schemas — Zod schemas for HttpDispatchEnvelope and HttpResponseEnvelope. Validated on both sides.
  • ./streaming-marshal — request-body streams AND response-body streams via Transferable; back-pressure propagated.
  • ./header-allowlist — only headers in the bridge's allowlist (or matching the configurable per-route allowlist) cross the boundary; the rest are dropped on the main side so worker code cannot read Authorization / Cookie etc.
  • ./error-codes — BRIDGE_* codes (BRIDGE_ENVELOPE_INVALID, BRIDGE_HANDLER_THROW, BRIDGE_HEADER_NOT_ALLOWED, BRIDGE_RESPONSE_NOT_SERIALISABLE, BRIDGE_REQUEST_BODY_STREAM_CLOSED).

Out of scope

  • Marshalling the raw Request — explicitly forbidden by §6. The bridge marshals only the validated parts.
  • Routing itself — api-router / api-router-hono own URL matching. This package picks up after step 2.
  • Capability proxies — api-db-worker-proxy etc. live in their own packages. This package only marshals the request envelope.
  • Auth / RBAC — those run in the main process before the bridge. The bridge merely receives the resolved principal and tenantId.

Source mapping

Genesis-original. Design doc sections: §6 (per-request data flow, especially the "isolated / sandboxed" paragraph), §5.2a.

Requirement Design doc section
Raw Request NEVER marshalled — only validated body/params/query §6
Steps 1–7 + 9–11 always main-process §6
Streams transferred via Transferable §6, §5.2a
Auth / RBAC / tenant resolution never run inside worker §6

Verification approach

  • src/main-side/main-side.test.ts — given a fake RouteMatch + validated body, envelope is sent across paired MessageChannel; response rehydrates.
  • src/worker-side/worker-side.test.ts — given an envelope, handler is invoked with reconstructed ctx.request; response envelope serialises.
  • src/envelope-schemas/envelope-schemas.test.ts — Zod schemas reject malformed envelopes; valid envelopes round-trip.
  • src/streaming-marshal/streaming-marshal.test.ts — large request body stream + large response body stream both transfer; back-pressure ack works.
  • src/header-allowlist/header-allowlist.spec-conformance.test.ts — Authorization, Cookie, Set-Cookie MUST NOT cross by default; configurable allowlist works.
  • tests/no-raw-request.test.ts — pin the invariant: the wire envelope never contains a Request object reference; the raw header bag is filtered.

Migration impact

None — additive only. api-host will wire this in Stage 4 once api-router-hono lands.