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:
@@ -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.
|
||||
Reference in New Issue
Block a user