Rindle

API index and search · Build metadata

Supporting declarations

packages/room/src/token.ts. These declarations explain referenced types. Only package-page symbols are package exports.

Exact source

RoomTokenPayload

/**
 * 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;
}

MintRoomTokenOptions

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;
}

scopeSpecsHash

/** 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;

mintRoomToken

/** 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>;

RoomTokenError

/** 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);
}

VerifyRoomTokenOptions

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;
}

verifyRoomToken

/**
 * 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>;