/*! * Copyright (c) 2026 Interop Alliance. All rights reserved. */ /** * The mend entry point: a converging ensure over the tear states the * credential-anchored establishment can leave, so any door into a torn * account (a transient login, a remembered resume, a future step-up, another * wallet app) runs the same repairs instead of hand-rolling its own. The * arms, in order -- each fires at most once per invocation, and arms may * cascade within one invocation (there is deliberately no repair-wide * single shot): * * - The ESTABLISHMENT arm, when the pointer names no did:webvh. It probes * durable state first: an account log that already resolves, attributes * to this credential's ladder, AND carries the delegated-clients pointer * means the DID-less record is a RECORD DOWNGRADE left by a concurrent or * stale heal, and the mend is re-binding the record to the published DID * -- never re-running stage 1, which would re-write the DID-less record * and then die on the promoted Space, permanently. A revealed rung with * NO pointer is instead the stage-3 tear (the pointer entry never * landed), and falls through with every other probe outcome to the whole * establishment run, itself the ensure. Either way the arm returns * immediately with `reenter: true` on convergence: the record changed * durably, so the caller must re-fetch it through its own keyring fetcher * and re-enter -- and the single-shot re-entry marker stays CALLER-side, * on that re-entry glue (a mend-internal counter would reset on each * fresh invocation, letting a host that pins a stale DID-less record * drive an unbounded establish/re-fetch loop). A re-bind additionally * reports `reenterRepairShaped: true` -- its root registry window is * closed, so the caller's re-entry must carry `repairShaped: true` for * the registry arm to fire. A throw from the establishment run is caught * into the report as the arm's error, never propagated raw -- with one * exception: the probe's `ResourceLogContinuityError` rethrows by name, * since a served rollback, fork, or identity switch must surface as the * continuity refusal it is, never be met with a fresh establishment. * - The PROMOTION arm, on a promoted-pointer record. Two triggers: a * caller-supplied failed delegated read (`delegatedRead`), or an * authority-neutral probe. The probe must not mistake healthy for torn: * `space.describe()` under the bootstrap did:key cannot distinguish * "unpromoted" from "unauthorized" (WAS masks refusals), so a null read * is EVIDENCE OF PROMOTION and the tear classifies only on an authorized * read showing a non-account controller. The mend is * `ensurePromotedSpaceController` under the ladder VM's bare did:key. On * the delegated-read trigger the arm retries the read once and, on a * promotion attempt or retry that still fails, RETHROWS THE ORIGINAL * error unchanged (a healthy account's doomed promotion attempt lands * here too, which is what keeps a mere transport flap distinguishable * caller-side). Only a promotion that WROTE (`promoted` / `healed`) marks * the entry repair-shaped for the arms below; a `confirmed` outcome means * the account was healthy and the failed read a flap. The probe-triggered * direction has no antecedent error; its non-convergence is a report * member. * - The ROSTER-AND-EPOCHS arm, gated on the completion test "roster * delivered AND every encrypted collection carries epoch[0]" -- durable * state alone, never roster presence alone: a present roster is followed * by the completion probe (a Description read plus a verified * governing-log read per encrypted collection, the stated budget; epoch * presence is read off the verified log, never off the server-derived * Description member) unless another trigger already fired the arm. The healthy fast path never invokes the mend at all. A fresh user * key is minted ONLY when the shared stage's own decide-read observes the * roster absent, and only under the mint preconditions -- checked at that * same mint decision (`beforeMint`), whichever trigger fired the arm: no * client-local roster-epoch pin held, no OTHER standing credential published * in the verified document, no encrypted collection already epoch'd or * unreadable -- a fabricated-absent roster must not become a * single-recipient genesis that evicts every other standing credential. * The ensure, the delivered-key re-read, and the fan-out are the shared * mint-policy stage ({@link ensureRosterDeliveredEpochs}); a lost * roster-genesis race adopts and reports converged-elsewhere, and a * roster adopted with no wrap for this credential is its own outcome, * never folded into "no roster". A FAILED roster or descriptor read is a * transport error, never an incompleteness signal: it reports, and * neither fires a spurious mint nor a spurious refusal. * - The REGISTRY arm, on an entry the invocation found repair-shaped (an * arm above mended, or the caller flagged it): re-fires the caller's * read-first `beforePromotion` hook under the caller-supplied * post-promotion authority. The hook encapsulates its own registry * protocol (read-first upsert, skip on a refused read); the mend knows * nothing of the registry shape and only synthesizes the * establishment-shaped context the hook expects. Detection therefore * costs nothing on the healthy fast path: no tear, no hook fire. * * Caller obligations (they cross a package boundary, so they are contract, * not convention): * * - `account` MUST come from a BINDING-VERIFIED standing record: the * pointer, controller, and ladder seed are trusted here, and a caller * wiring the mend to a signature-checked but not binding-verified record * would let a malicious host steer a full establishment (and the * promotion arm) at an attacker-chosen account. * - Loudness ordering: every arm past the establishment arm exercises * credential-derived authority (a Space Description PUT, roster appends, * collection re-epochs) that extends no world-readable log, so the caller * must have made its loud entry (the transient enrollment, or an enrolled * client's standing) before invoking those arms. * - The re-entry single-shot marker for the establishment arm lives on the * caller's re-entry glue, exactly as today's `healAttempted` rides. * * The return contract is deliberately mixed, and callers consume it as * typed API: the establishment and roster arms report non-convergence as * outcome members (the arm's own error carried on the member); the * promotion arm THROWS the original delegated-read error unchanged when * that trigger's mend or retry fails, and reports on the probe-triggered * direction. "Converged" always means the durable state the arm gated on * CHANGED, never merely that the arm ran without throwing. */ import type { DIDLog } from '@interop/did-method-webvh'; import type { IKeyAgreementKey, IZcap } from '@interop/data-integrity-core'; import type { WasClient } from '@interop/was-client'; import { type EncryptionDescriptorStore } from '@interop/was-client/edv/core'; import type { ZcapClient } from '@interop/ezcap'; import type { KmsAuthenticationBinding, WebvhIdStore } from '../webvh/didWebvh.js'; import type { ICapabilityAgent } from '../webvh/zcap.js'; import type { AccountPointer } from '../keyring/recordEnvelope.js'; import { type UserKey } from '../keys/index.js'; import type { CredentialAnchoredBindRecordHook, CredentialAnchoredEstablishment } from './establish.js'; import { type StageNotifier } from '../log.js'; /** * The BINDING-VERIFIED account core the mend acts on, as one object rather * than loose fields: the caller's codec (the keyring layer) must have * verified the record's binding MAC before these members are trusted -- see * the module doc's caller obligations. */ export interface CredentialAnchoredAccountCore { controller: string; pointer: AccountPointer; ladderSeed: Uint8Array; } /** * The registry-arm context: the standing record members the synthesized * `beforePromotion` context carries in place of a live establishment result. * All of it comes from the caller's own record; the mend records nothing of * the registry protocol itself. */ export interface CredentialAnchoredRegistryContext { unlockSpaceId: string; manageCapability?: IZcap; delegation?: IZcap; delegatedClients?: IZcap; unlockKeyAgreementKeyId?: string; unlockKeyAgreementKeyMultibase?: string; } /** * What one mend invocation did. A member is present iff its arm FIRED (found * its tear, or was triggered); a healthy account that was never repair-shaped * produces an empty report. `reenter: true` says the establishment arm * changed the record durably (a completed establishment, or a record re-bind) * and the caller must re-fetch the record and re-enter -- carrying its own * single-shot marker across that re-entry. */ export interface CredentialAnchoredMendReport { reenter: boolean; /** * Set beside `reenter: true` when the re-entered record is still * repair-shaped: the record-downgrade re-bind rewrote the record but ran * no registry hook (the root window a live establishment's own write uses * is permanently closed here). The caller's re-entry glue MUST pass * `repairShaped: true` on the re-entry invocation so the registry arm * fires under the post-promotion authority. */ reenterRepairShaped?: boolean; establishment?: { converged: boolean; outcome: 'established' | 'rebound'; did?: string; error?: unknown; }; promotion?: { converged: boolean; outcome?: 'promoted' | 'retried'; error?: unknown; }; rosterEpochs?: { converged: boolean; outcome?: 'delivered' | 'converged-elsewhere' | 'no-wrap' | 'mint-refused'; userKey?: UserKey; error?: unknown; epochsFailed?: Array<{ collectionId: string; error: unknown; }>; }; registry?: { converged: boolean; skipped?: string; error?: unknown; }; } /** * Mends a credential-anchored account from whatever tear state its durable * artifacts show (see the module doc for the arms, their order, the caller * obligations, and the mixed return contract). The options are a superset of * {@link establishCredentialAnchoredAccount}'s hooks -- the establishment arm * hands them through verbatim -- so a caller passes one bundle. * * @param options {object} * @param options.account {CredentialAnchoredAccountCore} the * BINDING-VERIFIED account core (controller, pointer, ladder seed) from * the caller's standing record; never loose or unverified fields * @param options.standing {object} the credential's standing client * identity (`clientDid`, `keyAgreementKeyMultibase`, `recipientKid`, * `keyAgreementKey`), as the establishment takes it * @param options.bindRecord {CredentialAnchoredBindRecordHook} REQUIRED: * the unlock-record codec closure; the establishment arm's re-run and the * record-downgrade re-bind both write through it * @param options.rosterStoreFor {Function} REQUIRED: the establishment's * bootstrap-invoked roster store builder (`({ did, log }) => store`); used * only inside the establishment arm's re-run * @param options.collectionStoreFor {Function} REQUIRED: the * establishment's bootstrap-invoked per-collection descriptor store builder * (`({ did, log }) => (collectionId) => store`); used only inside the * establishment arm's re-run, and handed through verbatim * @param options.bootstrapWasFor {Function} REQUIRED: * `({ keyAgent }) => WasClient` signing as the ladder VM's bare did:key; * the promotion arm's mend and the establishment arm ride it * @param options.idStore {WebvhIdStore} the account's `id` collection * store (the establishment arm's probe and the registry arm's rung * attribution read through it) * @param options.lowEntropy {boolean} threaded to the establishment arm * (the hash-commitment fail-safe is the establishment's) * @param [options.priorCreatedAt] {string} the standing record's freshness * stamp; threads to the establishment re-run (skipping its first bind) and * to the record-downgrade re-bind * @param [options.delegatedClients] {IZcap} the record's sibling * delegation, for the establishment arm's stage-3 Space resolution * @param [options.provideKmsAuthentication] {Function} the * establishment's best-effort KMS thunk, handed through with its contract * (a served `keys.json` is adopted only after its `vmId` checks out against * the session's keystore listing) * @param [options.promoteKeystore] {Function} the establishment's * best-effort keystore promotion, handed through * @param [options.beforePromotion] {Function} the caller's read-first * registry hook. The establishment arm's re-run fires it in its own root * window; the registry arm re-fires it under the post-promotion * `invocation` authority on a repair-shaped entry. It must encapsulate its * own read-first-and-skip-on-refused-read rule * @param [options.invocation] {object} the post-promotion authority * triple: `was` and `zcapClient` signing as the caller's live invocation * identity (a transient visit's annex identity, or an enrolled client), and * `capability` (the generation delegation) their requests ride. Required * by the roster-and-epochs and registry arms * @param [options.rosterStore] {EncryptionDescriptorStore} the user-key * roster's store under the SAME post-promotion authority, with a * ladder-signed log signer -- the roster arm's store (the bootstrap * `rosterStoreFor` cannot serve it: the promoted Space refuses bootstrap * invocations) * @param [options.collectionStore] {Function} `(collectionId) => * EncryptionDescriptorStore` -- each encrypted collection's descriptor * store under that same post-promotion authority, the sibling of * `rosterStore`. Required by the roster-and-epochs arm, whose fan-out * lands each collection's epoch[0] through it * @param [options.delegatedRead] {object} the promotion arm's * failed-delegated-read trigger: `error` (the original failure, rethrown * unchanged on a non-converging mend) and `retry` (re-runs the caller's * read once after the promotion lands; its result stays caller-side) * @param options.hasRosterEpochPin {Function} REQUIRED: * `() => Promise` -- the mint precondition port: whether this * caller holds a client-local roster-epoch pin for the account. A caller with * no client-local pins (the transient visit's in-memory pins) passes * `async () => false` explicitly, so "no pin" is always a statement, never * a dropped option * @param [options.registry] {CredentialAnchoredRegistryContext} the * registry arm's context, from the caller's own standing record * @param [options.userKey] {UserKey} the session's user key, when the * caller already holds one -- the registry arm's context when the roster * arm did not deliver a key this invocation * @param [options.repairShaped] {boolean} the caller's explicit * repair-shaped flag: fires the roster completion and registry arms even * when no earlier arm found a tear (the registry-only tear's entry, and * the re-entry after a `reenterRepairShaped` report) * @param [options.collectionIds] {string[]} the encrypted-collection set * the roster arm covers (the completion probe, the mint preconditions, and * the epoch fan-out); defaults to the wallet Space roster's encrypted * collections * @param [options.now] {number} epoch milliseconds, for tests * @param [options.onStage] {StageNotifier} observational: called as each * arm finishes, with `establishment-arm`, `promotion-arm`, * `roster-epochs-arm`, and `registry-arm` -- only for the arms that * actually ran. It is forwarded into the establishment re-run, so that * arm's own stage names arrive first * @returns {Promise} * @throws {TypeError} synchronously, when a required hook is missing * @throws the original `delegatedRead.error`, unchanged, when that trigger's * promotion mend or read retry fails * @throws the establishment-arm probe's `ResourceLogContinuityError`, * unchanged: a served log refused against the chain-head pin must surface * as the continuity refusal it is, never be swallowed into a full * establishment re-run over a rolled-back or substituted log */ export declare function mendCredentialAnchoredAccount(options: { account: CredentialAnchoredAccountCore; standing: { clientDid: string; keyAgreementKeyMultibase: string; recipientKid: string; keyAgreementKey: IKeyAgreementKey; }; bindRecord: CredentialAnchoredBindRecordHook; rosterStoreFor: (options: { did: string; log: DIDLog; }) => EncryptionDescriptorStore; collectionStoreFor: (options: { did: string; log: DIDLog; }) => (collectionId: string) => EncryptionDescriptorStore; bootstrapWasFor: (options: { keyAgent: ICapabilityAgent; }) => WasClient; idStore: WebvhIdStore; lowEntropy: boolean; priorCreatedAt?: string; delegatedClients?: IZcap; provideKmsAuthentication?: (options: { spaceReady: Promise; }) => Promise; promoteKeystore?: (options: { did: string; }) => Promise; beforePromotion?: (context: { was: WasClient; zcapClient: ZcapClient; did: string; userKey: UserKey; establishment: CredentialAnchoredEstablishment; }) => Promise; invocation?: { was: WasClient; zcapClient: ZcapClient; capability: IZcap; }; rosterStore?: EncryptionDescriptorStore; collectionStore?: (collectionId: string) => EncryptionDescriptorStore; delegatedRead?: { error: unknown; retry: () => Promise; }; hasRosterEpochPin: () => Promise; registry?: CredentialAnchoredRegistryContext; userKey?: UserKey; repairShaped?: boolean; collectionIds?: string[]; now?: number; onStage?: StageNotifier; }): Promise; //# sourceMappingURL=mend.d.ts.map