Initial Commit
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
# `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
|
||||
|
||||
```ts
|
||||
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
|
||||
|
||||
```ts
|
||||
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.
|
||||
Reference in New Issue
Block a user