/*! * Copyright (c) 2026 Interop Alliance. All rights reserved. */ /** * The client-revocation cascade: disconnecting an enrolled wallet client from * an account, run synchronously in the revoking client, in dependency order. * One orchestrator for every wallet, with the two app-specific stages injected * rather than forked. * * 1. **The document edit** (`revokeWebvhClient`): the revoked client's two * verification methods, its update key, and both its standing prerotation * commitments leave the account's did:webvh document in one log entry. * Under the current-key-set rule that single edit is the revoked client's * pull axis EVERYWHERE -- its invocations, and every delegation and app * grant it ever signed, stop verifying the moment its verification method * leaves the document. There is no per-collection revoke anywhere in the * cascade; apps a revoked client had connected reconnect through the * ordinary connect flow. * 2. **The user key rotation** in the wrap-set roster, recipients resolved * from the document the edit itself just resolved to (no re-fetch of the * log this call just extended). On a log-governed roster store the * orchestrator itself guarantees post-edit anchoring: the controller view * built from the edit's own post-edit log is set as the store's minimum * controller version (`setMinimumControllerVersion`) before anything * roster-side runs, so the rotation and the seal backstop anchor at or * past the edit even where the app's injected controller resolution still * serves a cached pre-edit view. The roster delivers, never sources, so the * stage names no client at all: it converges the roster onto that document * (the login sweep's own path), retiring every current-epoch recipient the * document no longer keys in one rotation. Nothing here has to pair a * client with its key-agreement key, so a client that published several * of them, or none the caller knows, is retired just the same. An * account with no roster yet stops here: the client IS disconnected, with * nothing to rotate. On a sealable (log-governed) roster store, the seal * backstop follows: a rotation that no-op'd appended nothing, so the * roster log may still be anchored before the document edit -- `seal()` * re-anchors it with an idempotent no-op entry, best-effort and reported * rather than thrown. * 3. **The collection fan-out**: every encrypted collection is re-epoch'd onto * the fresh key in parallel, so writes stop landing under epochs the * revoked client can still decrypt. Failures are collected per collection * and never abort the fan-out. * 4. **The generation-delegation re-mint** (optional, a client-annex log * entry replacing the service entry): revoking the * enrolled client that minted the current annex generation's delegation * kills it under the same current-key-set rule, and without this stage * the death is silent mid-generation. The injected closure runs the * signer-death axis of `ensureGenerationDelegationCurrent` against the * post-edit document; it runs even when the account has no roster (the * document edit alone is what rots the delegation). What the stage does * not cover -- App Connect grants a transient session minted under the * old delegation -- stays dead: mid-generation grant death is a stated * consequence of ordinary disconnects, healed by the app reconnecting. * * No unlock record is written anywhere in the cascade. Every record's frame * proof is signed by its own credential's unlock identity key, and its bridge * and `delegatedClients` sibling delegations by that credential's own ladder * VM (`decisions/0019`), so a client's removal rots no record and the * cascade owes no re-seal. * * The revoking session then adopts the fresh key in place (`onRotationAdopted` * -- profile vault keys, storage ciphers, engine restarts: whatever "in place" * means for the app), so it keeps operating without a re-login. * * Convergence is the design: every stage detects its own completion from * durable state alone -- the log entry is idempotent, the roster no-ops once * the revoked entry is off the current epoch, and a collection is stale * exactly when its current epoch names a non-current key generation -- so a * mid-cascade crash strands nothing permanently and a naive full re-run * finishes it (the login-time completion sweep is the standing backstop). The * honest ceiling is unchanged: ciphertext the revoked client already fetched * stays readable to it, and old epochs open to keys it already held. */ import type { IKeyAgreementKey } from '@interop/data-integrity-core'; import type { CollectionEncryption } from '@interop/was-client'; import type { EncryptionDescriptorStore } from '@interop/was-client/edv/core'; import { type AccountLogSigner, type PublishedKeyDocument, type RevokedClientKeys, type WebvhIdStore } from '../webvh/index.js'; import { type CascadeCollections, type RosterSealReport, type UserKey, type UserKeyCascadeResult } from '../keys/index.js'; export type { CascadeCollections, RosterSealReport }; /** * What the generation-delegation re-mint stage reports: whether an annex * entry replaced the service entry's delegation on this run, and -- when the * stage could not run -- why it was skipped (`no-pointer`: the account has no * annex inventory; `no-ladder-seed`: the session holds no ladder seed to * sign the annex entry with; `failed`: the closure's own best-effort * catch, reported rather than thrown so the cascade's remedy stages never * abort on it). */ export interface GenerationDelegationRemint { renewed: boolean; skipped?: 'no-pointer' | 'no-ladder-seed' | 'failed'; } /** * What a completed cascade reports: whether the roster actually rotated on * this run (a re-run of an already-complete revocation reports `false`), the * roster's seal-backstop report (present when the roster store is sealable * and the roster stage ran), the per-collection fan-out result, the document * as the edit left it. */ export interface ClientRevocationResult { rotated: boolean; rosterSeal?: RosterSealReport; collections: UserKeyCascadeResult; document: object; userKey?: UserKey; rosterDescriptor?: CollectionEncryption; generation?: GenerationDelegationRemint; } /** * Runs the whole revocation cascade for one enrolled wallet client. See the * module doc for the order and the convergence story. Throws before touching * anything on a call that must not proceed: a client whose active update key * was not attributed is refused here, and a client revoking itself is refused * by the document edit's own client-arm check, from the seeds it holds, before * its entry publishes -- the one wording every wallet sees, with the `self` * eligibility refusal in `policy.ts` naming the rule for the surface. Once * the document edit lands, a thrown later stage leaves durable state a naive * re-run -- or the login-time sweep -- converges from. * * The document edit's `StagedCommitmentAmbiguousError` passes through * unwrapped, so a surface can re-word it ("disconnect this wallet from the * other one instead"). Match it on `err.name === * 'StagedCommitmentAmbiguousError'` -- a stable contract on that class -- * rather than on `instanceof`, which does not survive a linked or duplicated * copy of this package. * * @param options {object} * @param options.idStore {WebvhIdStore} the account's `id` collection store * @param options.signer {AccountLogSigner} who signs the document edit: * THIS client's did:webvh update-key seeds, or the acting credential's * ladder seed * @param options.revokedClient {RevokedClientKeys} the revoked client's * public halves (its two verification-method multibases and its active * update key) * @param [options.projectionStore] {object} an `id`-collection store the * caller may write through, passed straight to the document edit: the * post-removal `did:web` projection is PUT through it immediately before * that entry publishes, so a ladder-signed disconnect does not leave * `did.json` naming the revoked client. Best-effort, and omitted the * behavior is unchanged (see `revokeWebvhClient`) * @param [options.knownLatentHashes] {string[]} the standing recovery codes' * update-key hashes, so the document edit can tell the revoked client's * staged commitment apart from a latent recovery commitment (the one * ambiguous log shape). Best-effort on the caller's side: an unreadable * registry simply omits them * @param [options.expectedDid] {string} the account DID from the caller's * stored account pointer; supplied, the document edit refuses a `did.jsonl` * resolving to any other account * @param options.rosterStore {EncryptionDescriptorStore} the * `key-map/user-key.jsonl` roster store * @param [options.userKey] {UserKey} this client's cached user key * @param options.clientKeyAgreementKey {IKeyAgreementKey} this client's own * (identity) key-agreement key -- its roster entry * @param [options.pinnedEpochId] {string} the locally pinned latest-seen * roster epoch * @param [options.onUserKeyAdopted] {Function} persists a rotated key: called * with `{ userKey, latestEpochId, descriptor }` after the roster read and BEFORE * the fan-out. The key and the epoch pin must persist atomically * @param options.collections {CascadeCollections} the fan-out's work * @param [options.remintGenerationDelegation] {Function} `({ document }) => * Promise` -- the generation-delegation * re-mint stage, run against the post-edit document (stage 4 in the module * doc); expected to catch its own failures and report them * @param [options.onRotationAdopted] {Function} `({ userKey }) => Promise` * -- the live-session adoption of a rotated key, run last so the session * keeps operating without a re-login * @returns {Promise} */ export declare function revokeAccountClient({ idStore, signer, projectionStore, revokedClient, knownLatentHashes, expectedDid, rosterStore, userKey, clientKeyAgreementKey, pinnedEpochId, onUserKeyAdopted, collections, remintGenerationDelegation, onRotationAdopted }: { idStore: WebvhIdStore; signer: AccountLogSigner; projectionStore?: Pick; revokedClient: RevokedClientKeys; knownLatentHashes?: string[]; expectedDid?: string; rosterStore: EncryptionDescriptorStore; userKey?: UserKey; clientKeyAgreementKey: IKeyAgreementKey; pinnedEpochId?: string | null; onUserKeyAdopted?: (adopted: { userKey: UserKey; latestEpochId: string; descriptor: CollectionEncryption; }) => Promise; collections: CascadeCollections; remintGenerationDelegation?: (options: { document: PublishedKeyDocument; }) => Promise; onRotationAdopted?: (rotation: { userKey: UserKey; }) => Promise; }): Promise; //# sourceMappingURL=revocation.d.ts.map