Files
skills/docs/api-stack/api/api-ws-bun/src/handshake/handshake.md
2026-05-29 08:20:56 +01:00

2.9 KiB
Raw Blame History

handshake

(no submodules)

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

import { runHandshake } from "@suaveplan/api-ws-bun";

const outcome = runHandshake({
    request,
    principal,
    tenantId,
    capabilities: new Set([]),
    tokenExpiresAt: Date.now() + 3600_000,
});

Usage: Advanced

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.