2.9 KiB
reconnect-token
Purpose
Wraps the api-ws contract's signReconnectToken / verifyReconnectToken HMAC token helpers so the engine has a stateful issuer that enforces single-use semantics and maps the contract's single WS.RECONNECT_TOKEN_INVALID code onto the WS_BUN-namespace pair (WS_BUN.RECONNECT_TOKEN_INVALID vs WS_BUN.RECONNECT_TOKEN_EXPIRED). This gives the engine a clean place to plumb the HMAC secret, the TTL, and the clock without threading them through every handshake call.
The issuer tracks consumed tokens in a Set<string> so a replayed token (same wire string presented twice) is rejected with the same INVALID code as a tampered token — replay protection matches the spec scenario.
Features
- Async
sign/verifyover a pre-bound secret + TTL + clock. - Replay defence — every successful verify marks the token consumed; second presentation throws
WS_BUN.RECONNECT_TOKEN_INVALID. - Distinguishes expiry from generic invalidity at the WS_BUN namespace so engines can map onto different HTTP status codes (401 vs 419).
consumedCountaccessor for test assertions and capacity tracking.
Usage: Basic
import { ReconnectTokenIssuer } from "@suaveplan/api-ws-bun";
const issuer = new ReconnectTokenIssuer(new Uint8Array(32).fill(7));
const token = await issuer.sign({
tenant: "t1",
principalId: "u1",
lastEventId: null,
});
const payload = await issuer.verify(token);
console.log(payload.tenant); // "t1"
Usage: Advanced
import { ReconnectTokenIssuer, WS_BUN_CODES } from "@suaveplan/api-ws-bun";
const clock = { value: 0 };
const issuer = new ReconnectTokenIssuer(
new Uint8Array(32).fill(13),
1_000,
() => clock.value
);
const token = await issuer.sign({
tenant: "t1",
principalId: "u1",
lastEventId: "evt-9",
});
clock.value += 2_000;
try {
await issuer.verify(token);
} catch (err) {
// (err as { code }).code === WS_BUN_CODES.RECONNECT_TOKEN_EXPIRED
}
API Reference
new ReconnectTokenIssuer(secret, ttlMs?, clock?)— constructor.issuer.sign(payload): Promise<ReconnectTokenString>— sign a token.issuer.verify(token): Promise<ReconnectTokenPayload>— verify + consume.issuer.consumedCount: number— total tokens consumed.
Implementation Notes
The contract signReconnectToken already encodes exp = clock() + ttlMs into the payload, so the issuer does not need to maintain its own expiry index. Single-use replay defence, by contrast, is engine-specific (multi-host deployments would need a Redis SET-NX instead of an in-process Set), so the issuer owns that state directly.
The expiry detection in verify inspects the contract error message rather than re-running an expiry check. The contract error path is the canonical place where "expired" is differentiated from "bad MAC" / "malformed token"; mirroring that decision tree here would risk drift across versions.