Rindle

API index and search · Build metadata

Supporting declarations

packages/api-server/src/index.ts. These declarations explain referenced types. Only package-page symbols are package exports.

Exact source

DEFAULT_RINDLE_API_ROUTES

export declare const DEFAULT_RINDLE_API_ROUTES: {
    readonly query: "/api/rindle/query";
    readonly read: "/api/rindle/read";
    readonly mutate: "/api/rindle/mutate";
    readonly applyRowChangeTxn: "/api/rindle/apply-row-change-txn";
    readonly claimRoomEpoch: "/api/rindle/claim-room-epoch";
    readonly roomLmids: "/api/rindle/room-lmids";
    readonly roomBoot: "/api/rindle/room-boot";
    readonly stream: "/api/rindle/stream";
};

MaybePromise

export type MaybePromise<T> = T | PromiseLike<T>;

ApiContext

/** Request context supplied by the application after authentication. The HTTP adapter or trusted
 *  caller owns `user`; do not populate it from an unverified request body. */
export interface ApiContext<User> {
    user: User;
    request?: unknown;
}

ApiQueryResult

export type ApiQueryResult = Ast | Query<any, any, any>;

ApiQuery

export type ApiQuery<User, Args> = (ctx: ApiContext<User>, args: Args) => MaybePromise<ApiQueryResult>;

ApiQueries

export type ApiQueries<User> = Record<string, ApiQuery<User, any>>;

RunQueryInput

export interface RunQueryInput<User> {
    user: User;
    name: string;
    args: unknown;
    query: ApiQuery<User, any>;
    context: ApiContext<User>;
}

RunQuery

export type RunQuery<User> = (input: RunQueryInput<User>) => MaybePromise<ApiQueryResult>;

AuthorizeQueryInput

export interface AuthorizeQueryInput<User> {
    user: User;
    name: string;
    args: unknown;
    context: ApiContext<User>;
}

AuthorizeMutationInput

export interface AuthorizeMutationInput<User> {
    user: User;
    envelope: MutationEnvelope;
    context: ApiContext<User>;
}

Authorizer

/** An optional request gate. Return `false` or throw to deny; `true` and `undefined` allow.
 *  This gate does not add row predicates or replace access checks inside a mutator. */
export type Authorizer<T> = (input: T) => MaybePromise<boolean | void>;

MutationContext

export interface MutationContext<User> {
    user: User;
    envelope: MutationEnvelope;
    daemon: RindleDaemonClient;
    request?: unknown;
}

ServerSql

/** A deliberately narrow raw-SQL facade exposed by the API server. On {@link ServerMutationTx}
 *  it is bound to the open mutation transaction; on {@link MutationScope} each call runs in its
 *  own transaction outside the mutation boundary. Column aliases should be unique: positional
 *  driver rows are keyed by column name, so a duplicate alias is represented by its last value. */
export interface ServerSql {
    /** Queue/execute one statement. A transaction-bound call commits with the surrounding mutation. */
    execute(sql: string, params?: readonly WireValue[]): Promise<void>;
    /** Queue/execute an ordered statement batch. An empty batch is a no-op. On an
     *  outside-transaction surface ({@link MutationScope.sql}, `backend.outsideSql`) the batch MUST
     *  execute as ONE atomic transaction — all statements or none. Every built-in backend does (the
     *  daemon's `execute-sql-txn`, the sql-client's `/v1/sql/batch`, the Postgres plugger's
     *  BEGIN/COMMIT); a custom backend that loops statements without a transaction silently breaks
     *  the stream plane's chunk+CAS and compaction invariants, which ride single `batch` calls. */
    batch(statements: readonly SqlStatement[]): Promise<void>;
    /** Run a read and return rows keyed by their column names. */
    query<Row = Record<string, unknown>>(sql: string, params?: readonly WireValue[]): Promise<Row[]>;
}

SqlMutationTx

/** The raw-SQL escape hatch for relational/authority statements a keyed op can't express — an
 *  owner-gated cascade, a `NOT EXISTS` dedup. Prefer `tx.sql`; `exec` remains the synchronous
 *  compatibility shorthand for a queued `tx.sql.execute`, and `statements` is the raw write list. */
export interface SqlMutationTx {
    readonly sql: ServerSql;
    exec(sql: string, params?: WireValue[]): void;
    readonly statements: readonly SqlStatement[];
}

ServerMutationTx

/** The write handle a server mutator runs against — the ASYNC twin of the client's `MutationTx`. It
 *  is both the isomorphic {@link ServerWriteTx} logical surface (insert/update/upsert/insertIgnore/
 *  delete/row, rendered to dialect SQL) AND the legacy {@link SqlMutationTx} raw escape hatch. Both
 *  implementations run reads through the OPEN transaction (read-your-writes): Postgres executes
 *  everything live; the SQL-client and daemon adapters accumulate writes and lazily upgrade to an
 *  interactive mutation session at the first read (DAEMON-INTERACTIVE-TXN-DESIGN.md §5). */
export interface ServerMutationTx extends ServerWriteTx, SqlMutationTx {
    /** Run a full query (a fluent `Query` or its wire `Ast`) INSIDE the open transaction —
     *  read-your-writes, like {@link ServerWriteTx.row} but for arbitrary shapes. Returns the
     *  parsed nested result tree: an array for a plural root, an object or `null` for a `.one()`
     *  root, with cells in their raw SQLite storage-class representations (the same vocabulary
     *  `row` speaks). Remote SQLite backends: compiled by `@rindle/query-compiler`'s sqlite dialect
     *  (bind params, NO casts — §5.4) and executed through the mutation session. Postgres: lands with
     *  the read-compiler catalog integration (POSTGRES-READ-COMPILER-DESIGN.md Phase B). */
    query(q: Ast | Query<any, any, any>): Promise<unknown>;
}

ApiMutatorResult

export type ApiMutatorResult = void | SqlStatement[] | SqlTxn;

ApiMutator

/** A server mutator: a plain async function against the live {@link ServerMutationTx}. Two ways to
 *  write one (MUTATORS-ISOMORPHIC): drive it directly (the raw escape hatch — an owner-gated cascade,
 *  a `NOT EXISTS` dedup — plus a returned `SqlStatement[]`/`SqlTxn`), OR delegate to a SHARED generator
 *  (the SAME body the client predicts) via {@link runSharedMutation}, keeping only the server-only
 *  authority (arg parse, principal, policy) in the wrapper. */
export type ApiMutator<User, Args> = (tx: ServerMutationTx, args: Args, ctx: MutationContext<User>) => MaybePromise<ApiMutatorResult>;

MutationRejected

/** Thrown by {@link MutationScope.transact} when the transacted body BUSINESS-rejects: the data
 *  rolled back and `lmid` advanced alone (§2.4). Catch it to COMPENSATE an outside-tx side effect
 *  (refund the charge), then rethrow or return — the mutation's protocol outcome is already sealed
 *  as rejected, so a post-reject throw can't change it. A DB/infra failure is NOT this — it
 *  propagates as the raw driver error (the client retries; `lmid` did not advance). */
export declare class MutationRejected extends Error {
    readonly reason: string;
    constructor(reason: string);
}

MutationScope

/** The per-mutation server handle a {@link ScopedMutator} runs against. Code before {@link transact}
 *  runs OUTSIDE the transaction; code after a clean `transact` runs AFTER the commit. The
 *  `lmid`-always-advances invariant is the HARNESS's, not the author's: {@link RindleApiServer.pushMutation}
 *  seals the response from this handle's recorded outcome, so an early return, a never-called
 *  `transact`, or a swallowed {@link MutationRejected} still advances `lmid` and never wedges the
 *  client's pending queue. */
export interface MutationScope {
    /** Raw SQL OUTSIDE the mutation transaction. Every call commits independently and therefore may
     *  be observed even if {@link transact} later rejects or fails. Calls may also repeat when an
     *  envelope is retried, so outside writes need their own idempotency key/unique constraint. */
    readonly sql: ServerSql;
    /** Open the ONE atomic write transaction and drive `body` inside it, committing (stamping `lmid`
     *  co-transactionally) on a clean return. MAY be called at most once — a second call throws.
     *
     *  Two forms:
     *   - `transact(sharedMutator, args, ctx)` — drive a SHARED (generator) mutator (the same body the
     *     client predicts); pass the already-parsed `args` and the server `ctx` (fold server-only
     *     values like a charge id into `ctx` here).
     *   - `transact(run)` — a raw callback receiving the live {@link ServerMutationTx} (the escape
     *     hatch: `tx.exec`, logical writes, read-your-writes reads).
     *
     *  A THROW from the body that is not a {@link BackendError} is a BUSINESS rejection: the data rolls
     *  back, `lmid` advances alone, and this method throws {@link MutationRejected} (so surrounding
     *  code can compensate). A {@link BackendError} is INFRA: it propagates (the client retries).
     *
     *  **The callback form RETURNS ITS BODY'S VALUE**, and only on the committed path — a rejection
     *  throws, so there is never a value to act on for a transaction that rolled back. That is what
     *  lets a post-commit effect be decided by a TRANSACTIONAL read instead of a second, racy one
     *  afterwards:
     *
     *  ```ts
     *  const kick = await scope.transact(async (tx) => {
     *    const prior = await tx.row("message", { id: a.assistantMessageId });
     *    if (prior) return undefined;                 // a replayed envelope — do NOT re-fire the effect
     *    tx.insert("message", …);
     *    return { streamId: a.assistantMessageId, history: await readHistory(tx, a.chatId) };
     *  });
     *  if (kick) void startGeneration(kick);          // committed, and decided against committed state
     *  ```
     *
     *  A tx-form mutator cannot do this: its return value is already the logical-write channel
     *  ({@link ApiMutatorResult}). A post-commit effect chosen by a transactional read is exactly what
     *  the scoped form is for. */
    transact<T>(run: (tx: ServerMutationTx) => T | Promise<T>): Promise<T>;
    transact<A, C extends MutatorCtx>(mutator: SharedMutator<A, C>, args: A, ctx: C): Promise<void>;
}

ScopedMutator

/** A SCOPED server mutator (WORK-OUTSIDE-TX): server-only code, ONE `scope.transact`, optional
 *  post-commit code. Register it by wrapping in {@link scoped} — the tag the api-server routes on to
 *  hand it a {@link MutationScope} instead of running its whole body inside the transaction. */
export type ScopedMutator<User, Args> = (scope: MutationScope, args: Args, ctx: MutationContext<User>) => void | Promise<void>;

ScopedApiMutator

/** A {@link ScopedMutator} tagged by {@link scoped} so the harness invokes it with a
 *  {@link MutationScope}. Typed as a BRANDED tx-form {@link ApiMutator} purely so it registers in the
 *  `mutators` record without widening it to a union (which would break contextual inference for every
 *  plain tx-form entry). Its true runtime shape is `(scope, args, ctx)`; the tag — not the type —
 *  routes it, and it is never actually called as a tx-form mutator. */
export type ScopedApiMutator<User, Args> = ApiMutator<User, Args> & {
    readonly __rindleScoped: true;
};

scoped

/** Mark a mutator as SCOPED so the api-server gives it a {@link MutationScope} (author-controlled tx
 *  boundary via `scope.transact`) rather than running its whole body inside the transaction. Register
 *  it alongside the tx-form mutators — it wins by key like any override:
 *
 *  ```ts
 *  mutators: defineApiMutators({
 *    ...sharedApiMutators(sharedMutators, sharedCtx),        // tx-form (common case)
 *    createOrder: scoped(async (scope, raw, ctx) => {        // needs outside-tx work
 *      const args = createOrder.args.parse(raw);
 *      const chargeId = await stripe.charge(args.amount, { idempotencyKey: ctx.envelope.mid }); // outside tx
 *      try {
 *        await scope.transact(createOrder, args, { ...sharedCtx(ctx), chargeId });              // inside tx
 *      } catch (e) {
 *        await stripe.refund(chargeId);                       // compensate — the write rejected
 *        throw e;
 *      }
 *      await sendReceipt(ctx.user);                           // after commit
 *    }),
 *  }),
 *  ```
 */
export declare function scoped<User, Args>(fn: ScopedMutator<User, Args>): ScopedApiMutator<User, Args>;

ApiMutators

export type ApiMutators<User> = Record<string, ApiMutator<User, any>>;

QueryLeaseRequest

export interface QueryLeaseRequest<User> {
    user: User;
    name: string;
    args: unknown;
    request?: unknown;
    /** The browser's stable `clientId` (sent on the query POST) — the anonymous routing-key fallback
     *  when there is no authenticated subject and no session cookie (READ-ROUTER-DESIGN.md §1.5/§2.2).
     *  A routing HINT only, never authorization. */
    clientId?: string;
    /** The browser's opaque follower-affinity ticket (FOLLOWER-AFFINITY-DESIGN.md §3), read off the
     *  query POST and forwarded opaquely on `materialize` so the fleet edge selects the follower
     *  the browser's ws is pinned to (§2, §4) — both legs co-locate. The api-server does NOT verify
     *  it (the fleet does); it holds no signing key. Absent ⇒ single daemon / affinity off. */
    affinity?: string;
}

QueryLeaseRealtime

/**
 * The room-serve block on a query lease (RINDLE-REALTIME-QUERY-ENABLEMENT §2.1 step 5 / §2.4,
 * slice G-iv-b): present when the named query carries a realtime label naming a configured room
 * profile (302 §5 — declared, not derived; no coverage proof). The G-v client uses it to open the
 * room transport for THIS query beside — never instead of — its daemon session.
 *
 * It is a dedicated block on purpose: the ROOM ws is a SEPARATE connection this query opens beside
 * its daemon session (never a migration of the daemon session — the daemon ws host is fixed and
 * placed by the affinity ticket). A room-served lease's top-level fields are byte-identical to the
 * daemon-served ones.
 */
export interface QueryLeaseRealtime {
    /** The client store's gate/domain key for this room source (`connectSource`) AND the string the
     *  wasm engine's `parse_source_key` accepts: any string other than the reserved `"daemon"`
     *  parses as a room source, and the established convention is `"room:" + doc`
     *  (e.g. `room:document/doc:d1`). */
    sourceKey: string;
    /** Where the client opens the ROOM ws for this query (from `realtime.locateRoom`) — its OWN
     *  connection, distinct from the daemon session's fixed ws host. */
    wsEndpoint: string;
    /** The room's self-authorizing signed lease (`@rindle/room/token`): the APPROVED query AST +
     *  doc + subject, HMAC-signed with `realtime.roomTokenKey` so the room shell's
     *  `downstream.tokenKeys` ring verifies it. The room materializes on first presentation. */
    roomToken: string;
    /** Token expiry (ms epoch) — the client's renewal clock (renewal = a fresh lease). */
    exp: number;
    /** The wire room doc (`"<profile>/<key>"`, minted server-side — never client-derived). */
    doc: string;
    /** Per-footprint-table specs: the §2.2 owned/followed split + §3.2 routing metadata. Since
     *  H-iii each spec also carries `footprintWhere` — the same exact membership predicate the boot
     *  wire ships the room gate (one compiler, `compileRoomScopeSpecs`) — feeding the client's §3
     *  prove-or-slow-path router. Advisory routing metadata, never a credential (§3.2). */
    tables: RoomTableSpec[];
}

QueryLeaseLifecycleLease

/**
 * One minted SYSTEM-STREAM lease on a query lease's `lifecycle` block (RINDLE-REALTIME-QUERY-
 * ENABLEMENT §4, Slice I-iii): an ordinary daemon materialization over one of the four
 * `_rindle_*` lifecycle system tables (registered by the daemon's `enable_realtime_lifecycle` —
 * `rust/rindle-replica/src/mutations.rs`), attachable by the EXISTING client subscribe path
 * (present `leaseToken` on a `subscribe` frame, exactly like the primary lease). The identity
 * fields (`scope`/`doc`/`clientId`) document the minted AST's predicate — the client keys its
 * retains and its release-time row filters on them.
 */
export interface QueryLeaseLifecycleLease {
    /** Which system table this lease's subscription serves. */
    table: string;
    leaseToken: string;
    /** DOORBELL only: the §4.1 occupancy scope — the wire room doc (`"<profile>/<key>"`). */
    scope?: string;
    /** FENCE entries only: the room doc the predicate is scoped to. */
    doc?: string;
    /** FENCE ledger/outcome entries: present iff the predicate was ALSO client-scoped (the lease
     *  request carried `clientId`). Absent ⇒ doc-only predicate — the client filters to its own
     *  rows regardless (defense in depth). */
    clientId?: string;
}

QueryLeaseLifecycle

/** The §4 lifecycle block on a query lease (Slice I-iii): present only when BOTH the realtime
 *  `lifecycle` config is on AND the query is realtime-labeled. `doorbell` rides EVERY labeled
 *  lease (occupancy is counted whether or not the query is room-served — the 1→2 upgrade trigger
 *  needs solo watchers subscribed BEFORE any room exists, §4.1); `fence` rides only a ROOM-SERVED
 *  lease (the §4.2/§7.1/§3.3 downgrade surfaces are meaningful only where a room domain exists).
 *  The §4.2 fence VALUE (`finalFlushSeq`) is deliberately NOT here — it arrives with the I-v
 *  downgrade response; I-iii only stands up the streams. */
export interface QueryLeaseLifecycle {
    doorbell: QueryLeaseLifecycleLease;
    fence?: QueryLeaseLifecycleLease[];
}

QueryLeaseRealtimeFence

/** The §4.2 downgrade fence on a query lease (Slice I-v): rides a labeled reply whose §4.1
 *  occupancy gate CLOSED (so there is NO `realtime` block) when the server could drain the room.
 *  A SIBLING of `realtime`, never nested inside it — block-ABSENCE is the downgrade signal, and
 *  the fence rides alongside that absence. Its `finalFlushSeq` is the room's last COMMITTED flush
 *  seq; the client's frozen room ghost holds visible until the daemon plane has provably absorbed
 *  it (`_rindle_room_watermark(doc) ≥ finalFlushSeq`). Absent from every non-downgrade reply. */
export interface QueryLeaseRealtimeFence {
    /** The retiring room source's gate/domain key — `"room:" + doc`, matching what the room-served
     *  lease's {@link QueryLeaseRealtime.sourceKey} carried. */
    sourceKey: string;
    doc: string;
    finalFlushSeq: number;
}

QueryLeaseResponse

export interface QueryLeaseResponse {
    leaseToken: string;
    materializationId: string;
    queryKey?: string;
    reused?: boolean;
    /** The public daemon/fleet WebSocket endpoint. With the unified `rindle` connection this is
     *  derived from the same ingress URL (or `rindle.wsUrl`) so a browser can open its transport from
     *  this lease and needs no application-authored runtime-config route. */
    wsEndpoint?: string;
    /** Fresh opaque follower-placement ticket minted with this follower-local lease. The optimistic
     *  client offers it on the WebSocket opened at {@link wsEndpoint}, pinning both legs to the same
     *  follower even though the first connection is created only after this response. */
    affinity?: string;
    /** The room-serve block (G-iv-b) — see {@link QueryLeaseRealtime}. Absent ⇒ the lease is
     *  byte-identical to the legacy daemon-served shape. */
    realtime?: QueryLeaseRealtime;
    /** The §4.2 downgrade fence (Slice I-v) — see {@link QueryLeaseRealtimeFence}. Present only on a
     *  labeled reply whose occupancy gate closed AND `realtime.lifecycle.drainRoom` could drain the
     *  room; absent otherwise (including on every room-served reply). */
    realtimeFence?: QueryLeaseRealtimeFence;
    /** The §4 lifecycle system-stream block (Slice I-iii) — see {@link QueryLeaseLifecycle}.
     *  Minted ONLY under the opt-in `realtime.lifecycle` config; absent ⇒ byte-identical to the
     *  pre-lifecycle response. */
    lifecycle?: QueryLeaseLifecycle;
}

QueryReadRequest

/** A one-shot SSR read of a named query (SSR-DESIGN.md §6): same `(name, args)` surface as a lease,
 *  but the daemon serializes the current view ONCE and registers no subscriber. */
export interface QueryReadRequest<User> {
    user: User;
    name: string;
    args: unknown;
    request?: unknown;
    /** The browser's stable `clientId` — the anonymous routing-key fallback (see
     *  {@link QueryLeaseRequest.clientId}). Lets the SSR read co-locate on the follower the booting
     *  client's first subscribe will hit (READ-ROUTER-DESIGN.md §2.4). */
    clientId?: string;
    /** The browser's opaque follower-affinity ticket — see {@link QueryLeaseRequest.affinity}.
     *  Forwarded on the one-shot `query` so an SSR read lands on the same pinned follower. */
    affinity?: string;
}

QueryReadResponse

/** The assembled (nested-by-name) first-paint snapshot the server-side Store seeds + dehydrates
 *  (SSR-DESIGN.md §3.3). `rows` hydrate without an engine; `cvMin` is their watermark baseline. */
export interface QueryReadResponse {
    rows: Array<{
        cols: Record<string, unknown>;
        [rel: string]: unknown;
    }>;
    cvMin?: number;
    queryKey?: string;
}

MutationRunInput

/** The context a {@link MutationBackend} needs to run one mutation inside its transaction. */
export interface MutationRunInput {
    envelope: MutationEnvelope;
    /** Schema-derived render metadata (from {@link RindleApiServerOptions.schema}). A logical op on a
     *  table absent here throws loudly — never a silent dropped write. `{}` when no schema is set. */
    render: RenderIndex;
    /** Invoke the (authorized) mutator against the backend-provided tx. A THROW that is NOT a
     *  {@link BackendError} is a BUSINESS rejection (roll the data back, then advance `lmid` alone,
     *  §2.4); a {@link BackendError} (a DB-layer failure) is INFRA and rejects the returned promise. */
    run(tx: ServerMutationTx): Promise<void>;
}

MutationOutcome

/** The result of {@link MutationBackend.runMutation}: either the data+lmid committed together, or a
 *  business rejection whose data was rolled back but whose `lmid` still advanced (§2.4). */
export type MutationOutcome = {
    accepted: true;
    output: SqlTxnOutput;
} | {
    accepted: false;
    reason: string;
    output?: unknown;
};

MutationBackend

/**
 * Where a mutation runs and who stamps `lmid` — the seam that makes the mutator authoring surface
 * backend-agnostic (`BYO-POSTGRES-LMID-CONTRACT-DESIGN.md` §6; MUTATORS-ISOMORPHIC plan). Three
 * implementations ship: {@link sqlBackend} is the preferred managed-SQL path; {@link daemonBackend}
 * keeps the private control-plane compatibility path; and {@link postgresBackend} runs a real
 * interactive PG transaction with confirmation riding the CDC loop down.
 *
 * The load-bearing invariant is that a mutation ALWAYS advances the client's `last_mutation_id`:
 * - `runMutation` runs the mutator inside the backend's transaction; on success it advances `lmid`
 *   to `envelope.mid` in the SAME atomic unit (OPTIMISTIC-WRITES-DESIGN.md §8.2); on a BUSINESS
 *   rejection it rolls the data back but STILL advances `lmid` (§2.4 — else the client's pending
 *   queue never drains and the optimistic stack wedges).
 * - `reject` is the pre-flight path (unknown mutator, failed authorization): NO data, `lmid` alone.
 * - An infrastructure failure THROWS from `runMutation` — never a user rejection (the client
 *   retries; mid dedup absorbs any applied prefix).
 */
export interface MutationBackend {
    /** The SQL dialect this backend renders logical ops to (drives placeholder style). */
    readonly dialect: SqlDialect;
    /** Optional raw-SQL surface outside the mutation transaction. Built-in backends provide it;
     *  custom backends may omit it, in which case `scope.sql` fails as an infrastructure error. */
    readonly outsideSql?: ServerSql;
    runMutation(input: MutationRunInput): Promise<MutationOutcome>;
    reject(input: {
        envelope: MutationEnvelope;
        reason: string;
    }): Promise<unknown>;
}

PushMutationRequest

export interface PushMutationRequest<User> {
    user: User;
    envelope: MutationEnvelope;
    request?: unknown;
}

PushMutationsRequest

export interface PushMutationsRequest<User> {
    user: User;
    envelopes: MutationEnvelope[];
    request?: unknown;
}

PushMutationResponse

export type PushMutationResponse = {
    accepted: true;
    rejected: false;
    output: SqlTxnOutput;
} | {
    accepted: false;
    rejected: true;
    reason: string;
    output?: unknown;
};

RindleApiRoutes

export interface RindleApiRoutes {
    query: string;
    read: string;
    mutate: string;
    applyRowChangeTxn: string;
    claimRoomEpoch: string;
    roomLmids: string;
    roomBoot: string;
    stream: string;
}

RoomHostResponse

/** A room-host reply the transport writes VERBATIM (`status` + JSON `body`): the
 *  daemon's fence/conflict/identity semantics ride specific statuses and body shapes
 *  the room's `httpAuthority` decodes, so this endpoint trio can't run through the
 *  throw-on-error result shapes the viewer endpoints use. */
export interface RoomHostResponse {
    status: number;
    body: unknown;
}

PinnedQuery

/** A named query to keep permanently materialized (warm with zero subscribers). */
export interface PinnedQuery {
    name: string;
    args?: unknown;
}

PinFanout

/** The explicit fleet pin fan-out seam (READ-ROUTER-DESIGN.md §4.2). The api-server resolves each
 *  pin's authoritative AST under `pinUser` and hands the ready {@link MaterializeInput}s here; the
 *  implementation (the read router) fans EACH across all live followers. Distinct, on purpose, from
 *  a per-viewer `materialize` (which routes ONE) — a pin-assert always sprays ALL. */
export interface PinFanout {
    assertPins(pins: readonly MaterializeInput[]): Promise<void>;
}

RoomBootFlush

/** The room-boot flush leg (enablement §5): where the placed room's write-behind lands and what
 *  credential it presents. `urls` are the trio's ROUTE PATHS (root-relative — the shell resolves
 *  them against the boot call's origin), so an app that overrides `routes` needs no out-of-band
 *  sync; `headers` ride every flush call verbatim (`httpAuthority`'s `headers`). */
export interface RoomBootFlush {
    urls: {
        apply: string;
        claim: string;
        lmids: string;
    };
    headers: Record<string, string>;
}

RoomBootResponse

/** The `/room-boot` response (RINDLE-REALTIME-DESIGN.md §10.1 — the DO shell's cold-boot
 *  callback): the claimed placement epoch, the room's upstream footprint lease, and the flush
 *  leg. A cold room boots inert and serves nothing until this returns. */
export interface RoomBootResponse {
    epoch: number;
    upstreamLeaseToken: string;
    /** Where the room opens its upstream subscription (a routed deploy's follower). Absent ⇒ the
     *  shell's statically configured rindled ws endpoint. */
    upstreamWsEndpoint?: string;
    /** Fresh opaque follower-placement ticket minted alongside `upstreamLeaseToken`. The DO offers
     *  it with `rindle.v1` on the separate upstream ws so a static fleet endpoint replays to the
     *  exact follower holding that local lease. Absent when daemon affinity is off. */
    upstreamAffinity?: string;
    /** Per-footprint-table scope specs (H-iv-b), compiled from the resolved footprint AST + the
     *  profile's context set (the legacy anonymous profile compiles with an empty context set):
     *  what the shell hands the wasm room's `enableWritesV2` — the §3.3 commit gate. Optional
     *  only for wire compatibility with pre-H-iv-b servers; a shell that doesn't receive them
     *  enables the v1 table-granular write plane exactly as before. */
    scopes?: RoomScopeSpec[];
    flush: RoomBootFlush;
}

RindleRealtimeOptions

export interface RindleRealtimeOptions<User> {
    /** The boot shell secret (§10.1 — a host binding, never client-derived). Authenticates the
     *  room's `Authorization: Bearer` on `/room-boot` (the default {@link authorizeBoot}) and keys
     *  the DEFAULT epoch-bound flush credential. */
    shellSecret: string;
    /** NAMED room profiles (RINDLE-REALTIME-QUERY-ENABLEMENT-DESIGN.md §2.1): profile name → key
     *  derivation + canonical unwindowed footprint + read-only context tables. The wire room key
     *  for a named profile is `"<profile>/<key>"`; `/room-boot` splits it and resolves the
     *  profile's footprint with the bare key. Compiled + validated LOUDLY at construction (§2.3):
     *  a windowed footprint, a context table missing from the schema/footprint, or a registered
     *  query whose realtime label names a missing profile all throw from `createRindleApiServer`. */
    rooms?: Record<string, RoomProfile<User>>;
    /** doc → the room's approved upstream footprint (§3.1) — an `Ast` or fluent `Query`. MAY
     *  delegate to the named-query registry internally; throw `RindleApiError("not-found", …, 404)`
     *  for a doc that shouldn't exist. The §9 footprint budget belongs here — it runs once per
     *  placement, at lease mint.
     *  @deprecated Prefer named {@link rooms} profiles (RINDLE-REALTIME-QUERY-ENABLEMENT §2.1).
     *  This bare form remains as the single-profile LEGACY alias — the anonymous/default profile:
     *  a doc with no known `"<profile>/"` prefix resolves here, byte-identically to before named
     *  profiles existed (and with none of their construction/boot-time validation). */
    resolveFootprint?: (doc: string, ctx: ApiContext<User>) => MaybePromise<ApiQueryResult>;
    /** Gates the flush trio — `authorizeRoom`, relocated. Default: verify the default flush
     *  credential from the {@link ROOM_FLUSH_CREDENTIAL_HEADER} request header — which requires the
     *  transport to pass its incoming request as `context.request` (Fetch `Request` and node
     *  `IncomingMessage` shapes are both understood). */
    authorize?: Authorizer<ApiContext<User>>;
    /** Gates `/room-boot`. Default: constant-time `Authorization: Bearer` check against
     *  {@link shellSecret} (same `context.request` requirement as {@link authorize}). */
    authorizeBoot?: Authorizer<ApiContext<User>>;
    /** Mint the flush headers a placed room presents on every flush call. Default:
     *  {@link mintRoomFlushCredential} under {@link ROOM_FLUSH_CREDENTIAL_HEADER}. Override this
     *  and {@link authorize} TOGETHER — they are the two ends of one credential. */
    mintFlushHeaders?: (input: {
        doc: string;
        epoch: number;
    }) => MaybePromise<Record<string, string>>;
    /** Lease TTL for the room's upstream footprint materialization (defaults to the server-wide
     *  `leaseTtlMs`, else the daemon's default). */
    upstreamLeaseTtlMs?: number;
    /** Static endpoint where rooms open their upstream subscription. In a follower fleet this is the
     *  fleet ws URL; `/room-boot` pairs it with the materialization's fresh placement ticket so the
     *  room lands on the exact follower holding its lease. Absent ⇒ no explicit upstream (the Node
     *  room shell may use its own default; the shipped DO shell requires this endpoint). */
    upstreamWsEndpoint?: string;
    /** Locate (or place) the room serving `doc` and return the ROOM ws endpoint a room-served
     *  lease's client should open (G-iv-b; on the DO shell this is the Worker's room URL). The
     *  endpoint rides the lease's dedicated `realtime.wsEndpoint` — its OWN connection, distinct from
     *  the daemon session's fixed ws host. Absent ⇒ room-serving is OFF: labeled queries serve from
     *  the daemon exactly as today (fail-open). */
    locateRoom?: (doc: string) => MaybePromise<{
        wsEndpoint: string;
    }>;
    /** The room lease token signing key (`@rindle/room/token`): `kid` + secret, matching an entry
     *  in the room shell's `downstream.tokenKeys` ring. Required for room-serving (without it a
     *  labeled query fail-opens to the daemon with a one-time warning). A separate secret from
     *  `shellSecret` on purpose — the shell's ring is the client-token trust domain, the shell
     *  secret is the boot/flush trust domain. */
    roomTokenKey?: {
        kid: string;
        secret: string;
    };
    /** Room lease token TTL, ms (default 5 minutes — the §4.1 short-TTL backstop; renewal is a
     *  fresh lease through this server, never an extension). */
    roomTokenTtlMs?: number;
    /** Loud-diagnostics sink for the realtime layer (profile compilation warnings + the one-time
     *  per-(query, profile) "not room-served" serve-decision warnings). Defaults to
     *  `console.warn`; injectable for tests. */
    warn?: (message: string) => void;
    /** The §4 upgrade/downgrade lifecycle plane (RINDLE-REALTIME-QUERY-ENABLEMENT §4, Slice
     *  I-iii): PRESENCE of this block is the opt-in — every realtime-labeled lease then
     *  additionally mints the doorbell system lease (occupancy, §4.1) and every ROOM-SERVED lease
     *  the fence bundle (watermark + ledger + outcomes, §4.2/§7.1/§3.3) — see
     *  {@link QueryLeaseLifecycle}. Requires the daemon to have run `enable_realtime_lifecycle`
     *  (the four `_rindle_*` system tables must be registered or the minted materializations fail
     *  — which fail-opens with a one-time warning, never blocking the lease). Absent ⇒ the lease
     *  response is byte-identical to pre-lifecycle. */
    lifecycle?: RindleRealtimeLifecycleOptions;
}

RindleRealtimeLifecycleOptions

/** {@link RindleRealtimeOptions.lifecycle}. PRESENCE of the block is the opt-in switch (I-iii);
 *  the fields below are the Slice I-iv occupancy knobs (§4.1, decisions D4/D6/D7). All optional —
 *  `lifecycle: {}` gets the designed defaults. */
export interface RindleRealtimeLifecycleOptions {
    /** D6 (§4.1): the occupancy threshold for room-serving. A labeled lease whose scope counts
     *  FEWER than this many distinct unexpired sessions (the caller's own included) ships WITHOUT
     *  the realtime block — served from the daemon, indistinguishable from an uncovered query —
     *  but WITH the doorbell, so the 1→2 transition wakes it (that is the point: solo docs never
     *  cost room infrastructure). Default **2** (the design's 1→2 trigger). Set `1` to room-serve
     *  solo viewers (the pre-I-iv behavior under lifecycle config). */
    minSessions?: number;
    /** The §9.1 hysteresis window, ms (default **120_000**). Two consumers: (a) the lazy sweep
     *  (D4) keeps expired session rows lingering at least this long past expiry — Slice I-v's
     *  downgrade decision ("no other unexpired row AND the newest other row expired > graceMs
     *  ago") is read FROM those rows, so they must survive to be read; (b) this slice's gate
     *  applies the same hysteresis upward: a scope with an other-session row expired ≤ graceMs
     *  ago keeps room-serving through the window (see `lifecycleOccupancy` — no flap on one
     *  client's brief lapse). §9.1-tunable: raise it for docs where collaborators churn slowly. */
    graceMs?: number;
    /** TTL of an occupancy session row, ms — `expires_at = now + sessionTtlMs` on every labeled
     *  lease mint/renewal (D7: session identity = the request's `clientId`; two tabs are two
     *  sessions iff their clientIds differ). Default = the server's `leaseTtlMs`, else 5 minutes —
     *  matching the room-token renewal cadence (`roomTokenTtlMs`, renewed 30s early), so a
     *  room-attached client's renewals keep its row unexpired; a daemon-attached solo client's row
     *  MAY lapse (it has no renewal timer) and is refreshed by its next doorbell-triggered
     *  re-lease — occupancy converges through the doorbell itself. */
    sessionTtlMs?: number;
    /** The §4.2 downgrade drain hook (Slice I-v). When the occupancy gate CLOSES for a labeled
     *  lease whose scope PLAUSIBLY hosted a room (an other-session row still lingers — never a
     *  never-shared solo doc), the api-server calls this to drain the room's pending write-behind
     *  and learn its last COMMITTED `flush_seq`, then rides the value back on the lease as the
     *  {@link QueryLeaseRealtimeFence}. The deployment wires it to the room shell's / DO's `/drain`
     *  control. Absent ⇒ no fence is attached (the client hits its loud legacy downgrade path);
     *  a throw fails OPEN to the same (a downgrade never blocks the lease). Concurrent drains across
     *  api-server instances are fine — `/drain` is idempotent. */
    drainRoom?: (doc: string) => Promise<{
        finalFlushSeq: number;
    }>;
}

ROOM_FLUSH_CREDENTIAL_HEADER

export declare const ROOM_FLUSH_CREDENTIAL_HEADER = "x-rindle-room-credential";

RoomFlushCredentialPayload

export interface RoomFlushCredentialPayload {
    v: 1;
    doc: string;
    epoch: number;
    iat: number;
}

mintRoomFlushCredential

/** Sign the default epoch-bound flush credential (`/room-boot` mints one per placement). */
export declare function mintRoomFlushCredential(opts: {
    shellSecret: string;
    doc: string;
    epoch: number;
    /** Mint time; defaults to `Date.now()`. Injectable for tests. */
    now?: number;
}): Promise<string>;

verifyRoomFlushCredential

/** Verify a flush credential's MAC, then its claims; returns the payload or throws. The MAC is
 *  checked FIRST — no claim is trusted before it passes. */
export declare function verifyRoomFlushCredential(credential: string, shellSecret: string): Promise<RoomFlushCredentialPayload>;

RindleDatabaseOptions

/** Database connection used by the API server's managed SQL path.
 *
 *  `intMode` defaults to `"number"` because logical Rindle rows use the JSON-safe {@link WireValue}
 *  vocabulary — `"bigint"` does not survive `JSON.stringify`, and `"string"` silently retypes every
 *  integer, breaking arithmetic in mutator bodies. The cost is a HARD BOUND: a mutator read of an
 *  integer outside ±(2^53 − 1) rejects that mutation rather than silently rounding it. Tables with
 *  keys beyond that range (snowflake ids, and so on) must override `intMode` and have their mutators
 *  handle the resulting type. Commit receipts are unaffected — they never decode row values. */
export type RindleDatabaseOptions = Pick<SqlClientOptions, "url" | "authToken" | "fetch" | "intMode">;

RindleConnectionOptions

/** The unified fleet connection — one URL, one key. A Rindle is BOTH layers at once: the SQL
 *  database below and the sync/IVM control plane above, served by a single ingress that routes
 *  each request to the right tier. This option derives both from that one origin: the SQL layer
 *  ({@link RindleApiServerOptions.database}) at `url` with `token`, and the control-plane client
 *  ({@link RindleApiServerOptions.daemon}) at the same `url` — so an application configures "a
 *  rindle", not two subsystems.
 *
 *  The derived control-plane client sends the same bearer. A unified ingress is an explicitly
 *  merged customer API-server trust tier: it routes SQL to the master and control requests to a
 *  follower, while the browser still receives neither credential. Deployments that keep those
 *  trust tiers separate pass an explicit {@link RindleApiServerOptions.daemon}; explicit fields
 *  always win over this derivation. */
export interface RindleConnectionOptions {
    /** The single ingress origin. Defaults to `$RINDLE_URL` — exported by `rindle dev` whenever
     *  the rendered topology collapsed read+write onto one ingress. */
    url?: string;
    /** Public subscription WebSocket endpoint returned on query leases. Defaults to {@link url} with
     *  `http:` → `ws:` / `https:` → `wss:`. Override when HTTP and WebSocket ingress differ. */
    wsUrl?: string;
    /** The server-side bearer for both legs of the unified ingress. Defaults to
     *  `$RINDLE_DATABASE_TOKEN` (also a `rindle dev` export); required through one of those channels
     *  unless both legs are configured explicitly. It never reaches the browser. */
    token?: string;
}

RindleApiServerOptions

export interface RindleApiServerOptions<User> {
    /** The sync/IVM control-plane client (leases, materialization, rooms). Optional once
     *  {@link rindle} is configured — the api-server then derives an HTTP client against the single
     *  ingress. Pass one explicitly for a tokened production fleet, a split/routed deployment, or a
     *  custom transport; an explicit client wins over the derivation. */
    daemon?: RindleDaemonClient;
    /** The unified connection (one URL, one key) that derives {@link daemon} and {@link database}
     *  from the fleet's single ingress. `rindle: {}` resolves both halves from the `rindle dev`
     *  environment. Any explicitly configured `daemon` / `database` / `sql` / `backend` field takes
     *  precedence over its derived counterpart. */
    rindle?: RindleConnectionOptions;
    /** Preferred managed setup. The API server constructs and owns its SQL client; authoritative
     *  mutators, `tx.sql`, and `scope.sql` use it, while `daemon` remains only the lease/query/
     *  materialization/room control plane. Mutually exclusive with {@link sql} unless `backend`
     *  explicitly replaces both. */
    database?: RindleDatabaseOptions;
    /** Advanced injection/test seam for an already-created SQL session. Most applications should
     *  configure {@link database} and never import `createSqlClient`. When present (and `backend` is
     *  absent), authoritative mutators run through {@link sqlBackend}. */
    sql?: SqlSession;
    /** Where mutations are applied and `lmid` is stamped ({@link MutationBackend}). Default:
     *  managed `sqlBackend` when `database` or `sql` is configured, otherwise the compatibility
     *  `daemonBackend(daemon)`. Pass `postgresBackend(...)` when Postgres is the source of truth. */
    backend?: MutationBackend;
    /** The typed schema (`createSchema`/`refineSchema`). Required only when a mutator uses the LOGICAL
     *  write vocabulary (`tx.insert`/`update`/`upsert`/`insertIgnore`/`delete`/`row`) — it drives the
     *  dialect SQL renderer (column order, pk, quoting). A logical op with no schema configured throws
     *  loudly. Pure raw-`tx.exec` mutators do not need it. */
    schema?: Schema;
    queries?: ApiQueries<User>;
    runQuery?: RunQuery<User>;
    mutators?: ApiMutators<User>;
    /** Optional gate before resolving a named query, for leases and one-shot reads. If omitted,
     *  no gate runs. The query definition still owns row visibility predicates. */
    authorizeQuery?: Authorizer<AuthorizeQueryInput<User>>;
    /** Optional gate before mutation execution. If omitted, no gate runs. Mutators still own
     *  row-level checks and business rules inside their authoritative transaction. */
    authorizeMutation?: Authorizer<AuthorizeMutationInput<User>>;
    /** Rindle Realtime (RINDLE-REALTIME-ENABLEMENT-DESIGN.md §3.1): the ONE named opt-in.
     *  Presence activates the room flush trio AND `/room-boot`; absence keeps every room
     *  endpoint 403. Takes precedence over the deprecated {@link authorizeRoom}. */
    realtime?: RindleRealtimeOptions<User>;
    /** The LM stream plane (LM-STREAM-CHECKPOINT-DESIGN.md): the ONE named opt-in for streaming a
     *  model response live while the durable store only ever sees coarse checkpoints. Presence
     *  activates {@link RindleApiServer.openStream}/{@link RindleApiServer.subscribeStream} and
     *  `/stream`; absence keeps them 403. */
    streams?: RindleStreamOptions<User>;
    /** The room write-authority gate (§5.3.1): validates the caller is a placed room —
     *  the epoch-bound flush credential rides `context.request`, and what it means is
     *  the app's to define. The room endpoints are DISABLED (403) until this is set:
     *  hosting a write authority is an explicit opt-in, never a default.
     *  @deprecated Use {@link realtime} (its `authorize`) — this bare form gates the
     *  flush trio but never activates `/room-boot`. When both are set, `realtime` wins. */
    authorizeRoom?: Authorizer<ApiContext<User>>;
    routes?: Partial<RindleApiRoutes>;
    mode?: StreamMode;
    materializationPolicy?: MaterializationPolicy | ((input: QueryLeaseRequest<User>) => MaybePromise<MaterializationPolicy>);
    leaseTtlMs?: number;
    /** Idle TTL (ms) the warm pipeline a one-shot SSR {@link RindleApiServer.readQuery read} leaves
     *  behind is held at (SSR-DESIGN.md §3.4) — it must comfortably cover page-load + client-boot +
     *  the follow-up live `subscribe` so the browser lands on a still-warm pipeline (the warm
     *  handoff). The TTL is NOT part of the dedup key (max-wins), so it only ever extends a shared
     *  query's window. Absent ⇒ the daemon's default idle TTL. */
    readIdleTtlMs?: number;
    subject?: string | ((input: QueryLeaseRequest<User>) => MaybePromise<string | undefined>);
    /** The ANONYMOUS routing key forwarded to the read router (READ-ROUTER-DESIGN.md §2.2) — used by
     *  HRW placement when there is no authenticated `subject`. The router keys on `subject ?? this`;
     *  the resolved value rides `metadata.routingKey` to the daemon. Default: the browser-supplied
     *  `clientId` (from the query POST body). NOTE: the default cannot co-locate an ANONYMOUS SSR read
     *  with the booting client — an SSR server can't see the browser's localStorage `clientId`, so the
     *  two legs compute different keys and §2.4's warm handoff misses (still correct — just an extra
     *  first-touch materialize). For anonymous SSR co-location, set this to read a server-set session
     *  cookie from `input.request` (it rides the SSR request AND every browser request). A routing
     *  HINT only — never authorization. Ignored by a single (unrouted) daemon, which has nothing to
     *  route. */
    routingKey?: string | ((input: QueryLeaseRequest<User>) => MaybePromise<string | undefined>);
    /** The EXPLICIT fleet pin fan-out — when set, {@link RindleApiServer.assertPins} fans each
     *  resolved pin across ALL live followers through it (a fleet control action over the machine
     *  list — FOLLOWER-AFFINITY-DESIGN.md §11) instead of materializing each pin once on the (single)
     *  daemon. A per-viewer `materialize` always routes ONE; a pin-assert always fans ALL — never
     *  inferred from `policy.kind`. Absent ⇒ single-daemon behavior (one materialize per pin). */
    pinFanout?: PinFanout;
    /** Named queries to keep permanently materialized via {@link RindleApiServer.assertPins}.
     *  Each is materialized with a `pinned` policy (survives zero subscribers) so late joiners
     *  attach to an already-warm result. Pins are viewer-independent — resolved with `pinUser`. */
    pinnedQueries?: PinnedQuery[];
    /** The user context pins resolve under (pins are shared, so they should not depend on a
     *  per-viewer identity). Defaults to `undefined`. */
    pinUser?: User;
    /** Surfaced when a SCOPED mutator ({@link scoped}) throws from code that runs AFTER `scope.transact`
     *  has already sealed the protocol outcome — a post-commit effect, or a compensation handler running
     *  after a business rejection. The outcome is fixed (this callback CANNOT change the client's
     *  response or the `lmid` advance), but the throw must not vanish: a failed refund is real money.
     *  Absent ⇒ the error is logged to `console.error`. */
    onScopeError?: (err: unknown, info: {
        phase: "committed" | "rejected";
        envelope: MutationEnvelope;
    }) => void;
}

RindleApiServer

export interface RindleApiServer<User> {
    readonly routes: RindleApiRoutes;
    /** Close the SQL client created from {@link RindleApiServerOptions.database}, and drop every live
     *  stream's readers and timers WITHOUT a durable write (that is {@link drainStreams}). Injected
     *  SQL sessions and custom backends remain caller-owned. Idempotent. */
    close(): void;
    /** Open an LM stream (LM-STREAM-CHECKPOINT §2): commits the durable POINTER row, then hands back
     *  the producer handle. A resolved handle means the message already exists for every client's
     *  query — so a subscriber that arrives before the first token has something to attach to.
     *  Throws 403 unless {@link RindleApiServerOptions.streams} is configured. */
    openStream(input: OpenStreamInput<User>): Promise<StreamHandle>;
    /** Attach a reader at `from` — the same call serves a first-touch subscriber (`from: 0`), a late
     *  joiner (`from` = the seq its IVM view shows), and a reconnect (`from` = `Last-Event-ID`).
     *  Terminates with `end`, or with `stale`/`absent` when the client should fall back to the
     *  durable plane (both are ordinary answers, never errors). */
    subscribeStream(input: SubscribeStreamInput<User>): Promise<StreamSubscription>;
    /** Parse a default `{streamId, from?}` subscribe body and run {@link subscribeStream}. For the
     *  GET + `EventSource` shape, use {@link streamResponse} (or build the body with
     *  `streamRequestFromHttp(request)` yourself). */
    handleStreamJson(body: unknown, context: ApiContext<User>): Promise<StreamSubscription>;
    /** The subscribe route in ONE call: parse a GET (`?streamId=…&from=…`, with `Last-Event-ID`
     *  winning), authorize + subscribe, and encode the SSE response. A refusal comes back as a JSON
     *  error `Response` (403 for denied or unconfigured) rather than a throw, so the route body is a
     *  single expression after authentication. For custom transports, compose
     *  `streamRequestFromHttp` + {@link subscribeStream} + `streamFramesToSse` instead. */
    streamResponse(request: {
        url: string;
        headers: {
            get(name: string): string | null;
        };
    }, context: ApiContext<User> & {
        keepAliveMs?: number;
    }): Promise<Response>;
    /** Checkpoint every live stream's outstanding tail, then seal it `interrupted`
     *  (LM-STREAM-CHECKPOINT §5). Wire it to SIGTERM: without it a rolling deploy drops each
     *  response's un-checkpointed tail and strands rows saying `streaming` forever. */
    drainStreams(): Promise<void>;
    createQueryLease(input: QueryLeaseRequest<User>): Promise<QueryLeaseResponse>;
    /** (Re-)materialize every `pinnedQueries` entry with a pinned policy. Idempotent — the daemon
     *  dedupes by canonical query, so a re-assert reuses the existing materialization. Call it at
     *  startup and whenever the daemon restarts (e.g. from the daemon-client `onBootId` hook), since
     *  the daemon holds no durable materialization state. No-op when `pinnedQueries` is empty. */
    assertPins(): Promise<void>;
    pushMutation(input: PushMutationRequest<User>): Promise<PushMutationResponse>;
    /** Apply an in-order batch (the client mutation queue's flush). Envelopes run strictly
     *  sequentially; a rejection still advances the daemon's lmid, so later envelopes in the
     *  batch stay contiguous and keep applying. A daemon ERROR throws for the whole batch —
     *  the client retries it and the daemon's mid dedup absorbs the already-applied prefix. */
    pushMutations(input: PushMutationsRequest<User>): Promise<PushMutationResponse[]>;
    /** One-shot SSR read (SSR-DESIGN.md §6): resolve `(name, args)` → AST (same authority path as a
     *  lease, `authorizeQuery` enforced), have the daemon serialize the current view once, and return
     *  the assembled rows for the loader to seed + dehydrate. Registers NO subscriber — a dropped
     *  render leaks nothing; the pipeline self-reclaims after the idle TTL ({@link
     *  RindleApiServerOptions.readIdleTtlMs}) unless the browser's follow-up `subscribe` lands first. */
    readQuery(input: QueryReadRequest<User>): Promise<QueryReadResponse>;
    handleQueryJson(body: unknown, context: ApiContext<User>): Promise<QueryLeaseResponse>;
    /** Parse a default `{name, args}` read body and run {@link readQuery}. */
    handleReadJson(body: unknown, context: ApiContext<User>): Promise<QueryReadResponse>;
    /** Accepts `{envelope}` (one) or `{envelopes: [...]}` (an in-order batch → array reply). */
    handleMutateJson(body: unknown, context: ApiContext<User>): Promise<PushMutationResponse | PushMutationResponse[]>;
    /** The room's flush (§5.3.1): gate on `authorizeRoom`, forward the txn to the write
     *  authority, and pass the store's verdict through VERBATIM — `200 {applied, cv}`,
     *  `409 {error:"fenced"|"conflict", …}`, or the loud identity `500`. Write
     *  `status` + `body` as-is; the room's `httpAuthority` decodes them. */
    handleApplyRowChangeTxnJson(body: unknown, context: ApiContext<User>): Promise<RoomHostResponse>;
    /** Claim the next placement epoch for a doc (§2.5), same gate + envelope. */
    handleClaimRoomEpochJson(body: unknown, context: ApiContext<User>): Promise<RoomHostResponse>;
    /** The room's boot probe (§3.3), same gate + envelope. */
    handleRoomLmidsJson(body: unknown, context: ApiContext<User>): Promise<RoomHostResponse>;
    /** The DO shell's cold-boot callback (§10.1; enablement §3.1): authenticate the shell
     *  secret, resolve the doc's footprint, claim the placement epoch, mint the upstream
     *  lease and the flush leg. 403 until {@link RindleApiServerOptions.realtime} is set. */
    handleRoomBootJson(body: unknown, context: ApiContext<User>): Promise<RoomHostResponse>;
}

RindleApiErrorCode

export type RindleApiErrorCode = "bad-request" | "forbidden" | "not-found" | "rejected";

RindleApiError

export declare class RindleApiError extends Error {
    readonly code: RindleApiErrorCode;
    readonly status: number;
    constructor(code: RindleApiErrorCode, message: string, status: number);
}

SplitDaemonClient

/**
 * A read/write-split `RindleDaemonClient` (READ-ROUTER-DESIGN.md §2.1). Writes
 * (`executeSqlTxn` / `rejectMutation` / `applyRowChangeTxn` / `migrate`) go to the single
 * write-master, UNCHANGED and never through the router; reads (`materialize` / `query` /
 * `dematerialize`, and raw `executeSqlRead`) go to the read router. Raw reads default to a replica
 * (`consistency:"eventual"`) so they scale off the write-master; pass `consistency:"strong"` on a
 * read to route it to the master for read-your-writes after a write to the same data. Hand one of
 * these to {@link createRindleApiServer} as `daemon` to point the reads leg at the router while
 * writes stay on the master — no placement logic enters the api-server.
 *
 * ```ts
 * const daemon = new SplitDaemonClient(
 *   new HttpRindleDaemonClient({ baseUrl: MASTER_URL, headers: writeAuth }),  // writes → master
 *   new HttpRindleDaemonClient({ baseUrl: ROUTER_URL, headers: routerAuth }), // reads  → router
 * );
 * ```
 */
export declare class SplitDaemonClient implements RindleDaemonClient {
    private readonly writes;
    private readonly reads;
    constructor(writes: RindleDaemonClient, reads: RindleDaemonClient);
    executeSqlTxn(input: SqlTxn): Promise<SqlTxnOutput>;
    executeSqlRead(input: SqlRead): Promise<SqlReadOutput>;
    rejectMutation(input: MutationRejection): Promise<MutationRejectionOutput>;
    beginMutationSession(input: MutationSessionBegin): Promise<MutationSessionBeginOutput>;
    execInMutationSession(input: MutationSessionExec): Promise<unknown>;
    queryInMutationSession(input: MutationSessionQuery): Promise<SqlReadOutput>;
    commitMutationSession(input: MutationSessionRef): Promise<SqlTxnOutput>;
    rollbackMutationSession(input: MutationSessionRef): Promise<unknown>;
    applyRowChangeTxn(input: RowChangeTxn): Promise<RowChangeTxnOutput>;
    claimRoomEpoch(input: ClaimRoomEpochInput): Promise<ClaimRoomEpochOutput>;
    roomLmids(input: RoomLmidsInput): Promise<RoomLmidsOutput>;
    migrate(input: MigrateInput): Promise<MigrateOutput>;
    materialize(input: MaterializeInput): Promise<MaterializeOutput>;
    query(input: QueryOnceInput): Promise<QueryOnceOutput>;
    dematerialize(input: DematerializeInput): Promise<DematerializeOutput>;
}

SqlDialect

/** A SQL dialect for the logical mutation renderer. */
export interface SqlDialect {
    readonly name: "sqlite" | "postgres";
    /** Render the i-th (1-based) bind placeholder. sqlite: `?`; postgres: `$i`. */
    placeholder(oneBased: number): string;
    /** Optional value coercion hook (e.g. a future SQLite `0/1` boolean). Default: identity. */
    encodeValue?(v: WireValue, type: ColType): WireValue;
}

sqliteDialect

export declare const sqliteDialect: SqlDialect;

postgresDialect

export declare const postgresDialect: SqlDialect;

TableRenderMeta

/** Per-table metadata the renderer needs (all reachable from a `TableMeta`). */
export interface TableRenderMeta {
    /** Columns in schema (wire) order — the stable INSERT column list + completeness check. */
    columns: string[];
    /** Primary-key column NAMES — the WHERE / ON CONFLICT target / SET partition. */
    pkNames: string[];
    /** Column name → declared type (only consulted by {@link SqlDialect.encodeValue}). */
    types: Record<string, ColType>;
    /** Columns a full insert must name — the non-nullable ones (design 206 §6.2). */
    required: string[];
    /** The nullable (omittable-to-null) columns — an omitted one binds `NULL` (design 206 §6.2). */
    nullable: ReadonlySet<string>;
}

RenderIndex

export type RenderIndex = Record<string, TableRenderMeta>;

buildRenderIndex

/** Build the {@link RenderIndex} from a typed schema (`schema.tables[name]` is a `TableMeta`). */
export declare function buildRenderIndex(schema: Schema): RenderIndex;

renderOp

/** Render one {@link MutationOp} to a `{sql, params}` for the dialect, or `null` for a no-op (an
 *  `update` whose row names only pk columns — nothing to SET, matching the client's no-op edit). */
export declare function renderOp(op: MutationOp, meta: TableRenderMeta, dialect: SqlDialect): SqlStatement | null;

renderPointRead

/** Render a point read (`tx.row`) — `SELECT <cols> FROM "T" WHERE <pk>` — for read-your-writes. */
export declare function renderPointRead(table: string, pk: KeyedRow, meta: TableRenderMeta, dialect: SqlDialect): SqlStatement;

BackendError

/** Thrown (wrapping the driver error) by a server tx's DB calls, so the seam can tell an INFRA
 *  failure (retry) from a mutator-body throw (business rejection). */
export declare class BackendError extends Error {
    readonly driverError: unknown;
    constructor(driverError: unknown);
}

daemonBackend

/** Legacy/private-plane adapter. Kept for existing deployments; its mutation policy is shared with
 * {@link sqlBackend}, so the two transports cannot drift. */
export declare function daemonBackend(daemon: RindleDaemonClient): MutationBackend;

sqlBackend

/** Run API-server mutators through `@rindle/sql-client`'s explicit mutation facade. Query leases,
 * SSR reads and room control continue to use `daemon`; only authoritative mutation execution moves
 * to the versioned SQL transport. */
export declare function sqlBackend(sql: SqlSession): MutationBackend;

PgQuery

/** The query surface a {@link PostgresPlugger} transaction exposes. `exec` runs one statement;
 *  `query` returns rows keyed by column name (read-your-own-writes inside the txn). */
export interface PgQuery {
    exec(sql: string, params?: unknown[]): Promise<void>;
    query(sql: string, params?: unknown[]): Promise<Array<Record<string, unknown>>>;
}

PostgresPlugger

/** The thin driver adapter that keeps `pg` / `postgres.js` out of this package's dependencies
 *  (`BYO-POSTGRES-LMID-CONTRACT-DESIGN.md` §6.2): run `fn` inside ONE transaction — commit on
 *  resolve, roll back on throw. {@link pgPoolPlugger} adapts a node-postgres `Pool`. */
export interface PostgresPlugger {
    transaction<T>(fn: (q: PgQuery) => Promise<T>): Promise<T>;
}

PostgresBackendOptions

export interface PostgresBackendOptions {
    /** Rewrite each MUTATOR statement's SQL before it runs (never the lmid upsert). The intended
     *  use is dialect bridging for a dual-topology app whose mutators are written SQLite-style:
     *  pass {@link questionToDollarParams} to convert `?` placeholders to `$1..$n`. */
    rewriteSql?: (sql: string) => string;
}

postgresBackend

/**
 * The BYO-Postgres {@link MutationBackend} (`BYO-POSTGRES-LMID-CONTRACT-DESIGN.md` §6.3): one PG
 * transaction runs the mutator's statements and ALWAYS upserts `_rindle_client_mutations` —
 * the upsert sits outside any acceptance guard by construction, so the §2.4 footgun (a rejection
 * that forgets to advance `lmid` and wedges the client's pending queue) cannot be written.
 *
 * Confirmation does NOT come from this call's response: the lmid row rides the same PG commit
 * through CDC → relay → follower and reaches the client in the same coherent release as the
 * data (§8.2 relocated upstream). A rejection's `reason` still returns on the HTTP reply, but
 * nothing rejection-shaped travels the replication path — the optimistic prediction snaps back
 * when the advanced `lmid` arrives.
 */
export declare function postgresBackend(plugger: PostgresPlugger, opts?: PostgresBackendOptions): MutationBackend;

PgPoolLike

/** The slice of a node-postgres `Pool` the plugger needs — structural, so `pg` stays a
 *  dependency of the APP, never of this package. */
export interface PgPoolLike {
    connect(): Promise<{
        query(sql: string, params?: unknown[]): Promise<{
            rows: Array<Record<string, unknown>>;
        }>;
        release(err?: unknown): void;
    }>;
}

pgPoolPlugger

/** Adapt a node-postgres `Pool` (or anything pool-shaped) to a {@link PostgresPlugger}:
 *  one client per transaction, `BEGIN`/`COMMIT` bracketing, `ROLLBACK` + rethrow on failure. */
export declare function pgPoolPlugger(pool: PgPoolLike): PostgresPlugger;

questionToDollarParams

/**
 * Rewrite SQLite-style `?` positional placeholders to Postgres `$1..$n`, for mutators written
 * once and run against either backend (pass as {@link PostgresBackendOptions.rewriteSql}).
 * Skips `'…'` string literals (with `''` escapes), `"…"` quoted identifiers, `--` line comments,
 * and non-nested C-style block comments. Do not mix `?` and `$n` styles in one statement.
 */
export declare function questionToDollarParams(sql: string): string;

defineApiQueries

export declare function defineApiQueries<User, Q extends ApiQueries<User>>(queries: Q): Q;

registerQueries

/**
 * Register a list of co-located client {@link NamedQuery `defineQuery`} values as the server's query
 * surface — the bulk, no-boilerplate counterpart to {@link defineApiQueries}. Each query already
 * carries its wire `name` and a `resolve` that re-runs its validator on the UNTRUSTED wire args and
 * builds the authoritative `Query`, so the server just imports every co-located query and hands the
 * list here. The same validated args build a byte-identical AST on both tiers.
 *
 * The query's AUTHORITATIVE {@link ApiContext} is forwarded as `resolve`'s ctx — so a context-scoped
 * `defineQuery` (e.g. "my issues") is built from the server's trusted principal, never the client's.
 * A context-free query simply ignores the extra argument.
 *
 * Use {@link defineApiQueries} instead (or in addition) only when the server must DIVERGE from the
 * client — define a server-specific `defineQuery` with the same name and register that.
 *
 * ```ts
 * queries: registerQueries<User>([issuesPageQuery, issueDetailQuery, recentCommentsQuery, usersQuery]),
 * ```
 */
export declare function registerQueries<User>(queries: readonly NamedQuery<any, any, any>[]): ApiQueries<User>;

defineApiMutators

export declare function defineApiMutators<User, M extends ApiMutators<User>>(mutators: M): M;

sharedApiMutators

/**
 * Bulk-register a SHARED (generator) mutator registry as server mutators — the mutator twin of
 * {@link registerQueries} (which does the same for co-located `defineQuery` values). Each shared
 * mutator carries its own arg validator (`shared(schema, gen)`), so this wraps every one with the
 * UNIVERSAL server triad and nothing else: parse the UNTRUSTED wire args (its `.args`), map the server
 * {@link MutationContext} to the shared {@link MutatorCtx} principal, and drive the SAME body the
 * client predicts ({@link runSharedMutation}). The point is that a shared mutator whose server run
 * adds NO authority beyond that triad needs no hand-written wrapper.
 *
 * Server-only AUTHORITY the client cannot predict (a title guard, an owner-gated cascade, a
 * `NOT EXISTS` dedup) stays an explicit {@link ApiMutator} that OVERRIDES the auto-wrapped default —
 * spread this first, then the overrides win by key:
 *
 * ```ts
 * mutators: defineApiMutators({
 *   ...sharedApiMutators(sharedMutators, (ctx) => ({ user: requireUser(ctx.user) })),
 *   createIssue: withTitleGuard(sharedMutators.createIssue), // + server-only policy
 *   deleteIssue: async (tx, raw, ctx) => { ... },            // raw owner-gated cascade
 * }),
 * ```
 */
export declare function sharedApiMutators<User>(registry: Record<string, SharedMutatorWithArgs<any>>, principal: (ctx: MutationContext<User>) => MutatorCtx): ApiMutators<User>;

guardMutator

/**
 * Wrap a SHARED (generator) mutator with a row-level ACCESS GUARD — the multi-tenant authz twin of
 * {@link sharedApiMutators}. It parses the untrusted wire args, derives the {@link MutatorCtx}
 * principal (the SAME mapping you pass to `sharedApiMutators`), evaluates `predicate` against the OPEN
 * mutation txn (so it can READ the rows the write depends on), and throws `forbidden` (403 — the
 * client's optimistic write snaps back) when access is denied; otherwise it drives the SAME body the
 * client predicts ({@link runSharedMutation}). Use it for the entries that need server-only authority
 * the client cannot predict, OVERRIDING the auto-wrapped default (spread `sharedApiMutators(...)`
 * first, then the guarded overrides win by key):
 *
 * ```ts
 * const principal = (ctx) => ({ user: requireUser(ctx.user) });
 * mutators: defineApiMutators({
 *   ...sharedApiMutators(sharedMutators, principal),
 *   updateSlide: guardMutator(sharedMutators.updateSlide, principal,
 *     async (tx, a, { user }) =>
 *       (await tx.query(q.slide.where.id(a.slideId).where(editableBy(user)).one())) != null,
 *     { message: "not permitted to edit this slide" }),
 * }),
 * ```
 *
 * The predicate keeps the shared body READ-FREE, so the client's `.folded` hot paths (drag/keystroke)
 * still fold — the read is server-side only. Return `false` to deny (→ the default or `opts.message`
 * forbidden); return `true`/nothing to allow. To reject with a different status/message (a business
 * rejection, a not-found), throw a {@link RindleApiError} from inside the predicate instead. `principal`
 * runs before the predicate, so it too may throw `forbidden` for an anonymous caller.
 */
export declare function guardMutator<User, Args>(gen: SharedMutatorWithArgs<Args>, principal: (ctx: MutationContext<User>) => MutatorCtx, predicate: (tx: ServerMutationTx, args: Args, ctx: MutatorCtx) => boolean | void | Promise<boolean | void>, opts?: {
    message?: string;
}): ApiMutator<User, unknown>;

ShapeExemplar

/** One exemplar invocation for {@link dumpQueryShapes} — the `args`/`user` a query is built with.
 *  Literal values never matter to the dump (shapes are deduped with literals stripped); what an
 *  exemplar buys is BRANCH coverage, so supply one per code path a query function can take
 *  (an optional filter present/absent, each enum axis, …). */
export interface ShapeExemplar<User = unknown> {
    args?: unknown;
    user?: User;
}

QueryShapesDoc

/** The query-shapes document `rindle indices suggest` consumes: the app's synced tables (name +
 *  primary key) and one wire AST per structurally distinct shape a registered query can build. */
export interface QueryShapesDoc {
    tables: Array<{
        name: string;
        primaryKey: string[];
    }>;
    queries: Array<{
        name: string;
        ast: Ast;
    }>;
}

dumpQueryShapes

/**
 * Dump every registered named query's wire AST — feeder 1 ("exemplar enumeration") of
 * `rindle indices suggest` (docs/INDEXING.md applied mechanically to the query set).
 *
 * Because named queries are FUNCTIONS of `(args, ctx)`, one query can build structurally
 * different ASTs on different args; each exemplar invocation contributes its shape, and shapes
 * that differ only in literal values (a limit, a filter string) dedupe to one entry. A query
 * with no configured exemplars is invoked once with no args. The registry is the app's whole
 * server-side query surface, so the resulting document is the complete static shape set —
 * modulo arg-value-dependent branches, which need an exemplar (or runtime shape recording) to
 * surface.
 */
export declare function dumpQueryShapes<User>(opts: {
    schema: Schema;
    queries: ApiQueries<User>;
    exemplars?: Partial<Record<string, ReadonlyArray<ShapeExemplar<User>>>>;
}): Promise<QueryShapesDoc>;

createRindleApiServer

export declare function createRindleApiServer<User = unknown>(options: RindleApiServerOptions<User>): RindleApiServer<User>;

runSharedMutation

/** Run a SHARED generator mutator (the SAME body the client predicts) against a live server
 *  transaction (MUTATORS-ISOMORPHIC): bind the tier-agnostic {@link isoTx} factory and drive it —
 *  each yielded logical op renders + runs against `tx` (dialect SQL, per backend), each `tx.row`
 *  suspends for read-your-writes, and `tx.all` fans out. A server mutator uses this to delegate its
 *  write body after parsing untrusted args and applying its server-only authority (principal, policy).
 *  A mutator-body throw remains a business rejection; a DB failure propagates as infra. */
export declare function runSharedMutation<Args, Ctx extends MutatorCtx>(mutator: SharedMutator<Args, Ctx>, args: Args, ctx: Ctx, tx: ServerMutationTx): Promise<void>;