import type { DIDLog } from '@interop/did-method-webvh'; import type { LadderRung } from '../unlock/ladderDerivation.js'; export { clientAnnexRungSeed, ladderRung, ladderRungSeed, ladderVmKeyMultibase, ladderVmSeed } from '../unlock/ladderDerivation.js'; export type { LadderRung } from '../unlock/ladderDerivation.js'; /** * How many rungs {@link attributeLadderRung} derives before concluding the * log commits none of them. Generous: one rung is consumed per * self-enrollment, so a real ladder's standing commitment sits at the number * of self-enrollments the credential has ever performed. */ export declare const LADDER_MAX_SCAN = 128; /** * Thrown when the published log's standing parameters match no derivable rung * -- the credential's inventory was revoked (or never published), the ladder * seed does not belong to this account, or the scan bound was exceeded -- or * when they match more than one rung in the same role, which no legitimate * history produces. Self-enrollment refuses loudly rather than guessing. */ export declare class LadderAttributionError extends Error { constructor(message: string); } /** * Generates a fresh random ladder seed. * * @returns {Uint8Array} */ export declare function generateLadderSeed(): Uint8Array; /** * Derives an annex generation's rung 0 in full: seed and public multibase. * Deliberately index-free -- the annex chain has length one, so there is * no rung to advance to and no attribution scan to run (see * {@link clientAnnexRungSeed}). * * @param options {object} * @param options.ladderSeed {Uint8Array} * @param options.generationId {string} the generation collection's name * @returns {Promise<{ seed: Uint8Array, keyMultibase: string }>} */ export declare function clientAnnexRung({ ladderSeed, generationId }: { ladderSeed: Uint8Array; generationId: string; }): Promise<{ seed: Uint8Array; keyMultibase: string; }>; /** * Where an attributed rung stands in the published log: `'committed'` (the * steady state -- only its hash stands in `nextKeyHashes`) or `'revealed'` (a * torn self-enrollment published the reveal entry but not the add entry, so * the rung sits in `updateKeys` awaiting the resumed add). */ export type LadderRungState = 'committed' | 'revealed'; /** * Recovers the ladder's current rung from the published log's standing * parameters -- the counter recovery that replaces any stored counter. Scans * rungs `0..maxScan - 1`; a rung whose key stands in `updateKeys` is a torn * self-enrollment to resume (`'revealed'`), else a rung whose hash stands in * `nextKeyHashes` is the standing commitment (`'committed'`). Exactly one * revealed rung, or exactly one committed rung beside it, is legitimate -- * the reveal entry keeps the spent rung's hash committed so a resumed run can * re-state it, which is why a revealed rung wins over a committed one. * Anything else fails closed with {@link LadderAttributionError}. * * @param options {object} * @param options.ladderSeed {Uint8Array} * @param options.published {object} the resolved log's standing parameters * @param options.published.updateKeys {string[]} * @param options.published.nextKeyHashes {string[]} * @param [options.maxScan] {number} how many rungs to derive before giving * up; defaults to {@link LADDER_MAX_SCAN} * @returns {Promise<{ rung: LadderRung, state: LadderRungState }>} */ export declare function attributeLadderRung({ ladderSeed, published, maxScan }: { ladderSeed: Uint8Array; published: { updateKeys: string[]; nextKeyHashes: string[]; }; maxScan?: number; }): Promise<{ rung: LadderRung; state: LadderRungState; }>; /** * Everything of one ladder that currently stands in the published log: the * revealed rung keys still authorized in `updateKeys`, the ladder VMs the * final document publishes that this ladder is attributed as the publisher of * (`ladderVmIds`), and the * committed hashes the ladder accounts for in `nextKeyHashes` -- including, * for a torn self-enrollment, the hashes the reveal entry committed under the * rung's authority for a client that was never published (its update- and * staged-key hashes), which are as much a latent re-seizure credential as the * rung's own commitment. */ export interface LadderStandingInventory { revealedKeys: string[]; committedHashes: string[]; ladderVmIds: string[]; } /** * Thrown when an edit's `nextKeyHashes` would come out empty. An empty list * switches prerotation off in did:webvh, so an entry that struck every * commitment would leave the account with no staged key at all. Every ceremony * that strikes hashes commits its own successors in the same entry, so the * list is non-empty by construction; this is the assertion that says so. */ export declare class NextKeyHashesEmptyError extends Error { constructor(message: string); } /** * Refuses to publish an entry whose `nextKeyHashes` came out empty. * * @param options {object} * @param options.nextKeyHashes {string[]} * @param options.ceremony {string} named in the refusal * @returns {string[]} the list, unchanged */ export declare function assertNextKeyHashesRemain({ nextKeyHashes, ceremony }: { nextKeyHashes: string[]; ceremony: string; }): string[]; /** * The anchor a credential's ladder walk starts from when the caller holds no * recorded update key -- the log-only anchoring a cold browser needs. Every * credential-class `keyAgreement` member names its own ladder's rung-0 * commitment (`ladderCommitment`, written by every bind site through * `unlockKeyVerificationMethod`), so the anchor is read off the member * itself rather than inferred from the shape of the entry that introduced * it. The value is `hash(rung 0)` in the form `nextKeyHashes` carries, the * same commitment the bind puts there. * * The value is taken from the entry that INTRODUCED the member, and only * from an account-controlled member (an enrolled client's marked twin is * never a credential). A member is introduced each time it appears after * not standing, so a member re-bound after a retirement (the same passphrase * added again) anchors on the fresh ladder's commitment, and a log read * before the retirement entry that struck it still names it. What a later * entry says about a member that already stood is checked, not adopted: the * property is a document member any update-key holder can restate, and a * value that changes while the member stands continuously is a retargeting * no bind performs. Such a member names no anchor for the rest of that * standing run, so the retargeting is reported as unclaimed rather than * quietly walked from the substituted hash. The write side carries the same * rule: `publishUnlockKey`, the one path in this library that rewrites a * standing member, refuses a bind whose rung-0 hash differs from the value * the member already names, so a retargeting in a served log is a foreign * writer's and never a torn re-run's. The guard covers only a * continuous run: the same update-key holder can strike the member in one * entry and re-add it under a fresh commitment in the next, which reads as a * legitimate re-bind and is not distinguishable from one seedlessly. That * shape rests on the log's loudness and on `survivingClientKeyProtection` * instead. A member without the property, or one nowhere in the log, names * no anchor either. * * The named hash must also be one the log COMMITTED FOR THE MEMBER: newly * added to `nextKeyHashes` by the introducing entry (the merged bind, the * ladder-anchored genesis), by a later entry of the same standing run (the * split bind's authority entry, two versions after its key entry), or by an * earlier entry whose signer the introducing entry retires (the recovery * continuations' reveal-and-commit entry, signed by the spent code's rung, * which the add-and-retire entry introducing the replacement's member and * the fresh credential's strikes -- every reveal entry of a torn and resumed * continuation is that shape). A member naming a hash that stood in * `nextKeyHashes` already, committed by some other entry for something else * -- an enrolled client's staged hash, say, which any update-key holder can * read off the log and restate as a commitment -- names no anchor. Without * the check the walk would claim that hash for the credential, and a * retirement that honored the claim would strike a surviving client's * staged commitment. The check is necessary rather than sufficient: an * approver can name a hash BEFORE the enrollment it approves commits it, and * that is the shape `survivingClientKeyProtection`'s positional exemption * closes. Every refusal returns `undefined`, which the callers report as * unclaimed rather than acting on. * * @param options {object} * @param options.log {DIDLog} a resolved, caller-verified log * @param options.credentialVmId {string} the credential's `keyAgreement` * verification-method id * @returns {{ anchorHash: string } | undefined} */ export declare function credentialLadderAnchor({ log, credentialVmId }: { log: DIDLog; credentialVmId: string; }): { anchorHash: string; } | undefined; /** * What the log says about a credential member's `ladderCommitment`, before * {@link credentialLadderAnchor} decides whether it is an anchor: the value * the entry introducing the member's latest standing run named (`named`), * whether the member stands in the head document (`stands`), whether a later * entry of that run restated the value (`retargeted`), and whether the log * committed the named hash for the member (`committed`). The anchor reader * is the refusing composition of these; the bind's write-side check reads * them apart, since a split bind's authority entry meets a member that names * its hash while the log has not committed it yet. * * @param options {object} * @param options.log {DIDLog} a resolved, caller-verified log * @param options.credentialVmId {string} the credential's `keyAgreement` * verification-method id * @returns {{ named?: string, stands: boolean, retargeted: boolean, * committed: boolean } | undefined} `undefined` for a member the log * never introduced */ export declare function credentialLadderCommitment({ log, credentialVmId }: { log: DIDLog; credentialVmId: string; }): { named?: string; stands: boolean; retargeted: boolean; committed: boolean; } | undefined; /** * The ladder VMs the log introduced ALONGSIDE something of this credential's * -- the question a fail-closed retirement gate asks when its seedless walk * claimed nothing. A standing VM is this credential's candidate in three * shapes: * * - the entry that introduced it also introduced this credential's own * `keyAgreement` member (the merged bind, and the recovery continuations' * add-and-retire entry); * - it newly committed the credential's anchor hash, or newly authorized its * anchor key (the split bind, whose key entry and authority entry are two * versions apart); * - it introduced no credential-class member at ALL. Such an entry installs * authority for a credential bound earlier, and the log does not say * which, so every credential retiring seedlessly must treat it as possibly * its own. A recovery code's issuance authority entry is exactly this * shape, and a code whose registry entry records the wrong anchor reaches * the gate through it. * * An empty result is the positive answer the gate needs: no entry of this log * ever brought a ladder VM in beside anything of this credential's, so a VM * standing now is a sibling's and the retirement has nothing of its own to * leave behind. That is what tells a torn issuance's orphan -- a credential * with a `keyAgreement` member, no ladder VM, and no committed rung -- from a * credential whose VM stands unattributable, which the gate must still * refuse. Every credential bind co-introduces its own member, so a sibling's * VM never reaches the result through the third shape. * * Deliberately entry-shaped rather than an attribution: it answers which VMs * COULD be this credential's, so a walk that refused still fails closed. * * @param options {object} * @param options.log {DIDLog} a resolved, caller-verified log * @param options.credentialVmId {string} the credential's `keyAgreement` * verification-method id * @param [options.anchorKeyMultibase] {string} the credential's recorded * update key (a registry entry's `updateKeyMultibase`) * @param [options.anchorHash] {string} the same anchor as a committed hash * @returns {Promise} the candidate ladder VM ids, in log order */ export declare function ladderVmIdsIntroducedWithCredential({ log, credentialVmId, anchorKeyMultibase, anchorHash }: { log: DIDLog; credentialVmId: string; anchorKeyMultibase?: string; anchorHash?: string; }): Promise; /** * The committed rung hashes of every standing credential the head document * lists, read off the log alone -- the latent commitments a client removal * must exclude before it attributes the removed client's staged hash. Each * credential-class `keyAgreement` member (an unmarked method, verbatim key or * commitment) is anchored from its member's `ladderCommitment` and walked * ({@link attributeLadderInventory}); a credential whose anchor or walk * refuses claims nothing and is named on `unclaimedCredentialVmIds`, so the * caller can tell "no latent hashes stand" from "one credential's could not * be read" rather than treating both as an empty list. * * Read-only and under-claiming by construction: a walk that refuses is * reported, never guessed, and the surviving-client protection is not applied * here (a claimed hash is only ever EXCLUDED from a staged-hash attribution, * so an over-claim can at worst leave the removed client's staged hash to the * positional rule, while an under-claim leaves it to the same rule). * * @param options {object} * @param options.log {DIDLog} a resolved, caller-verified log * @param [options.maxScan] {number} the ladder walk's bound * @returns {Promise<{ hashes: string[], unclaimedCredentialVmIds: string[] }>} * the claimed committed hashes, deduplicated, and the credentials whose * walk refused */ export declare function standingCredentialLatentHashes({ log, maxScan }: { log: DIDLog; maxScan?: number; }): Promise<{ hashes: string[]; unclaimedCredentialVmIds: string[]; }>; /** * The strike a retirement entry ALREADY published, recomputed by re-running * {@link attributeRetiredCredentialRungs} over the log as it stood just before * that entry. A resumed ceremony reports what its first run reported this way, * rather than through a second definition of "unclaimed" that could answer * differently. * * The entry is located by the key it authorized: every ceremony that calls * this detects its own completion by that key standing in `updateKeys`, and * the entry that FIRST authorized it is the one to walk back to. A log that * does not authorize the key, or authorizes it at the genesis entry, has no * usable prefix and is refused: a caller that reached this had already seen * the key authorized, so either shape is a caller defect rather than a * state to answer for. * * @param options {object} * @param options.log {DIDLog} the post-entry log * @param options.authorizedKeyMultibase {string} the update key the entry * authorized * @param options.credentialVmIds {string[]} the credentials the entry * retired, as the caller derived them from the log * @param [options.protectedHashes] {string[]} the same set the first run * passed * @param [options.protectedKeys] {string[]} likewise * @param [options.maxScan] {number} * @returns {Promise<{ struckHashes: string[], struckKeys: string[], * unclaimedCredentialVmIds: string[] }>} */ export declare function retiredCredentialRungsBeforeKey({ log, authorizedKeyMultibase, credentialVmIds, protectedHashes, protectedKeys, maxScan }: { log: DIDLog; authorizedKeyMultibase: string; credentialVmIds: string[]; protectedHashes?: string[]; protectedKeys?: string[]; maxScan?: number; }): Promise<{ struckHashes: string[]; struckKeys: string[]; unclaimedCredentialVmIds: string[]; }>; /** * What a full retirement must strike from the standing parameters for a set of * credentials being retired in one entry: their committed rung hashes, and any * rung of theirs standing revealed in `updateKeys`. Each credential is * anchored from the log alone ({@link credentialLadderAnchor}), so a cold * browser holding no registry and no seed can still strike them. * * The bias is under-striking, deliberately. Over-striking is silent and * unhealable -- a surviving credential or client keeps its verification * methods and its roster wrap, and only fails when someone finally uses it -- * while under-striking leaves a committed rung a retired credential's holder * could reveal, which the report names. Five things keep it that way: * * - a credential whose anchor is ambiguous or whose walk refuses is reported * as unclaimed and nothing of its is struck; * - only what the walk positively claims is a candidate; * - a hash or key the caller names as its own (`protectedHashes` / * `protectedKeys`, the successors the entry itself commits) is dropped; * - every SURVIVING enrolled client's active update key, its carry-over hash * and its staged hash are dropped, whatever the walk claimed * ({@link survivingClientKeyProtection}). That guard is structural rather * than a property of the walk, because a mis-anchored walk landing on a * client's key would otherwise end that client's ability to extend the * account log for good. The walks therefore run FIRST, and the hashes they * claimed are passed to the protection as walk-derived, so a retiring * credential's own rung cannot make a client's staged attribution ambiguous * and get itself protected as a candidate. The protection never lets such * a claim prune the hash the decision-0007 position names as a client's * staged hash, so a walk anchored on a member that names that hash claims * it and is withheld, rather than striking it; * - a listed enrolled client whose ACTIVE update key the log cannot attribute * withholds the WHOLE strike: nothing is struck and every credential is * reported, since the structural guard cannot say what that client holds. * * The report is a not-fully-retired report rather than a nothing-happened one. * A credential appears on `unclaimedCredentialVmIds` when its walk refused, * when it claimed nothing, AND when any single hash or key it claimed was * withheld by one of the kept sets. The rest of that credential's claims are * still struck; what the caller must not be told is that a partial retirement * was a whole one. * * @param options {object} * @param options.log {DIDLog} a resolved, caller-verified log, read BEFORE * the entry is built * @param options.credentialVmIds {string[]} the retiring credentials' * `keyAgreement` verification-method ids * @param [options.protectedHashes] {string[]} hashes the entry itself * commits, never struck * @param [options.protectedKeys] {string[]} update keys the entry itself * authorizes, never struck * @param [options.maxScan] {number} the ladder walk's bound * @returns {Promise<{ struckHashes: string[], struckKeys: string[], * unclaimedCredentialVmIds: string[] }>} */ export declare function attributeRetiredCredentialRungs({ log, credentialVmIds, protectedHashes, protectedKeys, maxScan }: { log: DIDLog; credentialVmIds: string[]; protectedHashes?: string[]; protectedKeys?: string[]; maxScan?: number; }): Promise<{ struckHashes: string[]; struckKeys: string[]; unclaimedCredentialVmIds: string[]; }>; /** * Attributes a ladder's FULL standing inventory from the log -- the retirement * counterpart of {@link attributeLadderRung}, which recovers only the single * current rung. Retiring a credential must strike every standing artifact its * ladder accounts for, so this walks the log's effective parameters forward * from an anchor (the recorded bind-time rung, however stale) and tracks the * ladder's inventory entry by entry: * * - a newly authorized key whose hash was a known ladder commitment is a rung * REVEAL; the hashes that entry newly commits are claimed by the ladder, * as are those of any later entry the ladder itself signed, since the * rung's authority stood behind them. Hashes an entry signed by some other * key commits stay OUT, even while the rung sits in `updateKeys`: a rung * stands revealed indefinitely after a forget (and after a torn * self-enrollment), and the account's enrolled clients go on extending the * log the whole time. One exception, the HANDOVER: when the revealed rung's * hash was committed earlier by a key the revealing entry itself retires * (the recovery continuation -- the spent code commits the fresh * credential's `hash(rung 0)` and `hash(rung 1)` adjacently with the * replacement code's hash last, then the add entry revealing rung 0 strikes * the code), the hash committed immediately after the rung's in that entry, * when it is not the entry's last addition, is the ladder's next commitment * and is claimed too, so a seed-less walk over a continuation log sees * rung 1; * - the entry that retires the revealed rung while authorizing a key whose * hash sits among those claims is the enrollment's COMPLETION: the new * client's update-key hash and the claim committed immediately after it * (its staged hash -- a reveal-and-commit entry appends the credential's * own next commitment LAST among its newly committed hashes, the ordering * convention in `decisions/0007-ladder-reveal-hash-order.md`) transfer to * the client and stop being ladder-owned. What the completion did not * transfer stays ladder-owned only where the ladder can say so POSITIVELY: * the hash derives from the ladder's own seed (or is the recorded key's), * or the credential itself survives the completing entry, which is what * makes the residue its next standing commitment. A SPEND -- the recovery * continuation, whose one entry retires the code's own inventory and * publishes its successor's -- leaves the replacement credential's * commitment in that position instead, and striking that would leave the * replacement unusable and unhealable; * - a claim or revealed key that later leaves the parameters without a * completion was struck by some other edit and simply stops standing; * - a ladder VM standing in the final document belongs to this ladder on * either of two arms, asked at the entry that PUBLISHED it (the entry at * which the id appeared among the document's ladder VMs). The SIGNER arm: * that entry was signed by a key this ladder accounted for at that point, * which covers every install a ladder rung signs (the ladder-anchored * genesis, the ladder-VM install, the transient recovery's add-and-retire * entry). The CO-INTRODUCTION arm: that entry also introduced this * credential's own `keyAgreement` member (`credentialVmId`), which is what * reaches a bind entry an ENROLLED CLIENT signed -- the shape * `publishUnlockKey` writes, whose signer is the binding client's update * key rather than a rung. Three guards keep that arm from over-claiming: * it needs `credentialVmId` in hand, the entry must introduce exactly ONE * credential-class `keyAgreement` member (the account-controlled class, * `credentialKeyAgreementMethods`; the transient recovery entry introduces * two and is left to the signer arm), and the entry must introduce exactly * ONE ladder VM. The question is anchored rather than free-standing: it * answers "is this VM mine", and a VM no anchored ladder claims is * identified by subtraction and left standing, since striking a key this * ladder cannot show it owns would take out a surviving credential's. * The COMMITMENT arm: that entry committed a hash the ladder knows a priori * (the anchor's, a seed-derived rung's, or one the backward pre-pass * recovered), introduced exactly one ladder VM, and introduced no OTHER * credential's `keyAgreement` member. It reaches the reinstall an * `establishStandingUnlock` re-run writes, which mints a fresh ladder seed * for a credential whose member already stands, so neither of the other * arms can see it. Like the co-introduction arm it needs `credentialVmId` * in hand, since with no id the foreign-member guard would pass vacuously. * * One narrowing runs across both the signer arm and the hash claims. A ladder * derives exactly ONE verification method from its seed, so a ladder already * holding a standing claimed VM cannot own a second one an entry introduces. * An entry introducing such a VM, or another credential's `keyAgreement` * member, is installing a foreign inventory: the acting credential's rung * signs exactly that on a ladder-branch bind and on a recovery code's split * issuance. The signer arm does not claim the VM there, and the hashes such * an entry commits are the bound credential's rung commitments rather than * this ladder's next one, so they stay out of the claims. Without the * narrowing, retiring the acting credential would strike the credential (or * the recovery code) it had just bound. * * The attribution is anchor-invariant across the shapes where each rung's * hash was committed by an entry that also revealed the previous rung, or by * a handover. The signer arm's key set is the anchor plus every earlier rung * a backward pre-pass recovers from the log's own positional rules ({@link * recoverEarlierRungs}, over `decisions/0007-ladder-reveal-hash-order.md`), * so an entry a spent rung signed is attributed however far the anchor has * since climbed. The ladder seed (`ladderSeed`) remains a shortcut and a * cross-check rather than a requirement: it makes every rung's key and hash * known outright and skips the pre-pass. A residue no arm can attribute is * still released -- the retirement then strikes what the recorded inventory * names and nothing more. More than one ladder reveal standing or arriving at * once matches no legitimate history and fails closed ({@link * LadderAttributionError}). * * One shape is out of the backward walk's reach. The last-client transition * strikes the ladder VM and reinstalls it in the same run * (`forgetLastEnrolledClient` stage 1): same seed, the credential's member * standing, the acting rung's hash still committed, and no hash added. A * later self-enrollment then spends that already-revealed rung, so its * reveal-and-commit entry authorizes no key while committing the next rung's * hash, and the registry anchor advances to that next rung. The backward walk * climbs from the anchor by asking which key the entry that committed its * hash authorized; that entry authorized none, so the walk cannot name the * rung that signed it, and a walk anchored on the registry key alone leaves * the earlier rung and the reinstalled VM unrecovered. A walk anchored on the * member's own rung-0 commitment reads that history forward with no climb, * which is why the removal paths walk from both anchors * (`attributeUnlockLadderInventory`) and act on the member's reading. * * The anchor comes in three forms, and the walk is the same afterwards. A * recorded update-key multibase (`anchorKeyMultibase`) is what a caller * holding a registry entry passes. A hash (`anchorHash`) is the same anchor * with the key withheld: the rung is picked up when the log reveals it, since * the reveal test already matches on the commitment. With neither, and a * `credentialVmId` in hand, the anchor is read off the credential's own * `keyAgreement` member ({@link credentialLadderAnchor}) -- the cold-browser * mode, where no registry is readable before the entry is written. A member * naming no ladder commitment refuses with {@link LadderAttributionError} * rather than walking from a guess. * * @param options {object} * @param options.log {DIDLog} a resolved, caller-verified log * @param [options.anchorKeyMultibase] {string} the credential's recorded * update-key multibase (bind-time rung 0, or a refreshed later rung) * @param [options.anchorHash] {string} the same anchor as a committed hash, * for a caller that resolved one without the key * @param [options.ladderSeed] {Uint8Array} the credential's ladder seed, * when the caller holds it * @param [options.credentialVmId] {string} the credential's own * `keyAgreement` verification-method id, which tells a climb (the * credential stands afterwards) from a spend (its inventory goes in the same * entry), which the ladder VM's co-introduction arm is anchored on, and * which supplies the anchor itself when neither anchor form is passed * @param [options.maxScan] {number} seeded pre-derivation bound; defaults to * {@link LADDER_MAX_SCAN} * @returns {Promise} what currently stands; every * array empty when the log carries nothing of the ladder any more */ export declare function attributeLadderInventory({ log, anchorKeyMultibase, anchorHash: suppliedAnchorHash, ladderSeed, credentialVmId, maxScan }: { log: DIDLog; anchorKeyMultibase?: string; anchorHash?: string; ladderSeed?: Uint8Array; credentialVmId?: string; maxScan?: number; }): Promise; //# sourceMappingURL=ladder.d.ts.map