/** * The AWAITABLE CHECKPOINT (SPEC §13.6): ONE durable pause primitive (approvals, guard holds, * payment authorization) as a pattern over the `cp` record, the one-use `epf….cp.` * settle fact, and the MEDIATED timer plane (§13.2/§13.12) — zero new transport. * * The moving parts and their owners (§13.9 rows): * - the `cp` RECORD (commit path): spec = the minted checkpoint (token, the goal it pauses, * the bound holder); status = `waiting` with the DEADLINE GENERATION — the monotonic * counter every heartbeat CAS-advances BEFORE replacing the timer. * - the SETTLE FACT (commit path): ONE create-only CAS on `epf..cp.` is the * arbiter for the checkpoint's single settlement — a RESUME and the deadline EXPIRY race * there, first claim wins, the loser observes it. Resume authorization is thereby ONE-USE * (a duplicate resume is `conflict`) and expiry FAILS CLOSED (a post-expiry resume is * refused with the recorded expiry). * - the TIMER WRITER (its own principal): instances publish only `.schedule` REQUESTS into * the schedules-DISABLED `EPT_REQ` stream, where any client-set scheduling header is inert * bytes — and the writer REJECTS a request carrying one (the ADR-51 confused-deputy * closure). The writer alone publishes the authoritative `.armed` (on EPT) with * `Nats-Schedule-Target` = the sibling `.fire` derived from the AUTHENTICATED request * subject's own tokens, never a body field. Every arm is FENCED: the writer reads the * `.armed` subject's last sequence, proves the authoritative status LEADER-SERVED * (`STREAM.MSG.GET`, never a possibly-follower Direct Get), and publishes with * `Nats-Expected-Last-Subject-Sequence` pinned to that read — a delayed writer whose proof * was superseded is rejected by the BROKER, so `.armed` can never roll back to a stale * deadline. A same-`(timerId, generation)` arm re-derives * the same `.armed` — the server's same-subject rollup makes a duplicate a no-op * replacement, so the durable RECONCILER simply re-emits a `.schedule` at the current * generation for every `waiting` status it owns; over-emission is harmless and a missing * schedule is repaired without any status↔schedule read. * - FIRE handling (the endpoint's trusted seam): act only on a fired message whose * broker-authored `Nats-Scheduler` header equals the exact sibling `.armed` subject AND * whose carried `(timerId, generation)` matches current status AND whose deadline is due * at the owner's clock; everything else is discarded (forged) or a stale no-op. * * Deadlines are mandatory; clocks are inputs (`now`), never a module-internal Date.now. */ import type { KV } from "@nats-io/kv"; import { type JetStreamClient, type JetStreamManager } from "@nats-io/jetstream"; import { type MsgHdrs, type NatsConnection } from "@nats-io/transport-node"; import { type EpCaller } from "./endpoint-subjects.js"; /** A checkpoint's coordinates: the owning endpoint + the minted token. */ export interface CheckpointRef { endpoint: string; token: string; } /** The one-use settle-fact subject (`epf..cp.`, §13.9). */ export declare function checkpointSettleSubject(space: string, ref: CheckpointRef): string; /** The checkpoint SPEC (written once at mint): what is paused and who may resume. `holder` is * MANDATORY (§13.6:1622/§13.10:2315: checkpoint resume is holder-bound — an omitted holder * would make the token a BEARER credential resumable by anyone who learns it). The deep * signature verification is the capability-handle slice; this seam enforces the recorded * identity. `goal` is present iff the checkpoint pauses an action goal. */ export interface CheckpointSpecValue { v: 1; token: string; goal?: { caller: EpCaller; goalId: string; }; /** The guard's VERIFIED signed obligations, persisted at mint so they SURVIVE the pause * (§13.6/§13.10: obligations are MUST-apply and reusable within their goal — a hold that * verified them and then dropped them would release the executor with zero attenuations). * Present iff the hold carried a non-empty set; release/reconcile return exactly this * recorded set, signatures intact. Only meaningful with a `goal` binding. */ obligations?: CheckpointObligation[]; holder: { id: string; lifecycleUid: string; }; mintedAt: number; /** The initial (generation-1) deadline, recorded in the IMMUTABLE spec (distsys 8dcad72 M3): * a mint that crashes after the spec exists but before the status is created must, on retry, * re-derive the SAME initial deadline from the spec - never silently install a different one * because the status was missing. Heartbeats extend the LIVE deadline in the status; this is * only the original intent, fixed at mint. */ initialDeadline: number; } /** The persisted shape of one guard obligation (structurally identical to the guard module's * GuardObligation; typed here to keep this module free of a runtime guard dependency). */ export interface CheckpointObligation { v: 1; space: string; requestId: string; signer: { keyId: string; }; attenuations: unknown[]; iat: number; exp: number; sig: string; } /** The checkpoint STATUS — the SINGLE SETTLEMENT ARBITER. Its KV revision is the one * linearization point heartbeat, resume, and fire all contend on (a settle CAS-es the status * BEFORE deriving the EPF fact, so a stale-generation fire loses to a heartbeat that advanced * the revision). `state` is `waiting` until the ONE settlement; the deadline generation is the * monotonic heartbeat counter and `deadline` is the CURRENT generation's absolute bound. When * settled it carries the settlement details (`settledHolder`/`settledGeneration`/`settledTs`) so * the derived one-use `epf..cp.` fact is RECONSTRUCTABLE — a crash between the status * CAS and the fact publish is repaired by re-deriving the fact from the settled status. */ export interface CheckpointStatusValue extends Record { state: "waiting" | "resumed" | "expired"; deadlineGeneration: number; deadline: number; observedSpecRevision: number; settledGeneration?: number; settledHolder?: { id: string; lifecycleUid: string; }; settledTs?: number; /** * WHICH answer a resume accepted (the checkpoint-answer record's id), recorded so the derived * settle fact is reconstructable field-for-field after a crash between the CAS and the publish. * Present only on a `resumed` settlement, and OPTIONAL there: a plain approval hold carries no * answer payload, and inventing an id for it would name a record that does not exist. */ settledAnswerId?: string; } /** The one-use settlement: resume and expiry race for this single create-only CAS. */ export interface CheckpointSettleFact { v: 1; token: string; settle: "resumed" | "expired"; generation: number; holder?: { id: string; lifecycleUid: string; }; /** * The ANSWER this settlement accepted. * * The payload of an answer — its value, its artifact — deliberately does NOT live here: these * keys are closed and this fact is the small arbiter of a race. But the arbiter has to NAME what * it chose. Every resolver reaches a workflow checkpoint through the run driver and therefore * presents as the SAME principal, so an answer cannot be matched back to the winning settle by * its presenter: the key `(token, answerId)` and this field are what discriminate. A loser's * answer record is orphaned and read by nothing. * * Absent on an expiry (nothing was accepted) and on a resume that carried no answer. */ answerId?: string; ts: number; } /** Read the checkpoint's current status (`undefined` = unknown token). Fail-closed on DEL. */ export declare function readCheckpointStatus(kv: KV, ref: CheckpointRef): Promise<{ value: CheckpointStatusValue; revision: number; } | undefined>; /** Publish a `.schedule` REQUEST onto the mediated timer plane — the ONLY timer publish an * instance holds (§13.9). Plain bytes, NO scheduling headers (a header would be inert on the * schedules-disabled request stream and the writer rejects it anyway); the mint, the * heartbeat, and the durable reconciler all share exactly this emission, which is what makes * reconciliation over-emission harmless (same `(timerId, generation)` → the writer re-derives * the same `.armed`, a no-op replacement). */ export declare function emitScheduleRequest(js: JetStreamClient, space: string, args: { endpoint: string; instanceId: string; epoch: number; token: string; generation: number; deadline: number; }): Promise; /** Mint a checkpoint (the commit path, §13.6): the durable token's spec + a `waiting` status * at generation 1 with the MANDATORY deadline, then the `.schedule` request. The record is * durable BEFORE the timer exists; a crash between the two is exactly what the reconciler's * re-emit repairs. */ export declare function mintCheckpoint(kv: KV, js: JetStreamClient, space: string, args: { ref: CheckpointRef; instanceId: string; epoch: number; goal?: { caller: EpCaller; goalId: string; }; /** The guard's VERIFIED obligations to persist across the pause (goal-bound holds only); * an empty array is treated as absent. */ obligations?: CheckpointObligation[]; /** MANDATORY (§13.6/§13.10): resume is holder-bound; an omitted holder = a bearer token. */ holder: { id: string; lifecycleUid: string; }; deadline: number; now: number; }): Promise<{ specRevision: number; }>; /** Heartbeat/extend: gate on the SETTLE FACT first (a settled checkpoint refuses and its * lagging status is converged), then CAS-advance the deadline generation IN STATUS FIRST, then * replace the timer (a new `.schedule` at the new generation — the mediated writer's * same-subject `.armed` publish is the server rollup; the 2.14 atomic stop-plus-publish is NOT * assumed at the 2.12 floor). The generation order is load-bearing: a crash after the CAS and * before the emission leaves a stale-generation timer whose fire NO-OPS at the handler, and the * reconciler re-emits the current generation — never a fire acting on a superseded deadline. */ export declare function heartbeatCheckpoint(kv: KV, js: JetStreamClient, jsm: JetStreamManager, space: string, args: { ref: CheckpointRef; instanceId: string; epoch: number; deadline: number; now: number; }): Promise; /** The timer writer's RESOURCE-ATTESTED context: an OPAQUE token carrying only the ONE space * this writer serves. The broker resources it operates with (the `.armed` publish rail and the * leader-read authority) are derived from ONE connection at construction and live in a * module-private WeakMap keyed by the frozen token — they are not reachable properties, so a * holder of the token cannot rebind the writer's publish rail or its status authority after * construction (`ctx.js = evil` has nothing to assign to), and a hand-assembled look-alike * carries no resources at all (the same containment as the receipt and contract stores). */ export interface TimerWriterContext { readonly space: string; } export declare function timerWriterContext(nc: NatsConnection, space: string): Promise; /** A running timer-writer pump. `stop()` ends the loop and waits it out; `done` rejects if the * pump dies of a fault that is not one message's own (a lost connection, a refused bind), so a * host can log WHY its space stopped expiring pauses instead of discovering it by silence. */ export interface TimerWriterHandle { stop(): Promise; readonly done: Promise; } /** * Host the timer writer: the one standing consumer that turns `.schedule` requests into the * authoritative `.armed` publishes (SPEC 13.2). Everything it serves already lives here — * {@link armCheckpointTimer} carries the fence, the fresh-check, and the self-heal — so this is * only the pump: ensure the `timerw_` durable on EPT_REQ, fetch, arm, ack. * * WITHOUT A RUNNING WRITER NO PAUSE ON THE SPACE EVER EXPIRES: a mint or reconcile emits the * schedule request and nothing turns it into an armed broker schedule. The delivery daemon hosts * this pump on a live mesh; the suites pump {@link armCheckpointTimer} by hand because grading * WHEN a timer is armed is their subject. * * THE ERROR SPLIT IS THE ACK DECISION. A request the writer refuses by its own rules — malformed, * foreign-space, carrying a client scheduling header — answers identically on every redelivery, * so it is TERMINATED (poison, logged, never redelivered). Anything else (an `unavailable` status * authority, a broker hiccup) is NAKed with a delay and retried: the request is fine, the moment * was not. A fault outside a message's own handling ends the pump through `done`, because a * writer that swallowed its own death would leave the space silently unable to expire anything. */ export declare function startTimerWriter(nc: NatsConnection, space_: string, opts?: { ackWaitMs?: number; pollMs?: number; }): Promise; /** The TIMER WRITER: turn one authenticated `.schedule` request into the authoritative `.armed` * publish. The armed/fire subjects derive from the REQUEST SUBJECT's own tokens (never body * fields); the body must agree with the subject's timerId; any scheduling header on the request * is a loud refusal; a request whose subject space is not the context's space is refused (the * writer's status authority answers for ONE space). The writer FRESH-CHECKS the authoritative * `(generation, deadline)` LEADER-SERVED within a bounded budget and arms ONLY the current * generation under the {@link armCoordinate} broker CAS fence — a stale/delayed request is * DISCARDED (`{ armed: false }`) and a delayed PUBLISH is rejected by the broker itself, so * `.armed` can never roll back to a superseded deadline. A current request re-derives the same * `.armed`; the server rollup makes it an idempotent no-op replacement (what the reconciler's * over-emission rests on). After a successful arm the writer RE-READS the status and, if a * heartbeat advanced it mid-flight, immediately arms the LIVE coordinate (bounded self-heal; * the durable reconciler stays the crash backstop). */ export declare function armCheckpointTimer(ctx: TimerWriterContext, msg: { subject: string; headers?: MsgHdrs; data: Uint8Array; }, opts?: { statusBudgetMs?: number; }): Promise<{ armed: boolean; armedSubject?: string; fireSubject?: string; generation?: number; reason?: "stale" | "settled" | "unknown"; }>; export type CheckpointFireVerdict = { acted: false; reason: "forged-origin" | "stale-generation" | "not-waiting" | "not-due" | "re-armed"; } | { acted: true; settle: CheckpointSettleFact; won: boolean; }; /** Handle one fired timer message: act ONLY on a fire whose broker-authored `Nats-Scheduler` * header equals the exact sibling `.armed` subject (anything else is forged and discarded), * whose `(timerId, generation)` matches the CURRENT waiting status (stale fires no-op: a * heartbeat superseded that deadline), and whose authoritative deadline is due at the * owner's clock. A valid fire EXPIRES the checkpoint by claiming the one-use settle CAS — * where a concurrent resume may have lawfully claimed first: the fire then no-ops observing * it. Expiry fails the checkpoint CLOSED (status `expired`; the paused goal's own terminal * path is the action commit point). */ export declare function handleCheckpointFire(kv: KV, js: JetStreamClient, jsm: JetStreamManager, space_: string, args: { ref: CheckpointRef; /** The instance/epoch the checkpoint's timer is armed under — bound so a colliding-token * fire for a DIFFERENT instance/epoch can never settle this ref. */ instanceId: string; epoch: number; msg: { subject: string; headers?: MsgHdrs; data: Uint8Array; }; now: number; }): Promise; /** Resume a waiting checkpoint: HOLDER-BOUND (the spec's recorded holder MUST be the * authenticated presenter) and ONE-USE (the status-arbiter CAS is the single settlement). A * resume AT/AFTER the authoritative deadline does NOT claim `resumed` — expiry fails closed, so * it drives/observes the EXPIRED settlement instead. Contends on the status revision with a * concurrent heartbeat/fire: a stale (heartbeat-superseded) attempt retries once. */ export declare function resumeCheckpoint(kv: KV, js: JetStreamClient, jsm: JetStreamManager, space_: string, args: { ref: CheckpointRef; presenter: { id: string; lifecycleUid: string; }; now: number; answerId?: string; }): Promise; /** Read the recorded settlement (`undefined` = still waiting). */ export declare function readCheckpointSettle(jsm: JetStreamManager, space_: string, ref: CheckpointRef): Promise; /** Read the checkpoint's recorded SPEC (`undefined` = unknown token; fail-closed on a deletion * marker). The spec records WHAT the checkpoint pauses (its optional `goal` binding) and WHO * may resume (`holder`) — a caller that must confirm a token pauses THIS goal reads it here. */ export declare function readCheckpointSpec(kv: KV, ref: CheckpointRef): Promise; /** OWNER-forced expiry (§13.6): the pause's owner settles a DUE checkpoint `expired` without a * broker fire — used when the owner already knows the hold deadline passed (a guard-hold * expiry). Idempotent and fail-closed: an already-settled checkpoint returns its recorded * settlement (the owner observes the winner); a still-live checkpoint (`now < deadline`) * REFUSES (only a due checkpoint expires); a stale-generation CAS loss re-reads the winner. * Never resets a settled checkpoint and never fabricates a settlement. */ export declare function expireCheckpoint(kv: KV, js: JetStreamClient, jsm: JetStreamManager, space_: string, args: { ref: CheckpointRef; now: number; }): Promise; /** The durable reconciler's re-emission (§13.6): for every genuinely-`waiting` checkpoint the * endpoint owns, re-emit the `.schedule` request at the CURRENT generation. Idempotent at the * writer (same-generation re-arm is a rollup no-op), so over-emission is harmless and a missing * schedule is repaired without observing whether one exists. GATED ON THE SETTLE FACT, not the * status projection: an already-settled checkpoint whose status still lags `waiting` (the * crash window) is converged and NOT re-armed — without this gate the reconciler would re-emit * schedules forever for a settled checkpoint (the C1 timer leak). */ export declare function reconcileCheckpointSchedule(kv: KV, js: JetStreamClient, jsm: JetStreamManager, space_: string, args: { ref: CheckpointRef; instanceId: string; epoch: number; }): Promise<{ reEmitted: boolean; generation?: number; }>; //# sourceMappingURL=endpoint-checkpoint.d.ts.map