Files
2026-05-27 23:29:53 +01:00

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 / verify over 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).
  • consumedCount accessor 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.