Move docs/ package mirror into document-navigator plugin

Relocate the ~6k-file docs tree from the repo root into
plugins/document-navigator/docs/ so the plugin is self-contained and
portable. docs/superpowers/ (specs and plans) stays at the repo root.

Moved via cp+rm rather than git mv because directory rename() fails with
EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the
identical-content files at commit time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

View File

@@ -0,0 +1,86 @@
# `reconnect-token` — HMAC envelope for resumable WebSocket sessions
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
When the host closes a WebSocket connection cleanly (heartbeat failure,
deploy, transient network issue), it issues a reconnect token the client can
present on a follow-up upgrade request. The host validates the HMAC and
re-attaches the previous subscription set with no plugin code change. This
module owns the abstract payload shape and a Web-Crypto-based sign/verify
pair. State is carried inside the token; the host stores nothing.
The TTL default is 5 minutes, matching design doc §11.5. Anything older is
rejected with `WS.RECONNECT_TOKEN_INVALID` so a stolen token has a hard
upper bound on its usefulness.
## Features
- Strict Zod schema for the payload — `tenant`, `principalId`,
`lastEventId` (nullable), `exp`.
- `signReconnectToken(input, { secret, clock?, ttlMs? })`.
- `verifyReconnectToken(token, { secret, clock? })` — throws on bad
encoding, bad MAC, bad shape, or expired payload.
- Uses Web Crypto (`globalThis.crypto.subtle`) — runs unchanged in Bun,
Node 20+, browser, and worker tiers.
- Constant-time MAC comparison.
## Usage: Basic
```ts
import {
signReconnectToken,
verifyReconnectToken,
} from "@suaveplan/api-ws/reconnect-token";
const secret = crypto.getRandomValues(new Uint8Array(32));
const token = await signReconnectToken(
{ tenant: "t1", principalId: "p1", lastEventId: "e-9" },
{ secret }
);
const payload = await verifyReconnectToken(token, { secret });
```
## Usage: Advanced
```ts
import {
RECONNECT_TOKEN_TTL_MS,
signReconnectToken,
verifyReconnectToken,
} from "@suaveplan/api-ws/reconnect-token";
const clock = (): number => mockNow;
let mockNow = 1_000_000;
const token = await signReconnectToken(
{ tenant: "t1", principalId: "p1", lastEventId: null },
{ secret, clock, ttlMs: RECONNECT_TOKEN_TTL_MS }
);
mockNow += RECONNECT_TOKEN_TTL_MS;
await verifyReconnectToken(token, { secret, clock }); // throws WS.RECONNECT_TOKEN_INVALID
```
## API Reference
- `const RECONNECT_TOKEN_TTL_MS = 300_000`.
- `interface ReconnectTokenPayload { tenant, principalId, lastEventId, exp }`.
- `reconnectTokenPayloadSchema: z.ZodSchema<ReconnectTokenPayload>`.
- `signReconnectToken(input, options): Promise<string>`.
- `verifyReconnectToken(token, options): Promise<ReconnectTokenPayload>` —
throws `WsError(WS.RECONNECT_TOKEN_INVALID)` on every failure mode.
## Implementation Notes
- HMAC-SHA-256 over the canonical JSON of the payload (the exact bytes
signed are the bytes verified — no re-serialisation).
- `base64url` encoding (no padding, `-`/`_` instead of `+`/`/`) keeps the
token URL-safe for the `?reconnectToken=…` query parameter.
- The verifier uses constant-time comparison so a malicious client cannot
recover the MAC byte-by-byte through timing.
- All four failure modes (malformed wire form, bad encoding, bad MAC,
expired) collapse to a single `WS.RECONNECT_TOKEN_INVALID` code so
clients cannot probe for which guard rejected them.