import { type AnchorResolver } from "./endpoint-signing.js"; export * from "./endpoint-session-rail.js"; /** Grant validity ceiling — a session grant is LIVE-class authority (§13.6 "expiry per the * handle rules"; it is one-use and epoch-bound on both sides, never sturdy). */ export declare const SESSION_GRANT_MAX_TTL_MS: number; export declare const SESSION_GRANT_MAX_BYTES: number; /** §13.6 "fresh unguessable sessionId": at least 22 base64url chars (≥128 bits) — enforced on * CALLER-SUPPLIED ids too, so a short guessable id cannot ride in through the mint arg. */ export declare const SESSION_ID_MIN_CHARS = 22; /** Mint a fresh unguessable sessionId: 32 CSPRNG bytes, base64url (43 chars, within the * bounded id-token grammar the subject builder pins). */ export declare function mintSessionId(): string; /** The §13.6/§13.10 session grant: one-use, holder-bound, epoch-pinned on BOTH sides. */ export interface SessionGrant { v: 1; sessionId: string; space: string; endpoint: string; /** BOTH rail subjects, explicit in the signed form AND re-derived at verification. */ subjects: { in: string; out: string; }; /** The redeeming caller: live authority — bound to lifecycle AND current process epoch. */ holder: { id: string; lifecycleUid: string; processEpoch: number; }; /** The serving instance the session is pinned to; the epoch rides the subjects too. */ serving: { instanceId: string; epoch: number; }; /** Max in-flight (unacknowledged) data frames per direction. */ window: number; iat: number; nbf?: number; exp: number; /** Freshness/anti-replay token for the establishment exchange (the one-use is the ledger * create-CAS; the nonce only disambiguates re-issued grants for the same pair). */ nonce: string; /** Signing key (anchor registry, role `sessions`). */ issuer: { keyId: string; }; sig: string; } /** Everything {@link mintSessionGrant} needs besides the signing key. */ export interface MintSessionGrantArgs { space: string; endpoint: string; sessionId?: string; holder: { id: string; lifecycleUid: string; processEpoch: number; }; serving: { instanceId: string; epoch: number; }; window?: number; ttlMs: number; issuerKeyId: string; now?: number; } /** Build + sign a session grant (the serving side's establishment answer). The rail subjects * are DERIVED here — the signature pins them; a verifier re-derives and compares. */ export declare function mintSessionGrant(args: MintSessionGrantArgs, keyPair: { sign(input: Uint8Array): Uint8Array; }): SessionGrant; /** Parse + verify a presented session grant, D28-exact: the signature verifies over the EXACT * raw artifact (sig absent), the parsed projection is for semantics only; the anchor is * resolved FRESH (role `sessions`, scope ceiling covers the endpoint); the rail subjects are * re-derived and compared; currency is checked at `now`. Fail-closed everywhere; every nested * object is a CLOSED schema. */ export declare function verifySessionGrant(raw: unknown, opts: { space: string; resolveAnchor: AnchorResolver; now?: number; }): Promise; export declare const SESSION_TERMINAL_STATES: readonly ["closed", "expired", "superseded", "retired"]; export type SessionTerminalState = (typeof SESSION_TERMINAL_STATES)[number]; export type SessionState = "issuing" | "active" | SessionTerminalState; /** The durable revocation authority that survives the serving endpoint (§13.6). Both * credential ids are recorded FROM CREATION (the `issuing` write), so a crash at any later * point leaves a row that names exactly what the sweep must revoke. The row also pins the * ENDPOINT: `instanceId` is unique only within `(space, endpoint)`, so a row without the * endpoint could not prove WHICH endpoint's serving party is entitled to retrieve/close. */ export interface SessionLedgerRow { sessionId: string; /** The serving endpoint name — with `serving.instanceId`/`epoch` this is the full serving * identity every serving-party operation authenticates against. */ endpoint: string; serving: { instanceId: string; epoch: number; }; holder: { principal: string; lifecycleUid: string; }; /** The WINNING grant's Ed25519 signature — the full verified-artifact identity (it covers * window, holder processEpoch, iat/nbf, nonce, issuer, everything signed). The lost-response * retry re-releases ONLY when the presenting grant's signature equals this, so a DIFFERENT * signed grant that merely reuses the sessionId + the compared coordinate subset can never * re-release the winner's credential (§13.6/§13.10). */ grantSig: string; credCaller: string; credServing: string; /** Per-credential revocation completion (durable), created all-false: set only when that * id's revoke SUCCEEDED, so a swallowed revoke failure anywhere is retried by every later * sweep pass instead of silently leaking half a pair. */ revoked: { caller: boolean; serving: boolean; }; state: SessionState; exp: number; } /** The auth-store key (`session.`, §13.12). */ export declare function sessionLedgerKey(sessionId: string): string; /** The monotonic state grammar: `issuing → active`, `issuing → terminal` (the sweep collecting * a crashed half-issue), `active → terminal`. Terminal states never transition. */ export declare function assertSessionStateTransition(from: SessionState, to: SessionState): void; type MaybePromise = T | Promise; /** The auth path's durable half: real implementations back this with the auth-store KV * (create-only CAS / revision-pinned update); smokes supply a faithful in-memory fake. */ export interface SessionLedger { /** AUTHORITATIVE row read (leader-served, the §13.9 read-service class): the serving * retrieval and the holder's post-finalize retry decide on THIS, never on a caller-supplied * projection. `undefined` = no row. */ read(sessionId: string): MaybePromise; /** Create-only CAS of the `issuing` row (naming BOTH credential ids). `"exists"` = the * one-use is already burned. */ createIssuing(row: SessionLedgerRow): MaybePromise<"created" | "exists">; /** CAS `issuing → active`. `false` = the row moved under us (a close/sweep/barrier raced the * finalize; the redemption LOSES and releases nothing). */ finalizeActive(sessionId: string): MaybePromise; /** CAS the row to a terminal state (close/expiry/barrier/abandoned redemption). `false` = * already terminal (idempotent for the caller's purposes). */ transitionTerminal(sessionId: string, to: SessionTerminalState): MaybePromise; /** Durably mark ONE credential id's revocation as COMPLETED. The sweep's terminal-row retry * is real only because the row remembers which halves confirmed. */ markRevoked(sessionId: string, credentialId: string): MaybePromise; } /** The two per-session credential IDs (the credential-ledger ids revocation names). Allocated * BEFORE the one-use `issuing` create, so the row names both from its first durable write and a * crash at any later point leaves the sweep able to revoke the whole pair. */ export interface SessionCredentialIds { credCaller: string; credServing: string; } /** One released per-session credential: the id revocation names, the usable creds bytes, and * the credential's OWN expiry — which MUST be ≤ the session row's `exp` (the seam validates * and fails loud on a hook that mints past the session's life). A credential is authority * ONLY once its session row is `active`; nothing releases usable bytes before finalize. */ export interface SessionCredential { id: string; creds: string; exp: number; } /** One OBSERVED lifecycle issuance gate (§13.1): the auth-store gate key plus its revision at * the observation (a LEADER-SERVED read). The staged credential writes are PINNED to it — a * barrier that moves the gate between the observation and the stage makes the pinned write * LOSE. This is what makes the two-gate stage→commit ordering testable in core rather than an * opaque promise: the pin is data, and an adversarial probe can move the gate under it. */ export interface LifecycleGatePin { key: string; revision: number; } /** The AUTHENTICATED presenter of a redemption — established by the trusted auth path's own * connection/exchange (§9/§10), NEVER read from the grant. {@link redeemSession} refuses * unless it equals the grant's holder exactly: possession of a leaked grant releases nothing. */ export interface SessionPresenter { id: string; lifecycleUid: string; } /** * Everything {@link redeemSession} needs from the trusted auth path. The LIFECYCLE FENCE is the * revision-pinned {@link stagePair} write against both parties' OBSERVED gates (a moved gate * makes it LOSE), NOT a boolean read — fresh reads are not fences (§13.1/§13.9). The * process-epoch and gate reads ARE fencing/observation reads and MUST be leader-served (the * auth bucket `allow_direct=false` → `STREAM.MSG.GET`, per the §13.9 read-service class), * never a follower Direct Get. Every hook re-runs FRESH at finalize; a cached answer would * reopen the §13.1 window. */ export interface SessionRedemptionHooks { ledger: SessionLedger; /** Allocate the two credential-ledger ids for this session (bounded ids, NO usable bytes) so * the `issuing` row can name both from its first write. */ allocateCredentialIds(grant: SessionGrant): MaybePromise; /** The holder's CURRENT process epoch (leader-served read), or undefined when the lifecycle * has no live process. */ holderProcessEpoch(holder: { id: string; lifecycleUid: string; }): MaybePromise; /** The serving instance's CURRENT epoch (leader-served read), or undefined when it is not * registered/live. */ servingEpoch(endpoint: string, instanceId: string): MaybePromise; /** LEADER-SERVED observation of the HOLDER's lifecycle issuance gate (§13.1). Throws when * the gate is gone (the lifecycle is retired). */ observeHolderGate(holder: { id: string; lifecycleUid: string; }): MaybePromise; /** LEADER-SERVED observation of the SERVING instance's lifecycle issuance gate. */ observeServingGate(endpoint: string, instanceId: string): MaybePromise; /** Stage BOTH per-session credential-ledger rows, each write REVISION-PINNED to its party's * observed gate (§13.1): caller = pub `in` + sub `out` EXACT, serving = the reverse. A gate * that moved since its observation (a barrier retired the lifecycle) makes the pinned write * LOSE — THROW; this is the lifecycle fence, a write loss, never a boolean read. The rows * are indexed under each lifecycle (a later-winning barrier enumerates and revokes them), * but confer NOTHING and release NO usable bytes until finalize. */ stagePair(grant: SessionGrant, ids: SessionCredentialIds, pins: { holder: LifecycleGatePin; serving: LifecycleGatePin; }): MaybePromise; /** After finalize → `active`, release ONE party's usable credential by id. IDEMPOTENT for * the row's lifetime: a repeated release of the same id returns the SAME credential bytes * (never a re-mint) — that is the authenticated lost-response retry path for BOTH parties. * Safe because every release sits behind exact presenter authentication (holder equality at * {@link redeemSession}; full serving identity at {@link retrieveServingCredential}), so a * repeat delivers no authority the party does not already hold; revocation (a terminal row) * is the authority boundary, not a release count. Per-party sinks: no private material ever * crosses between the two parties. */ releaseCredential(sessionId: string, credentialId: string): MaybePromise; /** Revoke one staged/released credential by id, IDEMPOTENTLY (re-revoking a dead id * succeeds — the sweep's terminal-row retry depends on it). */ revokeCredential(id: string): MaybePromise; now?(): number; } /** * Redeem a VERIFIED session grant (§13.6 finalize-CAS ordering), presented by an AUTHENTICATED * presenter. The panel-locked order — no half-issued session is ever usable, a redemption * racing a close loses its finalize and releases nothing: * * 0. the presenter must equal the grant's holder EXACTLY (identity before anything — * possession of a leaked grant releases nothing, §13.10 holder-binding); * 1. allocate both credential ids (no bytes; bounded, distinct); * 2. create-CAS the `issuing` row naming BOTH ids (the one-use — a duplicate loses here, * EXCEPT the authenticated holder retrying an active row after a lost response, which * re-releases the SAME holder credential); * 3. observe both lifecycle issuance gates (leader-served) and stage both credential rows * REVISION-PINNED to them (the lifecycle FENCE: a moved gate makes the pinned write lose); * 4. fresh-check both process epochs (leader-served reads) AND grant expiry; * 5. finalize-CAS `issuing → active` (a racing close/barrier wins here); * 6. release ONLY the HOLDER's credential (the serving side retrieves its own separately). * * The caller passes the output of {@link verifySessionGrant} (signature/anchor/currency already * enforced there) plus the presenter its OWN authenticated context established. Returns the * HOLDER's credential alone — {@link retrieveServingCredential} delivers the serving side's, so * no private material crosses between the two parties. A release failure AFTER finalize leaves * the row `active` and throws: the authenticated holder retries this same call and lands on the * re-release path (release is idempotent for the row's life), so a transient release outage is * recoverable without a half-session. */ export declare function redeemSession(grant: SessionGrant, presenter: SessionPresenter, hooks: SessionRedemptionHooks): Promise; /** The serving instance retrieves ITS OWN credential after the session is `active`, through its * own authenticated path (never the holder's redemption response — no private material crosses * between the two parties, §13.6 per-party release). The presenter is the AUTHENTICATED serving * identity (endpoint + instanceId + epoch, established by the auth path's own context); the row * is read AUTHORITATIVELY from the ledger, never accepted as a caller-supplied projection. * Release is idempotent for the row's life (lost-response retry), behind the exact identity. */ export declare function retrieveServingCredential(sessionId: string, presenter: { endpoint: string; instanceId: string; epoch: number; }, hooks: Pick): Promise; /** The expiry sweep's per-row decision (the auth path enumerates `session.>` and calls this): * an `issuing` or `active` row past its `exp` (plus the caller's margin) transitions * `expired` and BOTH credential ids are revoked by name, each MARKED on success. A TERMINAL * row with an UNMARKED id is retried — that retry (not a comment) is what makes every * swallowed revoke failure in this module safe: the mark is set only by a revoke that * succeeded, so half a pair can never quietly outlive its session. Returns whether this pass * did work. Fully-collected terminal rows are never touched (retention: rows live at least * max session exp + a recovery margin, §13.6). */ export declare function sweepSessionRow(row: SessionLedgerRow, hooks: Pick, opts: { now: number; marginMs?: number; }): Promise; //# sourceMappingURL=endpoint-session.d.ts.map