/*! * Copyright (c) 2026 Interop Alliance. All rights reserved. */ /** * The client-annex generation ensure a TRANSIENT visit runs -- a session * holding nothing but a standing unlock credential (its ladder seed, its * `delegatedClients` sibling delegation, and the standing-client identity * derived from the typed secret). Six durable states cut such a visit off * from the annex, or from the account log the annex is pointed at: the * account document carries no `#DelegatedClients` pointer; the pointed * auxiliary Space is gone from the server; the pointed * generation's log is gone (GC'd, or never minted); the embedded generation * delegation is expired, inside its renewal window, or signed by a key the * document no longer lists; the record carries no sibling delegation, or its * sibling targets another Space; the record's bridge delegation is expired, * inside its renewal window, or signed by a key the document no longer * lists. On a ladder-anchored account -- the ladder VM a document * verification method, the ladder's rungs the log's update keys -- the visit * itself can mend all five, and this module is the orchestrator: a * converging ensure that detects each state from durable state, mends it * with the existing annex primitives, and reports what ran. * * The ordering rules are the established ones, composed rather than * re-decided: * * - RENEW PRECEDES MINT: a live, verifiable pointed generation is renewed in * place (`ensureGenerationDelegationCurrent`, the ladder-signed minter); * only a dead generation -- or one whose log does not commit this * credential's annex rung (`ClientAnnexRungUncommittedError`, the same * escape the GC swap's no-committed-survivor arm takes) -- gets a fresh * mint. * - The PRE-FLIGHT RUNG ATTRIBUTION precedes any mint that will need a * pointer entry: when no current account-log update key is a rung of this * ladder, nothing is minted at all -- a generation the pointer entry could * not then name would only widen the orphan window. * - A fresh generation in an EXISTING Space mirrors the GC swap's stage * order minus its revoke stage (mint, install the delegation, re-point -- * no transient reach could invoke the old delegation's revocation; the * pointer move retires it on a conforming server and it otherwise rots on * its TTL); a fresh SPACE is created under the ladder VM's bare did:key, * the one identity a create may name, and its controller is flipped to the * account DID in the next request, before anything publishes into it. The * stranding window is therefore one request wide: a run torn inside it * leaves a did:key-controlled Space no server orphan sweep can reap, and a * run torn past the flip leaves an account-controlled one that a sweep * can. The generation then mints in that Space exactly as it does in an * existing one, under the ladder-signed sibling delegation, which is why * the flip must precede the mint. * - A POINTED SPACE THAT IS GONE is decided before any write, and by TWO * reads rather than one, since a storage server masks an unauthorized read * as the same 404 an absent Space answers: a ladder-signed GET-only child * of the Space's root, then a root invocation as the ladder VM's bare * did:key (the controller a torn establishment leaves behind). Only when * both answer a real 404 is the Space gone. Status alone decides, so a 2xx * is present whatever its body says and every other answer throws. The * first probe presupposes a server admitting the ladder delegation * clause's single-verb predicate; against an older one both reads are * refused alike and a live Space reads as gone. * - The BRIDGE RENEWAL PRECEDES EVERY ARM: the record's bridge delegation is * the credential's one write path into the account log, so a stale one is * replaced before any arm runs and the caller's account-log store is built * over the usable bridge (the `idStoreFor` factory). A pointer entry in * either minting arm would otherwise ride a delegation the server refuses. * - Pointer entries go through the caller's account-log store with * `logOnly: true`: a bridge-delegated writer has no `did.json` projection * rights, and the log is the source of truth. * * Both renewable record delegations -- the bridge, and the sibling (minted * when the record carries none, when it targets another Space, or when it is * stale) -- are handed back through the REQUIRED `onRebindRecord` seam after * the generation and pointer are durable. The seam always receives BOTH * usable delegations, whichever of them was freshly minted, so the caller * re-seals the unlock record from one pair; a run torn before that re-seal * re-derives everything from the ladder seed at the next visit. A re-seal * that fails is fatal when the sibling was fresh and is reported on the * outcome's `bridgeResealError` when only the bridge was, since the fresh * bridge already served this visit and the next visit re-mints it. */ import type { IZcap } from '@interop/data-integrity-core'; import type { ZcapClient } from '@interop/ezcap'; import { WasClient, type ServiceDescription } from '@interop/was-client'; import type { PublishedWebvhLog, WebvhIdStore } from '../webvh/didWebvh.js'; import type { ICapabilityAgent } from '../webvh/zcap.js'; import { attributeLadderRung } from './ladder.js'; import type { PointerEntryOutcome } from './log.js'; /** * The HTTP status a raw signed request's rejection carries, when it carries * one. `WasClient.request` applies no error mapping, so the status is all a * caller has to dispatch on, and different transports hang it in different * places (`status`, or `response.status`). Exported as the one reader of * those two places, shared with the establishment's authorization-refusal * check. * * @param err {unknown} * @returns {number | undefined} */ export declare function rawRequestStatus(err: unknown): number | undefined; /** * Why the visit cannot mend the annex. * * `'ladder-vm-not-anchored'`: this credential's ladder VM is not a * verification method of the account document, so nothing ladder-signed can * verify. A standing credential's VM stands for as long as the credential * does -- enrollment leaves it alone -- so this is the backstop for a * document that never carried it: a credential whose establishment was torn * before its document entry, or a visit by a credential to an account * another credential's ladder anchors (a passkey visiting an account the * passphrase established). * * `'update-key-not-attributable'`: a pointer entry is needed, but the account * log carries no rung of this ladder at all -- no revealed key and no * committed hash -- or the attribution is ambiguous, so the entry could not * be signed. A merely committed rung is not this state: the pointer move * reveals it first. */ export type ClientAnnexGenerationUnavailableReason = 'ladder-vm-not-anchored' | 'update-key-not-attributable'; /** * The typed refusal of {@link ensureCredentialClientAnnexGeneration}: the * account is not in a shape this visit can mend, and nothing was written. * Matched on `name` -- error classes do not survive crossing package copies. */ export declare class ClientAnnexGenerationUnavailableError extends Error { readonly reason: ClientAnnexGenerationUnavailableReason; constructor({ reason, message }: { reason: ClientAnnexGenerationUnavailableReason; message: string; }); } /** * The ladder-signed generation-delegation minter: the * `mintGenerationDelegation` closure shape `ensureGenerationDelegationCurrent` * takes, signing with the credential's ladder VM (`ladderVmZcapClient`) -- * the renewal must not depend on the very delegation it replaces, and on a * ladder-anchored account the ladder VM is the one licensed delegator. * Exported on its own: the transient App Connect approval's blocking renewal * stage consumes the same closure. * * @param options {object} * @param options.accountDid {string} the account did:webvh * @param options.ladderSeed {Uint8Array} the credential's ladder seed, from * its unlock record * @param options.wasServerUrl {string} the ACCOUNT Space's storage server * @param options.spaceId {string} the ACCOUNT Space's id (the delegation's * target subtree) * @param [options.now] {number} epoch milliseconds, for tests * @returns {Function} `({ clientAnnexDid }) => Promise` */ export declare function ladderSignedGenerationDelegationMinter({ accountDid, ladderSeed, wasServerUrl, spaceId, now }: { accountDid: string; ladderSeed: Uint8Array; wasServerUrl: string; spaceId: string; now?: number; }): (options: { clientAnnexDid: string; }) => Promise; /** * What one ensure pass did. Honest skips never throw: a `false` member means * the durable state was already current, not that a stage failed. A * superseded generation's own delegation is never revoked here (no transient * reach could invoke the revocation); the pointer move retires it on a * conforming server, and it otherwise rots on its TTL. */ export interface ClientAnnexGenerationEnsureOutcome { clientAnnexDid: string; generationDelegation: IZcap; /** * The usable bridge delegation -- the record's own, or the fresh one * `onRebindRecord` was handed. */ delegation: IZcap; /** * The usable sibling delegation -- the record's own, or the fresh one * `onRebindRecord` was handed. */ delegatedClients: IZcap; generationMinted: boolean; spaceMinted: boolean; /** * Which arm minted the Space: set when the account document's pointer * named an auxiliary Space the server no longer has, so a fresh one was * minted and pointed at in its place. `spaceMinted` is set with it. */ pointedSpaceMissing: boolean; delegationRenewed: boolean; siblingReminted: boolean; bridgeReminted: boolean; /** * Set when the re-seal of a renewed bridge failed and nothing else needed * the re-seal; the fresh bridge still served this visit, and the next * visit re-mints. */ bridgeResealError?: unknown; /** * The pointed generation's verified head, for the enrollment that follows * to build its first attempt on rather than re-reading the same log. * * Present ONLY when this pass published nothing to that log -- a pure no-op * report on a healthy account. A minted generation and a renewed delegation * both leave the head this member would carry superseded, and the publish * seam hands back no ETag for the post-write one, so the member is absent * and the enrollment reads for itself. */ generationLog?: PublishedWebvhLog; } /** * Ensures a transient visit can reach a live client-annex generation with a * current generation delegation and a usable sibling delegation, mending * from durable state alone (see the module doc for the states and the stage * orders). A healthy account is a pure no-op report. * * Known residue: the pre-flight rung attribution runs against the SUPPLIED * account view, while the pointer entry's own publish re-reads the log. The * no-orphan guarantee therefore holds against that view; a concurrent * ceremony advancing the rung between the two makes the pointer entry fail * loudly AFTER the mint, leaving an inert unpointed generation this ensure * does not reuse -- the next run converges on a fresh generation, and the * orphan is the standing collect fan-out's to pick up. * * @param options {object} * @param options.wasServerUrl {string} the account pointer's host * @param options.spaceId {string} the ACCOUNT Space's id * @param options.account {object} the VERIFIED account log view * (`{ did, doc, log }` -- the caller's `verifyAccountLog` read; never * re-fetched here) * @param options.ladderSeed {Uint8Array} the credential's ladder seed, from * its unlock record * @param options.standingClient {object} the standing-client identity * derived from the typed secret: `did` (the sibling delegation's * controller) and `zcapClient` (its signer, which invokes annex requests * under the sibling capability) * @param options.bootstrapWasFor {Function} `({ keyAgent }) => WasClient` * -- the storage client for the fresh-Space arm, signing as the ladder * VM's bare did:key (the caller wires the transport; the agent is derived * here from the ladder seed) * @param options.delegation {IZcap} the record's bridge delegation (PUT on * the account's `did.jsonl`), renewed here when it is stale * @param options.idStoreFor {Function} * `({ delegation }) => WebvhIdStore` -- builds the ACCOUNT log's store over * the usable bridge (a bridge-delegated store suffices: pointer entries * publish with `logOnly: true`). Called once, after the bridge renewal, so * a pointer entry never rides a lapsed delegation * @param options.onRebindRecord {Function} * `({ delegation, delegatedClients }) => Promise` -- REQUIRED: * re-seals the unlock record with the usable bridge and sibling * delegations; called whenever either was freshly minted, after the * generation and pointer are durable * @param [options.delegatedClients] {IZcap} the record's sibling * delegation, when the record carries one * @param [options.serviceDescription] {ServiceDescription} the server's * service description a client the caller already holds discovered * (`(await was.service()).description`), so this one skips discovery * @param [options.now] {number} epoch milliseconds, for tests * @returns {Promise} */ export declare function ensureCredentialClientAnnexGeneration(options: { wasServerUrl: string; spaceId: string; account: Pick; ladderSeed: Uint8Array; standingClient: { did: string; zcapClient: ZcapClient; }; bootstrapWasFor: (options: { keyAgent: ICapabilityAgent; }) => WasClient; delegation: IZcap; idStoreFor: (options: { delegation: IZcap; }) => WebvhIdStore; onRebindRecord: (options: { delegation: IZcap; delegatedClients: IZcap; }) => Promise; delegatedClients?: IZcap; serviceDescription?: ServiceDescription; now?: number; }): Promise; /** * The annex Space, in the settled resolution order: the account document's * `#DelegatedClients` pointer names it; else the record's sibling * delegation's target does (converging a torn establishment onto its own * stranded Space instead of minting another orphan); else nothing does and * the caller mints fresh. The one statement of the rule, shared by the * transient visit's ensure here and the establishment's stage-3 primitive. * * @param options {object} * @param options.doc {object} the VERIFIED account document * @param [options.delegatedClients] {IZcap} the record's sibling * delegation, when the record carries one * @returns {object} `pointer` (the pointed annex DID), `siblingSpaceId` * (the sibling's target Space), and `annexSpaceId` (the resolved Space, or * `undefined` when a fresh one must be minted) */ export declare function resolveClientAnnexSpaceId({ doc, delegatedClients }: { doc: PublishedWebvhLog['doc']; delegatedClients?: IZcap; }): { pointer?: string; siblingSpaceId?: string; annexSpaceId?: string; }; /** * The pre-flight rung attribution every pointer-moving arm runs before * minting anything: this ladder has a rung the pointer entry will be able to * sign with, either standing in `updateKeys` already (`revealed`) or * committed in `nextKeyHashes` and revealable by {@link movePointerAsLadder} * (`committed`). A ladder the log carries no rung of at all, and an * ambiguous attribution, refuse with * {@link ClientAnnexGenerationUnavailableError} before a generation or a * Space is minted -- the one place the ladder's `LadderAttributionError` * maps onto that refusal. * * @param options {object} * @param options.ladderSeed {Uint8Array} * @param options.log {DIDLog} the VERIFIED account log * @returns {Promise<{ rung: LadderRung, state: LadderRungState }>} */ export declare function attributePointerEntryRung({ ladderSeed, log }: { ladderSeed: Uint8Array; log: PublishedWebvhLog['log']; }): Promise>>; /** * The `#DelegatedClients` pointer move as a credential-only caller makes it: * ONE ladder-signed pointer entry ({@link setDelegatedClientsPointer} on the * ladder arm). Each attempt attributes the ladder's current rung from the * head it builds on, the rung reveals itself in the entry it signs, and when * it stood only committed the entry commits the next rung's hash beside it. * The one shape every ladder-held pointer move runs: the transient readiness * pass's fresh-generation arm, and the establishment's stage 3 (whose first * attempt builds on the head it minted or read, threaded in as `published`). * * A self-enrollment's add entry spends the revealed rung, so on any account * that has ever self-enrolled the rung is merely committed and the reveal is * what makes the pointer entry signable at all. * * ACCEPTED CONSEQUENCE (design FW-356, finding R3): the entry retires * nothing, so the acting rung stands in the account log's `updateKeys` * afterwards. The price of a pointer move is therefore a standing account * update key in the credential's hand -- direct document-edit authority * through the bridge with no further reveal -- retired at that credential's * next self-enrollment (whose add entry drops the attributed rung) or at its * retirement. This is documented rather than prevented. * * Attribution runs inside the conflict retry, so a racing ceremony that * consumes the rung between the read and the PUT climbs to the winner's * committed rung instead of refusing `update-key-not-attributable` on a rung * that is no longer current. A caller that signed the pointer entry with a * pair fixed before the retry would instead re-run a rung the winner retired, * and the client arm's not-authorized refusal is not a conflict, so its retry * would end there -- after the annex Space and generation were minted, with * nothing naming them. The pre-flight guard ({@link attributePointerEntryRung}) * therefore cannot fire from staleness here: it runs on the caller's snapshot * before anything is minted, while the entry is built on a head the attempt * read itself. * * @param options {object} * @param options.idStore {WebvhIdStore} the account log's store -- the * record's bridge delegation on a transient visit, the root-invoking store * in the establishment's stage 3 * @param options.ladderSeed {Uint8Array} the credential's ladder seed * @param options.clientAnnexDid {string} the generation to point at * @param options.accountDid {string} the account DID the log must resolve * to. The read and the publish run under the store's own chain-head pin * @param [options.logOnly] {boolean} whether the pointer entry publishes * the log alone (default `true`, a bridge-delegated writer's whole reach); * the establishment's root window passes `false` so its `did:web` * projection is republished beside the entry * @param [options.published] {PublishedWebvhLog} a head the caller already * read or published under the same pin, ETag included: the FIRST attempt * attributes and builds on it instead of reading, and a lost * compare-and-swap there falls through to the reading retry with its whole * budget (`withThreadedHeadOnce`) * @returns {Promise<{ did: string, doc: DIDDoc, published: PublishedWebvhLog, * rung: LadderRung }>} the pointer entry's outcome (the head it leaves * standing, ETag included) and the rung it was signed with -- the ladder's * current rung, which a lost race may have climbed past the caller's own * attribution; on the idempotent already-pointed path, the rung the * attempt attributed */ export declare function movePointerAsLadder({ idStore, ladderSeed, clientAnnexDid, accountDid, logOnly, published }: { idStore: WebvhIdStore; ladderSeed: Uint8Array; clientAnnexDid: string; accountDid: string; logOnly?: boolean; published?: PublishedWebvhLog; }): Promise>; //# sourceMappingURL=heal.d.ts.map