/*! * Copyright (c) 2026 Interop Alliance. All rights reserved. */ /** * The per-collection half of the user key rotation cascade: once the roster's * current epoch has moved to a fresh user key (a client revoked, a recovery * code spent or revoked), every encrypted collection must take a new epoch * naming the fresh user key and retire the old one -- otherwise writes keep * landing under an epoch key the revoked party still holds. * * The staleness rule is the one the revocation spike verified: **a collection * is stale exactly when its current epoch names a user key KAK other than the * roster's current** -- detectable from durable data alone, no checkpoint * resource anywhere, which is what makes a crashed cascade resumable by a naive * full re-run (and what the login-time completion sweep re-checks). Which KAKs * are "user key generations" is read from the roster itself: its epochs ARE the * user key generations, each escrow-wrapped to every enrolled client, so any * enrolled client can recover any generation's key ({@link * unwrapUserKeyGenerations}) -- needed both to recognize a stale epoch and to * escrow the fresh user key into a stranded collection's history. * * The cascade is rotation-only: every encrypted collection's descriptor * carries an epoch roster from provisioning (`ensureWalletSpaceEpochs`), so a * descriptor met without epochs can only mean a tampering or pre-provisioning * host and is refused fail-closed. A descriptor whose `currentEpoch` names no * epoch in its own list is refused the same way, through the same helper * `readUserKeyRoster` refuses on the roster itself ({@link currentEpochOf}, * `UserKeyRosterIntegrityError`): collection descriptors arrive host-served * with no server-side epoch invariants, so a mismatched pair is a * configuration no enrolled client authenticated, never something to * evaluate against the last epoch. No construction anywhere installs a * user-key secret as a collection epoch secret, which is what keeps a * collection-epoch escrow (an App Connect grant, a share) from ever handing a * grantee the user key itself. */ 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 { WebvhResourceLogController } from '../resourceLog/index.js'; import { type UserKey } from './userKey.js'; /** * What one collection's cascade step did: `noop` (already on the current user * key), `sealed` (already on the current user key, but the collection's * governing log still anchored before the membership change -- the backstop * append landed), `escrowed` (the current user key's wrap was completed into * history with no stale epoch to rotate), or `rotated` (a fresh epoch sealed * to the current user key, the stale generations retired). */ export type CollectionUserKeyRotationOutcome = 'noop' | 'sealed' | 'escrowed' | 'rotated'; /** * Brings ONE encrypted collection's epoch roster onto the current user key -- * the per-collection op of the revocation cascade and of the completion sweep: * * - **Stale current epoch** (names a non-current generation): one * `replaceRecipient` write -- the current user key escrowed into every epoch, * a fresh epoch minted without the stale generations. Two requests per * collection; app recipients and other readers ride through untouched (the * default did:key resolver re-wraps them). * - **Current already** and fully escrowed: no epoch write at all, so a naive * re-run after a mid-cascade crash converges with zero redundant epochs. On * a log-governed (sealable) store this is exactly where an unsealed log can * hide -- the rotation that should have re-anchored the log never wrote -- * so the store's seal backstop runs here (`sealed` when it appended); * rotated and escrowed writes seal by construction, anchored at the * caller's current controller head. * - **No epochs**, or a `currentEpoch` naming no epoch in the descriptor's * own list: refused fail-closed (see the module doc) -- provisioning * installs every encrypted collection's epoch[0], so the cascade never * mints a first epoch, and never guesses which epoch an inconsistent * descriptor meant. * * The pull axis is deliberately a no-op here: a user key rotation follows a * document edit (client revocation, code retirement) that already killed the * revoked party's server-side access everywhere under the current-key-set * rule -- there is no per-collection revoke. * * @param options {object} * @param options.store {EncryptionDescriptorStore} the collection's * descriptor store * @param options.userKey {UserKey} the roster's CURRENT user key * @param options.generations {UserKey[]} every roster generation this client * could unwrap ({@link unwrapUserKeyGenerations}), oldest first * @returns {Promise} * @throws {UserKeyRosterIntegrityError} the descriptor's `currentEpoch` * names no epoch in its own list */ export declare function rotateCollectionEpochsToUserKey({ store, userKey, generations }: { store: EncryptionDescriptorStore; userKey: UserKey; generations: UserKey[]; }): Promise; /** * What the collection fan-out did, per collection id: the outcomes for every * collection that needed (or took) work, and the per-collection failures the * caller surfaces -- the cascade never aborts on one stuck collection. */ export interface UserKeyCascadeResult { outcomes: Record; failed: Array<{ collectionId: string; error: unknown; }>; } /** * The collection fan-out of the user key rotation cascade: re-epochs every * named collection onto the roster's current user key, in parallel, unwrapping * the user key generations from the roster once. The wallet supplies what only * it knows -- which collections exist (`collectionIds`) and how to reach each * one's descriptor (`storeFor`); the per-collection staleness rule and re-epoch * live in {@link rotateCollectionEpochsToUserKey}. * * A collection that fails is reported in `failed` and the rest proceed; the * caller decides what a failure means (the login-time completion sweep is the * standing backstop, and the staleness rule makes a naive full re-run * converge with zero redundant epochs). * * **A verified-log refusal is a failure entry, carried verbatim.** A * collection whose governing log is fabricated or forked * ({@link isResourceLogRefusal}: a `ResourceLogIntegrityError`, or a * `ResourceLogContinuityError` whose reason is not `rollback`) lands in * `failed` like any other collection, with the refusal as its `error` * unwrapped, so a caller reading the report can tell it by `err.name`. The * fan-out never throws for it: every caller here is a ceremony past its pivot * (a client revoked, a credential retired, a client forgotten), whose later * stages -- the generation delegation re-mint, the session's adoption of the * fresh key -- must still run, and whose contract is that a fan-out failure * is a resumable success. A retry against the same served log cannot help * that one collection, and the report says so; it must not cost the account * the stages that follow. * * @param options {object} * @param options.collectionIds {string[]} the encrypted collections to * cover, deduplicated by the caller * @param options.storeFor {Function} `(collectionId) => * EncryptionDescriptorStore` -- the collection's descriptor store * @param [options.isEncrypted] {Function} `(collectionId) => * Promise` -- optional pre-filter, checked inside each collection's * own task so a throwing check lands in that collection's `failed` entry * (e.g. a standard collection an account never provisioned server-side) * @param options.rosterDescriptor {CollectionEncryption} the freshly read * `key-map/user-key.jsonl` roster (the source of the user key generations) * @param options.clientKeyAgreementKey {IKeyAgreementKey} this client's own * (identity) key-agreement key, unwrapping the generations * @param options.userKey {UserKey} the roster's current user key * @param [options.controller] {WebvhResourceLogController} the ceremony's * post-edit controller view, set as each log-governed collection store's * minimum controller version before that collection's first append. A * collection's governing log takes the same post-edit anchoring the roster * log does: a store resolving a stale cached view would anchor its rotation * before the document edit, so a ladder-signed append would be refused and * a no-op rotation's seal would be blind to the membership change. Absent * (a caller with no post-edit log in hand -- the login sweep), the injected * resolver's own freshness is what the appends anchor at * @returns {Promise} */ export declare function cascadeCollectionsToUserKey({ collectionIds, storeFor, isEncrypted, rosterDescriptor, clientKeyAgreementKey, userKey, controller }: { collectionIds: string[]; storeFor: (collectionId: string) => EncryptionDescriptorStore; isEncrypted?: (collectionId: string) => Promise; rosterDescriptor: CollectionEncryption; clientKeyAgreementKey: IKeyAgreementKey; userKey: UserKey; controller?: WebvhResourceLogController; }): Promise; //# sourceMappingURL=userKeyCascade.d.ts.map