testing/cross_backend/transport_composition.ts

Cross-backend front-door contract: what every transport answers before its request reaches per-action dispatch.

Each spine composes its front door differently. The TS spine mounts one stack at /api/* — body limit, then origin, then session and bearer — and each surface adds its own gates behind it. The Rust spine stacks global layers (body limit, then origin) and has each handler resolve its credential and run its own gates. Two compositions can agree on every per-surface suite and still disagree on which refusal answers when a request trips several, so this suite pins the answer per transport as a table:

surfaceforeign Originunreadable Originduplicated Originover cap + foreign Originno credentialnarrowed token
REST POST (/logout)403 forbidden_origin403 forbidden_origin403 forbidden_origin413 payload_too_large401 authentication_required403 credential_type_required
REST POST (/password)————401 authentication_required—
RPC POST403 forbidden_origin403 forbidden_origin403 forbidden_origin413 payload_too_large401 authentication_required403 token_scope_required (rpc:<method>)
RPC GET403 forbidden_origin403 forbidden_origin403 forbidden_origin—401 authentication_required403 token_scope_required (rpc:<method>)
WS upgrade403 forbidden_origin403 forbidden_origin403 forbidden_origin—401 authentication_required403 token_scope_required (surface:ws_upgrade)
SSE (audit stream)403 forbidden_origin403 forbidden_origin403 forbidden_origin—401 authentication_required403 credential_type_required
  • foreign Origin carries the keeper's session cookie, so only the origin check can refuse it — a front door missing that check answers the surface's success instead.
  • unreadable Origin is the allowed origin with a byte past ASCII (0xE9), with the keeper's session — a check that reads such a value as absent lets it through.
  • duplicated Origin sends the allowed origin on two header lines, with the keeper's session — refused whether a backend sees the two lines (the Rust spine) or one value joined from them (the TS runtimes), so a check that reads only the first lets it through.
  • over cap + foreign Origin declares a Content-Length one byte over the shared 1 MiB cap and carries no credential: the cap answers on the header, ahead of both the origin check and auth. Only the surfaces that take a body have the column.
  • no credential sends an allowed Origin and nothing else.
  • narrowed token sends a bearer minted with a method-scoped scope, and no Origin (a bearer in a browser context is discarded, not refused). The RPC probe calls a method the scope doesn't name. A narrowed token reaches no non-RPC surface, but on a session-only surface the credential gate answers first — the REST probe is session-only /logout, and the audit stream is session-only on both spines.

Each case asserts the status and the refusal body: a flat {error} (plus its named fields) or, past the RPC front door, the JSON-RPC error envelope with the reason on error.data.reason. A WebSocket refusal's body is required where the backend declares capabilities.ws_refusal_body; where it doesn't (Node's @hono/node-ws drops a refused upgrade's body), an empty body passes on its status and a body that is sent is still asserted.

Transport. Given a base_url, every probe is written to the socket (raw_http_exchange): an upgrade has to be, and an over-cap request sends its head alone, as body_size.ts explains — the 413 must arrive without a body byte. With base_url: null the probes go through the fixture's in-process transport, whole bodies included, and the WebSocket rows skip (there is no socket to upgrade).

Wire it for any backend that serves the spine's account routes, RPC endpoint, WebSocket endpoint and audit stream; the WS and SSE rows gate on capabilities.ws and capabilities.sse.

view source

Declarations
#

7 declarations

describe_transport_composition_cross_tests
#

testing/cross_backend/transport_composition.ts view source

(options: TransportCompositionCrossTestOptions): void import {describe_transport_composition_cross_tests} from '@fuzdev/fuz_app/testing/cross_backend/transport_composition.js';

Register the front-door table's cases — one test per cell, each on a fresh fixture.

This is the front-door contract: a backend that wires it answers each transport's probes as the table says — when one request trips several refusals, the one the table names answers.

options

returns

void

transport_composition_cases
#

TransportCompositionCase
#

testing/cross_backend/transport_composition.ts view source

TransportCompositionCase import type {TransportCompositionCase} from '@fuzdev/fuz_app/testing/cross_backend/transport_composition.js';

One cell of the front-door table.

surface

type TransportCompositionSurface

readonly

probe

type TransportCompositionProbe

readonly

expect

type TransportCompositionExpect

readonly

gate?

The capability the surface needs; the case skips on a backend without it.

type "sse" | "ws" | "cell_crud" | "cell_relations" | "account_lifecycle" | "fact_serving" | "ready" | "account_status" | "peer_request" | "ws_handshake_pipelining" | "ws_account_actions" | "ws_self_revocation_reply" | "cell_gated_create" | "ws_role_gated" | "ws_refusal_body"

readonly

TransportCompositionCrossTestOptions
#

testing/cross_backend/transport_composition.ts view source

TransportCompositionCrossTestOptions import type {TransportCompositionCrossTestOptions} from '@fuzdev/fuz_app/testing/cross_backend/transport_composition.js';

inheritance

base_url

Base URL the spawned backend is reachable at, or null for an in-process run. Given a URL, every probe is written to the socket; with null the probes go through the fixture's transport and the WebSocket rows skip.

type string | null

readonly

origin?

An Origin the backend allows. Default: the origin of base_url, or http://localhost:5173 in-process (which create_test_app's allow list admits).

type string

readonly

rest_path?

Session-only REST POST route the REST row probes. Default /api/account/logout.

type string

readonly

password_path?

Password-change route the /password row probes. Default /api/account/password.

type string

readonly

ws_path?

WebSocket endpoint path. Default /api/ws.

type string

readonly

sse_path?

Audit-log SSE path. Default /api/admin/audit/stream.

type string

readonly

TransportCompositionExpect
#

testing/cross_backend/transport_composition.ts view source

TransportCompositionExpect import type {TransportCompositionExpect} from '@fuzdev/fuz_app/testing/cross_backend/transport_composition.js';

The refusal a case expects.

status

type number

readonly

error

The refusal reason — the flat body's error, or the envelope's error.data.reason.

type string

readonly

fields?

Further fields the refusal must carry, beside the reason.

type Readonly<Record<string, unknown>>

readonly

envelope?

true when the refusal is a JSON-RPC error envelope rather than a flat {error} body.

type boolean

readonly

TransportCompositionProbe
#

testing/cross_backend/transport_composition.ts view source

TransportCompositionProbe

type "foreign_origin" | "unreadable_origin" | "duplicated_origin" | "over_cap_foreign_origin" | "no_credential" | "narrowed_token"

import type {TransportCompositionProbe} from '@fuzdev/fuz_app/testing/cross_backend/transport_composition.js';

What a probe sends, as the table's columns name it.

TransportCompositionSurface
#

testing/cross_backend/transport_composition.ts view source

TransportCompositionSurface

type "rest_post" | "rest_password" | "rpc_post" | "rpc_get" | "ws_upgrade" | "sse"

import type {TransportCompositionSurface} from '@fuzdev/fuz_app/testing/cross_backend/transport_composition.js';

A transport's front door, as the table's rows name it.

Depends on
#