v0.67.0
Removed
isSyncConflictError,isSyncNotFoundError,isUnknownEpochErrorand the
SyncStatustype from./sync. They ship from@interop/was-client/sync,
beside the classes whosenamethey match, and are imported from there.
Nothing is re-exported here: one owner per name.isKeyUnwrapErrorstays in
./descriptors, since it classifies a roster-membership failure the wallet
layer owns and no sync driver dispatches on it.
Changed
- The
@interop/was-clientdependency now requires^0.49.0, for the
predicates andSyncStatus. deriveUnlockIdentityandunlockIdentityFromSeedstate their return type
explicitly, andUnlockIdentityis an interface rather than an alias over the
inferred return. Same shape; the emitted declarations now name this package's
own copy of the agent types.decisions/0009amended (revisit criterion 1 fired) anddecisions/0021
added: the engine and the RxDB driver stay two algorithms.
Removed
- The three stages that re-sealed a SIBLING credential's unlock record. Every
record's frame proof is signed by its own credential's unlock identity key,
and its bridge anddelegatedClientssibling delegations by that credential's
own ladder VM (decisions/0019), so no ceremony owes a re-seal; each stage
also planted the acting session's key as the sibling record's frame signer,
which a later strike of that key turns into a lock-out no self-heal reaches.
Gone with them:remintRecoveryDelegations,RecordRemintOutcomeand
RecoveryDelegationEntry(/recovery) -- the shared re-mint core.
delegateLogWrite,recordedDelegationFields,zcapExpiring,
ZCAP_RENEWAL_WINDOW_MSandRECOVERY_DELEGATION_TTL_MSstay: a
credential's own login still refreshes its own bridge on those axes.revokeAccountClient'sremintRecoveryDelegationsoption and the
recoverymember ofClientRevocationResult(/clients). The
remintGenerationDelegationoption is unaffected and is now stage 4.retireUnlockCredential'sremintDependentRecordsoption and the
dependentRecordsmember ofUnlockCredentialRetirementResult(/unlock).
The ceremony now starts at the document edit, which is its only account-log
read; the read-only retirement gate (preflightUnlockCredentialRetirement)
is unchanged.forgetLastEnrolledClient'sunlockMethodsoption, the
UnlockMethodsRemintReachtype,RecordRemintFailedError, and the
unlockMethodsmember ofLastEnrolledClientForgetResult(/clientAnnex).
TheonBeforeRemovalseam, which re-binds the LOGIN credential's own
record, is unchanged and is now stage 5 of 6.
removeUnlockKey'sexpectedLadderVmIdsoption and
LadderInventoryDriftError(/unlock): the cross-check tied the edit's
ladder attribution to a pre-edit read only the removed re-mint stage made.revokeAccountClient'sownSigningKeyMultibaseoption (/clients). Its only
effect was the wording of the self-revocation refusal, which the document edit
enforces from the signer's own seeds regardless; an app that omitted it saw
different prose for the same refusal. The edit's refusal ("a client cannot
revoke itself") is now the one wording, and theselfeligibility refusal in
clients/policy.tsremains the surface's rule.
Added
-
recoverySpendRetirementFromLogandRecoverySpendRetirement(/recovery):
the retirement report of a recovery spend whose add-and-retire entry already
stands, read back off the log from the successor's public halves --
retiredCredentialVmIds,struckRungHashes,unclaimedCredentialVmIds, the
same members both continuations return. The continuations' completed branch
now calls it, so a resume that never re-enters the continuation (freewallet's
remembered spend resume) consumes one definition of what the spend retired
rather than a document-membership test of its own. A log that does not
authorize the successor key reports nothing retired. -
walletSpaceProvisionerandWalletSpaceProvisioningError(/keys): builds
the sync engine'sensureProvisionedclosure for a wallet Space --
provisionWalletSpacethenensureWalletSpaceEpochs, single-flight across
concurrent callers, throwing when a collection was left without its first key
epoch so the engine never memoizes a torn run. An optionalonSettledhook
receives every run's epoch report, a partial one included, ahead of that
refusal, so an eager minter still gets the descriptors that did settle. -
collectionDescriptorLogPinId(/descriptors): the pin-slot key for a
collection's governing descriptor log,
space/<spaceId>/key-map/<collectionId>.jsonl. -
isAppConnectQuery/isWalletOnboardingQuery(/request), beside
isZcapQuery: the one place each query type'stypestring is matched. -
advanceLogPin(/webvh),concludeUnchangedAccountEntry(/webvh),
isSoleEnrolledClientandladderVmSigners(/clientAnnex): the shared
forms of four blocks the ceremonies used to restate per site. -
RecoveryCredentialStandingError(/recovery): the recovery continuations
refuse, before their reveal entry, a fresh credential whosekeyAgreementid
already stands in the document (the same passphrase re-typed). The entry
retires every pre-recovery credential, and one it re-bound instead would keep
its old rung commitments under a struck ladder VM. The error carries the
standing ids; nothing is published. -
AccountEntrySigner(/webvh):signAccountEntrysigns under a third,
seam-only arm,{ kind: 'committed', updateSeed }-- a bare update key the
published log commits or already authorizes, revealing itself with the ladder
arm's unions.AccountLogSigner, what a ceremony body accepts, is unchanged. -
introducedCredentialKeys/retiredCredentialKeys
(resourceLog/document.ts, surfaced through/webvh): the credential-class
keyAgreementids one entry introduces and retires, the one definition the
anchor rule and the resumed spend's report share. -
mergeVerificationMethodsandRelationMembership(/webvh): the one merge
of added verification methods into a published document across the five
relations -- same-id replace intoverificationMethod, per-relation dedupe,
an optional retirement predicate over the existing document only. The
enrollment add entry, the self-enrollment add entry, and the recovery
add-and-retire entry build through it instead of three inline copies.
Fixed
- Eight catch sites that classify a rejection on its
nameread it through an
optional chain (clientAnnex/heal.ts,clientAnnex/rosterDeliveredEpochs.ts,
clientAnnex/forgetLast.ts,clientAnnex/log.ts,keys/spaceEpochs.ts,
webvh/revokeClient.ts). A seam rejecting with a nullish reason -- an
app-injected store's barePromise.reject()-- now propagates as it is
instead of raising aTypeErrorfrom inside the catch. Every site's other
branch rethrows, so no nullish rejection is adopted into a re-run or a skip. isDIDAuthOnlyRequest(/request) reads the query set throughqueriesOf,
as classification does, so a body carrying anullor untyped query entry
beside itsDIDAuthenticationquery is DID-Auth-only to both predicates
rather than to one.- The credential-anchored establishment's stage-3 pointer entry no longer fails
on a lost race that retired its signing rung. The entry used to retry with a
pair fixed before the retry, so a sibling self-enrollment spending rung 0
between the read and the PUT ended the run on an untyped not-authorized error
after the annex Space and generation were minted (the stage-3 orphan residue).
The entry is now moved as the ladder: every attempt attributes the ladder's
current rung, reveals it when only its hash stands committed, and signs with
it, landing the pointer on the winner's head with no second mint. The registry
records the rung the entry was signed with, or, when the document already
pointed, the ladder's currently attributed rung. The stage-3 primitive
attributes the rung before anything is minted, so a ladder the account log no
longer anchors refuses with no annex Space or generation minted. - A second recovery now retires the credentials the first recovery introduced.
The recovery add-and-retire entry is read as a bind shape by the anchor rule
(credentialLadderAnchor,/clientAnnex): the fresh credential anchors on
the successor key the entry authorized, and the replacement code on the reveal
entry's last committed hash, paired by thekeyAgreementrelation's order
(decisions/0014, amended). A later recovery therefore strikes the first
recovery's passphrase rung (standing authorized inupdateKeysuntil now) and
its replacement code's commitment, and reports neither on
unclaimedCredentialVmIds; a client removal's derived latent-hash set claims
a recovery replacement's commitment without the caller vouching for it. The
two anchors are independent: a replacement lookup that refuses leaves the
fresh credential anchored. A transient continuation torn at its seam more than
once (a fresh ladder seed each time, the same replacement) still anchors both,
the replacement's hash read past every resumed reveal to the one attempt that
committed it. A remembered continuation resumed with a different replacement
code than its reveal entry committed is refused an anchor rather than guessed;
a transient one anchors the replacement the document carries, and refuses only
when two attempts committed different ones.
Changed
-
The "install collection epochs only under the key the user-key roster
delivers" gate is one place:ensureWalletSpaceEpochs(/keys) takes an
optionalrosterDescriptorand refuses the whole fan-out when the roster's
current epoch is not the given user key, reporting it on the result's new
skippedmember ({ rosterEpochId? }, nothing written). Both genesis
ceremonies hand it their landed roster:ensureAccountGenesis(/genesis)
now setsAccountGenesisResult.epochsSkippedtoo, so a re-run adopting a
roster keyed to another user key no longer installs epoch[0] under the key it
was handed, and it runs no epoch stage at all behind a failed roster stage (no
roster means no epochs; the re-run installs both). -
ensureRosterDeliveredEpochs'sbeforeMintseam (/clientAnnex) is
required, refused with aTypeErrorbefore any read when absent: a served
absent roster is the one observation that can turn the stage into a
single-recipient roster genesis, so every caller states what licenses the
mint. The credential-anchored establishment's adopted-roster arm, which
supplied no preconditions, now refuses the mint outright, since the genesis
adopted a present roster a moment earlier and a host serving it absent
afterwards is contradicting itself. -
mintClientWebvhUpdateKeys(/webvh) is synchronous: it only draws two
random seeds, so it returns the pair directly instead of a promise. Callers
drop theawait. -
appConnectRequestOf(/request) andwalletOnboardingRequestOf
(/request/onboarding) sharesingletonQueryOf
(request/queryPredicates.ts) for the one-mental-model-per-exchange check --
exactly one query of the type, none of the mutually exclusive types -- so a
third singleton query type is one call rather than a restated loop. -
The recipient-retiring roster paths acquire the roster once per write.
rotateUserKeyRosterandreplaceUserKeyRosterRecipients(/keys) take an
optionalcurrent-- the read the caller just performed on the same store
instance (DescriptorStoreRead) -- and seed was-client's compare-and-swap
from it instead of reading again; a lost compare-and-swap re-reads as before.
convergeUserKeyRosterToDocumentseeds every branch from its own read (or
from a threadeddescriptorwhen itsetagcomes along),
retireRosterRecipientAndCascadeseeds its rotation from the read it decided
on, andaddUserKeyRosterRecipientseeds its escrow the same way.
UserKeyRosterReadResultcarries theetagof a read it fetched itself, and
convergeUserKeyRosterToAccount(/clients) takes it asetagso the login
sweep's convergence write is seeded from the start's read; the sweep then
adopts a rotation off the rotation's own result rather than re-reading. On the
log-governed store that is one hash-chain walk fewer per client disconnect,
credential retirement, and forget, and two fewer per converging login sweep. -
The log-governed roster store's
replaceno longer refuses when no read on
the same instance precedes it: it acquires the verified head itself and
appends under the caller's validator, so a compare-and-swap seeded from
another instance's read costs one read rather than a refusal. A replace on an
absent log is still refused. -
preflightUnlockCredentialRetirement(/unlock) also returns the attributed
ladderVmIdsand thedocumentthe gate ran over;retireUnlockCredential's
stage 0 now calls it instead of restating the read-attribute-gate sequence. -
verifyAccountLog's substituted-account refusal shares
assertPublishedLogDidwith the ceremony reads, so its message is now the
shared one (resolves to a different DID (X) than expected (Y)). -
deleteUnlockSpaceWithCapability(/keyring) is a re-export of
deleteSpaceWithCapabilityrather than a wrapper. -
Independent awaits run concurrently: the unlock record's member seals and
opens, the recovery spend's hash derivations, the contacts LWW decrypts, the
mend's collection describes, and the last-client forget's delegation
revocations. -
The two recovery continuations (
recoverWebvhClient,
recoverWebvhLadderAnchored) run over one shared body,
recoveryContinuationOnceinrecovery/continuation.ts; each variant
supplies only its successor key, the methods and relation memberships its
entry adds, and what itsonCommittedseam hands back. Both entries go
throughsignAccountEntryon its committed-key arm, so the pinned read, the
carry-over precondition, the conditional publish, and the pin advance are the
seam's; the add-and-retire entry is built on the head the reveal entry's own
publish leaves standing, with a re-read under the same pin only against a
store whose PUT serves no ETag. The entries both publish are byte-identical to
before.RecoveryKeyNotCommittedError,recoveryVmId, and the recovery key
types now live there, re-exported fromrecoveryWebvh.ts, so the/recovery
subpath is unchanged. -
SyncEngine(/sync) memoizesensureProvisioned: once a call resolves,
later cycles skip it until the newinvalidateProvisioning()is called; a
call that throws is not memoized. A new optionalremintPendingdep runs
every cycle right after provisioning, before the migration sweep and the push,
so an eager minter's create-loss re-mint always lands ahead of the push. -
Breaking.
delegatedWebvhLogStore(/webvh) no longer takes
publicRead. The read mode is now decided from the wallet Space roster: the
world-readableidcollection is fetched unauthenticated, and every other
collection (an annex generation included) reads through the delegation. -
Breaking.
logGovernedDescriptorSource(/descriptors) takesspaceId
in place oflogIdFor; it keys each collection's chain-head pin itself with
the newcollectionDescriptorLogPinId, so no caller builds the slot. -
clientRemovalFields(/webvh) now derives the latent commitments a
staged-hash attribution must exclude from the log itself
(standingCredentialLatentHashes, walking every standing credential's
ladder), cross-checking the caller'sknownLatentHashesagainst that
derivation and logging a mismatch rather than trusting either list blind. The
derived set never removes the hash the decision-0007 position names as the
client's staged hash. -
revealLadderRungWebvh(/clientAnnex) now returns the rung it attributed
and the head it leaves standing ({ revealed, rung, published }), assembled
from the entry's ownupdateDIDresult and publish ETag. The transient
readiness pass's pointer move builds its pointer entry on that head with the
rung in hand, dropping onedid.jsonlread and one ladder attribution per
attempt. TheLadderSignedEntryOutcometype carries the publish'setag, and
the newaccountEntryHead(/webvh) is the one head assembly over the
account-entry seam's outcome, shared with the pointer entry. A head with no
validator is still re-read and re-attributed, so the pointer entry's
compare-and-swap never degrades to an unconditional write. The
self-enrollment's add entry builds on its reveal entry's head the same way,
with the same no-validator fallback. -
delegatedWebvhLogStore(/webvh) now hands back the delegated PUT's ETag,
as the rootid-collection store already did. Without it every
bridge-delegated publish left the next entry to re-read the log for its
compare-and-swap token. -
The ladder-held
#DelegatedClientspointer move is one account-log entry.
setDelegatedClientsPointer(/clientAnnex) signs on the ladder arm as well
as the client arm: the attributed rung reveals itself in the pointer entry
and, when it stood only committed, the entry commits the next rung's hash
beside it, where a reveal entry followed by a client-signed pointer entry used
to land.movePointerAsLadder(now exported) is that arm over the transient
readiness pass's and the establishment's pointer moves, and the outcome
carriesrungon the ladder arm.pointerEntryUpdateKeysand its fallback
re-read are gone with the second entry. -
ensurePointedClientAnnexGeneration(/clientAnnex) takes an explicit
signer:{ kind: 'client', updateKeys }for an enrolled client's own update
keys, or{ kind: 'ladder' }to move the pointer as the ladder under the
suppliedladderSeed, which is what the credential-anchored establishment's
stage 3 does. The outcome'srungis the ladder arm's. TheupdateKeys
member is no longer accepted. -
signAccountEntry(/webvh) andsetDelegatedClientsPointerdefault
logOnlyper arm (trueon the ladder arm,falseon the client arm)
instead of forcing it on the ladder arm, so a ladder-signed entry written
through a root-invoking store can republish thedid:webprojection beside
itself by passingfalse. -
withThreadedHeadOncemoved to/webvh, besidewithLogConflictRetry, so a
base ceremony that saved a read can use it. -
mintedNewClientjoins thetest/node/fixtures/clientKeys.tsfixture, in
place of seven file-local copies. -
issueAppKeyCredential(/request): the caller-supplied-seed app-key
credential issuer undermintAppKeyCredentialandreissueAppKeyCredential
is now exported, so an application's own self-issue path signs the same
credential shape instead of maintaining a copy. It takes an optional
documentLoader(defaulting to the shared security loader) and an optional
description(defaulting to the wallet's consent sentence). -
retireRosterRecipientAndCascade(/keys): the recipient-naming twin of
rotateRosterToDocumentAndCascade, for a ceremony that rotates the roster
before its own document edit. It retires one named roster recipient, reads the
fresh key back through a caller-supplied key-agreement key, and runs the same
collection fan-out, with no seal backstop. -
anchorRosterStoreAt(/keys): sets a log-governed roster store's minimum
controller version from a given account log; shared by both cascade entry
points. -
rosterWrapsRecipient(/keys): the shared "does the current epoch wrap to
this recipient" probe, now also used byaddUserKeyRosterRecipient. -
UserKeyAdoptedHook(/keys): the named type for the roster-adoption
persistence callback both cascade entry points take.
Fixed
- The enrollment wrap (
addUserKeyRosterRecipient,/keys) now refuses a
roster whosecurrentEpochnames no epoch in its own list, instead of
proceeding toaddRecipient. The collection cascade's per-collection op
raisesUserKeyRosterIntegrityErrorfor the same shape rather than a bare
Error. The newcurrentEpochOf(/keys) is the one implementation both
sites, and the roster read, now share.
Changed
- The seeded ladder-rung scans (
/clientAnnex,attributeLadderRungand
attributeLadderInventory) derive the scanned range concurrently rather than
one rung perawait. Each rung is an HKDF expansion plus an Ed25519 keygen,
and the default range is 128 rungs. The full range is still scanned, the
classification order is unchanged, and no refusal changes. - The client-annex generation minters (
/clientAnnex,
mintClientAnnexGenerationandmintCredentialClientAnnexGeneration) return
the published head of the genesis log they just wrote -- the log, the DID and
document, the effective update-key parameters, and the PUT's own ETag when the
store hands one back -- rather than the log and document alone. - The credential-anchored establishment's generation-delegation install stands
on that head instead of re-fetching and re-resolving the log the mint wrote a
moment earlier: one fewer serialized request on every signup. The GC
generation swap and the annex heal's fresh-generation arm do the same. - A head carrying no ETag is not carried forward, since the install's entry
publishes under a compare-and-swap and a head with no validator would degrade
that to an unconditional write. The install then reads for itself, as before.
Either way its own publish advances the generation's chain-head pin. - The fresh-generation block the credential-anchored paths share -- mint the
generation, install its generation delegation, append the account document's
pointer entry -- is one helper,mintPointedClientAnnexGeneration
(/clientAnnex), with the pointer write injected: the establishment's two
arms sign it as a client, the transient visit's fresh-generation arm moves it
as the ladder. The establishment's bootstrap arm runs its controller flip
through the helper'sbeforePointerEntryseam, between the install and the
pointer entry, as before. rebindCredentialAnchoredRecordandcredentialAnchoredStandingFields
(/clientAnnex) are the one builder of the ladder-VM-signed bridge, sibling,
and record re-bind, and the one builder of the standing fields a registry
entry records; the establishment's stage 4 and the mend's record-downgrade
re-bind and registry arm now share them.- The annex heal's two pre-flight attributions merge into
attributePointerEntryRung(/clientAnnex), the one place a
LadderAttributionErrormaps ontoClientAnnexGenerationUnavailableError;
pointerEntryUpdateKeysis a wrapper over it that still refuses a
committed-only rung. - The controller flip's error-handling asymmetry is now stated in code: the
establishment's bootstrap arm swallows an authorization-class refusal because
its Space may be one a concurrent run already flipped, while the annex heal's
fresh-Space flip acts on a Space id minted a moment ago that no other run can
hold, so it propagates every failure. forgetEnrolledClientandforgetLastEnrolledClient(/clientAnnex) now run
their roster rotation and collection fan-out through
retireRosterRecipientAndCascadeinstead of restating it, so both forgets set
the roster store's minimum controller version the same way the revocation
cascade does.- BREAKING:
forgetLastEnrolledClienttakes a
rosterStore: EncryptionDescriptorStoreoption instead of a
rosterStoreFor: ({ did, log }) => storefactory. The ceremony now anchors
the roster store's minimum controller version itself -- at the pre-transition
head for the opening read, at the post-reinstall head for the rotation -- so a
store wired over a cached pre-transition view still anchors the transition's
one ladder-signed roster append past the reinstall entry. Callers building a
rosterStoreForfactory must pass one store instead. The store must be
anchorable (SealableEncryptionDescriptorStore); one without
setMinimumControllerVersionis refused with aTypeErrorbefore any read,
since the strike-and-reinstall pair would otherwise publish two entries per
attempt ahead of a rotation the ceremony-tail license refuses every time. isSealableDescriptorStore(/keys) probes both interface members (seal
andsetMinimumControllerVersion) rather thansealalone.
Removed
removeUnlockKey'srequireLadderVmClaimoption (/unlock): every caller
passedtrue, so the retirement gate now runs unconditionally on the remove
polarity.ladderSigningPair(/clientAnnex): the client-arm pair of a revealed rung
has no caller now that the pointer move signs as the ladder.- The enrolled-client listing (
listEnrolledWebvhClients,/webvh) attributes
every client's active update key in one forward pass over the log, matching
its single-pass enrollment-index scan, instead of walking the log tail once
per client. The pass tracks only the clients the final document lists, starts
at the earliest of their enrollment entries, and builds no per-entry Set.
attributeClientUpdateKeyruns the same pass for one client, which is the
tail walk from that client's add entry.