first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,65 @@
# `handshake`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Freezes the per-request pipeline result onto a per-connection `HandshakeOutcome`. The host runs the standard pipeline steps 1–7 (auth + tenant resolution + capability scoping) and passes the resolved principal, tenant id, capability set, and token expiry into `runHandshake`. The function snapshots that result into an immutable record the rest of the connection lifecycle treats as the source of truth — the principal cannot mutate from this point on.
Subprotocol negotiation also happens here, following RFC 6455 §4.1: the server picks the first protocol from its supported list that the client requested. If the client requested a subprotocol the server does not support, the handshake throws `WS_BUN.SUBPROTOCOL_UNSUPPORTED`.
## Features
- Immutable snapshot of principal + tenant + capabilities.
- RFC 6455 subprotocol negotiation with explicit unsupported-rejection.
- Throws `WS_BUN.HANDSHAKE_REJECTED` for the obvious "missing principal / tenant" failure modes so the engine never has to second-guess pipeline output.
## Usage: Basic
```ts
import { runHandshake } from "@suaveplan/api-ws-bun";
const outcome = runHandshake({
request,
principal,
tenantId,
capabilities: new Set([]),
tokenExpiresAt: Date.now() + 3600_000,
});
```
## Usage: Advanced
```ts
import { resolveSubprotocol, runHandshake } from "@suaveplan/api-ws-bun";
const outcome = runHandshake({
request,
principal,
tenantId,
capabilities: new Set(["ws.broadcast-cross-tenant"]),
tokenExpiresAt: Date.now() + 3600_000,
supportedSubprotocols: ["suaveplan.v1", "graphql-transport-ws"],
});
console.log(outcome.chosenSubprotocol);
// Or use the lower-level negotiation directly:
const choice = resolveSubprotocol(
["graphql-transport-ws"],
["suaveplan.v1", "graphql-transport-ws"]
);
```
## API Reference
- `HandshakeInput` — `{ request, principal, tenantId, capabilities, tokenExpiresAt, supportedSubprotocols? }`.
- `HandshakeOutcome` — `{ principal, tenantId, capabilities, tokenExpiresAt, chosenSubprotocol }`.
- `runHandshake(input): HandshakeOutcome` — freeze the pipeline result.
- `resolveSubprotocol(requested, supported): string | null` — RFC 6455 §4.1 negotiation.
## Implementation Notes
The function rejects missing principal or tenant with `WS_BUN.HANDSHAKE_REJECTED` even though the pipeline upstream is expected to guarantee both. This redundant guard exists because the engine treats this module as a hard wall against malformed input from a misconfigured pipeline — defending the rest of the connection lifecycle from `undefined`-propagation bugs is cheaper here than at every downstream call site.
Capability sets are copied into a fresh `Set` rather than referenced directly so the engine cannot accidentally mutate the host-supplied capability bag. The cost is one allocation per handshake, dwarfed by everything else the connection setup is doing.