Rindle

API index and search · Build metadata

@rindle/room/token

0.0.0 · Public export map; development manifest version (0.0.0).

Source revision 05d0bf2c2e56 · build details
Source revision: 05d0bf2c2e56.
TypeScript input SHA-256: aabe6cfcc4172b870d5e272142958e9ea8d8784c2aa23133156e5a7ee633318e
Generated 2026-09-04T23:58:25.590Z with TypeScript 6.0.3. Public TypeScript checks and declaration emit passed. Package runtime tests are separate.

Entry point source

mintRoomToken

FunctionDeclaration · Source: packages/room/src/token.ts:126 · Supporting declarations

Sign a room lease token. This runs on the API-server side (or a test playing it).

export declare function mintRoomToken(opts: MintRoomTokenOptions): Promise<string>;

MintRoomTokenOptions

InterfaceDeclaration · Source: packages/room/src/token.ts:46 · Supporting declarations

export interface MintRoomTokenOptions {
    doc: string;
    ast: unknown;
    /** The subject (user id) this token authorizes — the §4.1 revocation key. */
    sub: string;
    /** Key id + its secret (utf-8; give every room the same `keys` map). */
    kid: string;
    key: string;
    /** Expiry, ms from `now`. Keep short (minutes) — the §4.1 TTL backstop. */
    ttlMs: number;
    /** Mint time; defaults to `Date.now()`. Injectable for tests. */
    now?: number;
    /** The room's {@link scopeSpecsHash} for the profile this lease serves — stamped so the
     *  shell can flag scope skew (see {@link RoomTokenPayload.scopesHash}). Omit to not stamp. */
    scopesHash?: string;
}

RoomTokenError

ClassDeclaration · Source: packages/room/src/token.ts:149 · Supporting declarations

Why a token was refused. The reason is terse and safe to echo in a queryError.

export declare class RoomTokenError extends Error {
    readonly reason: string;
    constructor(reason: string);
}

RoomTokenPayload

InterfaceDeclaration · Source: packages/room/src/token.ts:29 · Supporting declarations

The room's self-authorizing signed lease token (RINDLE-REALTIME-DESIGN.md §10.1).

The API server is the authority (§4): it authenticates the user, resolves the named query to an approved AST, and signs this token. The token then IS the lease — the room materializes on first presentation, so no pre-placement /materialize control call ever touches it (the §10.1 inversion; on the DO shell the Worker verifies the same signature statelessly before get(id)).

Shape: rt1.<base64url(payload)>.<base64url(hmac-sha256(prefix.payload))> with payload { v: 1, doc, ast, sub, iat, exp, kid }:

  • doc — the room/document id the token authorizes (a token for one room presented to another is refused);
  • ast — the approved wire Ast (the client never composes this; §4's "ASTs never cross the public wire" holds because the token is opaque TO THE CLIENT — it carries it, it cannot mint or alter it);
  • sub — the subject (user id): the revocation key (§4.1);
  • iat/exp — issued-at / expiry, ms epoch. Renewal is re-authorization: clients obtain a fresh token through the API server, never extend this one. iat is what lets a revocation refuse pre-revocation tokens while a genuine re-grant (a newer token) passes immediately;
  • kid — which shared secret signed it (rotation).

HMAC via WebCrypto (crypto.subtle) so the exact same code verifies in Node (the test shell) and in a Cloudflare Worker/DO (P4) — no node:crypto import.

export interface RoomTokenPayload {
    v: 1;
    doc: string;
    ast: unknown;
    sub: string;
    iat: number;
    exp: number;
    kid: string;
    /** A short fingerprint of the room's compiled scope specs at mint time
     *  ({@link scopeSpecsHash}). Advisory, not a credential: the room's §3.3 gate is the
     *  contract regardless. It lets the shell detect SCOPE SKEW — a room profile edited
     *  while a room is already live arms the gate with the OLD specs (a one-shot at boot)
     *  while fresh leases prove against the NEW ones, so every routed write silently
     *  deopt-loops. Optional so a pre-stamp api-server / older token still verifies. */
    scopesHash?: string;
}

scopeSpecsHash

FunctionDeclaration · Source: packages/room/src/token.ts:87 · Supporting declarations

A stable short fingerprint of the compiled scope specs — the scope-skew tripwire. NOT security (the gate re-proves every write): FNV-1a-32 over the {@link canonicalJson} form, 8 hex chars. Both the api-server (stamping the lease token) and the room shell (hashing the boot-wire scopes it armed the gate with) run this over the SAME compiler output, so an unchanged profile ⇒ equal hash and a profile edited under a live room ⇒ mismatch. A collision only costs a missed diagnostic, never correctness. Accepts either wire's spec array (RoomScopeSpec[] / RoomTableSpec[] — structurally identical).

export declare function scopeSpecsHash(specs: readonly unknown[]): string;

verifyRoomToken

FunctionDeclaration · Source: packages/room/src/token.ts:172 · Supporting declarations

Verify a token's signature and claims; returns the payload (with the approved AST) or throws {@link RoomTokenError}. Signature is checked FIRST — no claim is trusted (not even kid's existence beyond the key lookup) before the MAC passes.

export declare function verifyRoomToken(token: string, opts: VerifyRoomTokenOptions): Promise<RoomTokenPayload>;

VerifyRoomTokenOptions

InterfaceDeclaration · Source: packages/room/src/token.ts:158 · Supporting declarations

export interface VerifyRoomTokenOptions {
    /** The room's own doc id — a token for any other doc is refused. */
    doc: string;
    /** kid → shared secret. Unknown kids are refused (never "try them all"). */
    keys: Record<string, string>;
    /** Verification time; defaults to `Date.now()`. Injectable for tests. */
    now?: number;
}