Releases: interop-alliance/wallet-core
Release list
v0.73.0
Added
userKeySigningSeed,userKeyRecordSigner,userKeySigningKeyMultibase, and
USER_KEY_SALT(/keys): the user key's Ed25519 signing half, derived from
the key-agreement half's raw 32-byte secret (HKDF-SHA256, salt
freewallet/keys/user-key/v1, infosigning). Every holder of the user key
can now sign and verify a record proof with nothing server-served in hand, so
a record sealed to the vault KAK carries authenticity as well as
confidentiality.recordSignerFromSeed(/keyring): the record signer seam over a raw 32-byte
Ed25519 seed, for a signing key derived on demand rather than held by an
agent.verified-registryinEVIDENCE(/menders): a registry record whose proof
verified under the user key the reader holds, before it was decrypted.
Changed
recordSignerFromSeed,updateKeySigner, and the ladder VM signer take their
signer from@interop/ed25519-verification-key8.2.0'sdidKeySigner()
instead of each naming the key pair by hand before asking for one.
Removed
- Breaking:
UserKey.signingSeedand the client-key record's
userKey.signingSeedmember. The signing half derives from the key-agreement
secret, so it is neither minted nor stored:mintUserKeyreturns
{ id, secret }, andencodeClientKeyRecord/decodeClientKeyRecord
neither write nor read the member.
v0.72.0
Removed
- Breaking:
getUnlockKeyringWithCapability,
putUnlockKeyringWithCapability, anddeleteUnlockSpaceWithCapability
(/keyring).getUnlockKeyring,putUnlockKeyring, anddeleteUnlockSpace
take an optionalcapabilityinstead, so every unlock Space operation has one
request shape whether it is a root invocation or rides the delegated
management zcap. - Breaking:
isSoleEnrolledClient(/clientAnnex), replaced by
isLastEnrolledClient({ doc, signingVmId })(/webvh).
Added
-
clientAdditionFields(/webvh): the add-side twin ofclientRemovalFields.
One enrolled client's marked verification-method pair and its relation
membership (all four signing relations pluskeyAgreement) as one bundle,
which the genesis assembly, the enrollment add entry, the self-enrollment add
entry, and the recovery continuation now take instead of restating the five
relations each. -
enrolledClientVmIdsandisLastEnrolledClient(/webvh): the one read of
which methods are enrolled clients, and the last-client rule over it. The
plain forget, the last-client transition, the enrolled-client listing, and the
disconnect policy's count all decide on it. -
The
/testingsubpath, test fixtures only:recordedGrant(a delegated zcap
as the delegation suite records it, the chain in the proof with a parent
delegation embedded as its last link) andaccountSignerCheckover
SIGNER_FIXTURE(one enrolled client and one ladder VM under
capabilityDelegation, a pointed and an old annex generation). The grant
revocation tests here and in freewallet import them in place of two
hand-written copies. Consumers keep it out of production code by lint
restriction, as with@interop/vh-resource-log/testing. -
The
/menderssubpath: the mender registry's shared half. It carries the
InvariantDeclarationandRegistrationtypes, the closed vocabularies
(AUTHORITIES,TRIGGERS,CHAIN_TRIGGERS,ACCOUNT_SHAPES,EVIDENCE,
MEND_OUTCOMES,GAP_KINDS) and theINVARIANT_IDScensus, the
menderRegistryreaders (all,byId,sites,admits,dueAt) with
heldAuthoritiesand itsResolvedAuthoritykind type, and the derived-set
helperstransientReachableInvariants,deriveGaps,undeclaredGaps, and
undeclaredInvariants.menderRegistrychecks its table and sites once at
construction and throws aTypeErroron a duplicated declaration id, a site
reporting an undeclared invariant, or aguardedByon a site that is not a
login-routingone (RegistrationSiteis a union ontrigger, so the type
forbids the third). A detector is not a mender: an invariant no site reports
derives anonegap whether or not it declaresholdsWhen, and
transientReachableInvariantscounts an unreported declaration only as a
detector checked at a trigger a transient visit fires, under the ladder held
set on the chain. A wallet declares its own table and its own registrations;
nothing here executes a ceremony. -
The mender runner on
/menders:runMenderBlockruns one chain trigger's
registrations in list order, filtered by the held authorities and by each
reported declaration'swhen(route)predicate. An optional seed registration
runs first and its failure aborts the block; past it, a registration that
throws warns once per reported invariant with that declaration's ownwarn
string through the wallet-suppliedLogger, reportsfailedcarrying
err.namealone, and the block continues. A registration whose entries do not
match the invariants it reports warns and is normalized tofailedentries
carryingMEND_REPORT_SHAPE_ERROR.onOutcomeis the single place an entry
is reported. An optionalregistrationslist overrides which registrations
the block runs, under the same authority and route tests, for a wallet that
runs one trigger's list in parts (a settle point partway through); a seed or
override registration listed under another trigger is refused with a
TypeError. A converge-free site the registry indexes beside its
registrations is never executed, whichever list the block runs from. -
mendReportAccumulator(/menders): the report collector a wallet creates
ahead of session assembly, so a routing site's entries and the chain's entries
assemble into oneMendReport.reportdoubles asrunMenderBlock's
onOutcome, andsettledresolves with the assembled report at the first
settle(). -
errorNameOf(/menders): the name a mend report carries for a thrown value
of any shape, so an app-side report site does not read.nameoff a
non-Errorthrow. -
LoginRoute(/menders): the route typeInvariantDeclaration.whenreads,
{ popup: boolean }today. -
addHistoryProfileCreated,addHistoryCollectionShared, and
addHistoryCollectionUnshared(/space), completing thewallet-activity
builder set. Theirtype/summarybytes match what the wallets were
writing inline, so existing records are unaffected. -
isDelegationExpiredand theRevokeGenerationDelegationOutcometype
(/clientAnnex): the expiry check and the outcome union
revokeTreatingAlreadyRevokedAsSuccessnow returns. -
revokeRecordedGrant,classifyGrantRevocationRefusal,
embeddedParentCapability,isClientAnnexDid, and theAccountSignerCheck,
GrantRevocationOutcome, andGrantRevocationRefusaltypes (/clientAnnex):
revoking one recorded app or agent grant under the same policy
revokeTreatingAlreadyRevokedAsSuccessapplies to a generation delegation.
The one local skip is a grant expired beyondREVOCATION_CLOCK_SKEW_MS;
everything else is POSTed;AlreadyRevokedErroris success; a plain
ValidationErroris read against the verified account document asexpired,
orphaned(a root-delegated grant whose signer left the document),
signer-gone(an embedded parent delegation whose proof key left the
document), orgeneration-swapped(a parent generation delegation naming a
generation the document no longer points at), and rethrown when the client
cannot say why. It composesdelegationExpired,delegationAtExpiry, and
delegationSignerGone(/webvh) withderiveGrantSignerState(/clients).
Changed
-
The byoe service-entry convention is read and written through one predicate in
/clientAnnex: the delegated-clients pointer and the generation delegation
each match on the type IRI and the endpoint shape (a string, a map), for the
readers and the in-place re-point alike. A malformed entry of the right type
is now skipped by both, where the writer used to re-point it in place while
the reader could not see it. -
decisions/0002,0004,0006,0008,0010, and0011saydurable
only of server-backed state: their client, session, login, and recovery-spend
uses take the enrolled and remembered words, prose only. -
Every topic doc under
docs/architecture/now states its ceremony's pivot
write, which of the ceremony's other writes sit before and after it, and the
invariants a torn run can leave violated, numbered as inINVARIANT_IDS.
did-webvh-account-log.mdgains an update-key rotation section for the one
ceremony that had no account of its own. -
ARCHITECTURE.md: a mender registry subsection beside "Ceremonies and cascades"
names the registry's unit, the authority values, and the trigger values, and
holds the four menders (the Space-controller promotion, the
credential-anchored mend, the transient readiness ensure, and the login-time
roster sweep) with their invariant ids. The ceremony table keeps only
ceremonies, each row now carrying itsCeremonyId, and gains the
recovery-code issuance and revocation rows. -
menderRegistry'sdueAttakes an optionalroute. With one, a registration
is due only when every reported declaration either declares nowhen
predicate or admits that route; without one, the route test does not apply and
the reader behaves as before. -
Breaking:
deleteUnlockSpacereturns
{ outcome: 'deleted' | 'not-found' }in both forms (a 404 is reported, not
decided), where the root-invoked form previously resolvedvoid. -
fetchKeyringRecord'skdfparameter is required. It no longer defaults to
KEYRING_KDF, so a caller names its unlock method's parameter set and a
method that omits it fails to compile instead of deriving the
passphrase-salted unlock identity and reading the wrong unlock Space. -
CapabilityAgentnow comes from@interop/capability-agent; the
@interop/webkms-clientdependency is dropped. The unlock identity derivation
is unchanged. -
The account-document relation reader memoizes its
verificationMethodindex
per document, so the resource-log controller no longer rebuilds it for each
relation it resolves per log entry. -
publishUnlockKey's fresh-bind check (a recorded update key that is not rung
0 of the ladder seed handed in refuses withLadderAttributionError, nothing
written) is documented as the checked half of theladderCommitmentcontract
onunlockKeyVerificationMethod. The seedless bind and the remembered
recovery continuation's replacement member are named as the two emitters that
still take the value on trust. -
mendCredentialAnchoredAccounthands the account log its roster-mint
preconditions verified to the registry arm instead of reading and verifying it
a second time. -
Breaking:
revokeTreatingAlreadyRevokedAsSuccess(/clientAnnex) no
longer reads every was-clientValidationErroras success, and now takes the
locally verified account document (accountDoc). It skips the POST only for a
delegation whose ownexpiresis past by more thanREVOCATION_CLOCK_SKEW_MS
(ten minutes; an absent or unparseableexpiresis not treated as expired),
and otherwise POSTs whatever the caller's document says about the signer. It
reads was-client ...
v0.71.0
Removed
- The
./identitysubpath. The WAS identity derivation (agentsFromSecret/
agentsFromSeed/agentsFromKeyAgent,ProfileAgents,singleKeyResolver,
zcapClientForSigner, theBOOTSTRAP_HANDLE/BOOTSTRAP_KEY_NAMEhandles)
moved byte-for-byte to@interop/was-client/identity0.55.0, with
zcapClientForSigneralso at was-client's root entry. Consumers repoint their
imports from@interop/wallet-core/identityto
@interop/was-client/identity.
Changed
@interop/was-clientdependency bumped to^0.55.0.- The identity fixture test (
test/node/identity.test.ts, pinning the
seed/secret derivation to its did:key and KAK multibase values) now runs
against the was-client home.
v0.70.0
Added
deriveGrantSignerState(clients): the grant-state check a connected-apps
surface runs over a recorded delegation's signers against the enrolled
clients' current signing keys, returningactive,orphaned, orunknown
(GrantSignerState). A signer is judged against the account document only
when the document could have listed it, its DID being the account DID or a
did:key. A client-annex per-visit signer (a transient session's grant) derives
asunknownrather thanorphaned: the document never lists it, and whether
its chain under the generation delegation is still alive is the revocation's
to settle.isWasLinkPayload(space): the structural recognizer for awas-linkQR
payload, handed to@interop/wallet-request's input classifier as a
recognizer.
Changed
- ARCHITECTURE.md is a short map again: module map, overviews, the key
hierarchy, the ceremony inventory, wire constants, glossary. Each area's full
account moved to a topic doc underdocs/architecture/, with history, roadmap
ids, and restated rules dropped along the way. publishUnlockKeyrefuses to re-bind a standing credential member under
another ladder: a bind whose rung-0 hash differs from the member's
ladderCommitmentthrowsLadderAttributionErrorwith nothing written, on
the merged entry and on either half of a split bind. It used to re-add the
member naming the new hash, which left the member unclaimable by every
seedless reader for the rest of its standing run and the first ladder's VM and
commitment as orphans. The converging re-run is the one holding the seed that
bound the member. A consumer whose retry mints a fresh seed for a member the
account already lists now sees the refusal instead of the orphans.attributeUnlockLadderInventory(the removal edit and the retirement
pre-flight) walks from both anchors a ceremony holds, the registry's recorded
update key and the member's ownladderCommitment, and cross-checks them: the
registry-anchored inventory must be contained in the member-anchored one, the
member's reading is what the strike acts on, and anchors resolving to
different ladders refuse withLadderAttributionError. With a ladder seed in
hand the seed's rung-0 hash must be the member's named commitment. The
member-anchored walk starts at rung 0 and needs no backward climb, so a ladder
VM the last-client transition reinstalled is now struck seedlessly after a
later self-enrollment advanced the recorded anchor past the acting rung (the
gap WC-158 named).
Fixed
- The ladder walk's reveal branch no longer claims the hashes of an entry that
installs another credential's inventory: a committed rung revealing itself in
the ladder-signed bind of a sibling credential took the sibling's rung-0
commitment as its own next claim, and once a later self-enrollment spent the
rung the acting credential's retirement struck that commitment, leaving the
sibling standing in the document with no update authority. The narrowing the
signed-while-revealed branch already applied now covers the reveal itself. - The seedless ladder walk (
attributeLadderInventory, read backwards by
recoverEarlierRungs) no longer recovers the acting credential's rung as the
bound credential's from a ladder-signed bind entry. A committed rung that
reveals itself in thepublishUnlockKeyentry it signs met every gate of the
climb rule, so a seedless retirement of the credential it bound (from an
enrolled client, or the retirement gate's pre-flight) struck the acting
ladder's revealed rung fromupdateKeys, leaving that credential unable to
extend the log. The climb now stops at an entry that installs the credential's
inventory (itskeyAgreementmember, or a ladder VM not standing before)
unless the signer's own hash is newly committed there, the ladder-anchored
genesis shape. - A credential member's
ladderCommitmentis adopted as the ladder's anchor
(credentialLadderAnchor) only when the log committed that hash for the
member: newly added tonextKeyHashesby the introducing entry, by a later
entry of the member's standing run, or by an earlier entry whose signer the
introducing entry retires (a recovery continuation's reveal entry). A member
restating a hash that already stood -- an enrolled client's staged hash --
names no anchor and is reported unclaimed. AndsurvivingClientKeyProtection
(whose parameter is nowderivedLatentHashes) never lets a walk-derived claim
prune the positional successor of a surviving client's update-key hash, so a
walk anchored on a client's staged hash can no longer strike it from
nextKeyHasheson a recovery spend. serializedAppUrl(request) refuses anappUrlwhose origin is opaque
(chrome-extension:,file:, any non-special scheme). Such an origin
serializes as the string"null"and is same-origin only with itself, so the
equality check accepted any two opaque-origin values as same-origin. The rule
stays the spec's: origin equality alone, with no scheme constraint, so a
same-originws:appUrl still passes. The wallet-onboardinghostvalidator
keeps its mirror shape (scheme check, no origin), and both are now covered on
that asymmetry by tests.
Changed
hasAppConnectQuery(queries)(request) is the one presence test for an
AppConnectQuery:processRequest's App Connect gate runs it, and every
typecomparison in the query predicates (isAppConnectQuery,
isWalletOnboardingQuery,exclusiveQueryOf's match) goes through one
private helper, so the type string is compared in one place.processRequest
gains its own test file.- The "one mental model per exchange" exclusion is one shared set
(EXCLUSIVE_QUERY_TYPESinrequest, currentlyAppConnectQueryand
WalletOnboardingQuery):exclusiveQueryOfderives each type's exclusion
list from it, so a third exclusive type is one entry and is refused from every
side. It replacessingletonQueryOf, which took a per-caller exclusion
predicate and message; the two refusal messages are unchanged. - Breaking:
KEYRING_KDF(keyring) is now Argon2id (64 MiB memory, 3
passes, parallelism 1, 32-byte output) under the salt
freewallet/keyring/unlock/argon2id/v1, passphrase version 2. The memory and
pass counts follow RFC 9106's second recommended option; the parallelism is a
deliberate departure from that option's four lanes, since noble is
single-threaded. The PBKDF2-600k set is replaced outright, with no try-both
locate: a passphrase account bound before this release cannot be entered by
passphrase, since its unlock Space is addressed by the PBKDF2-derived
identity. Passkeys (HKDF over the PRF output) and recovery codes (their own
HKDF) are unaffected. UnlockKdfgains anArgon2idarm (memoryin KiB,passes,parallelism,
salt; nohashmember), andderiveUnlockSeedderives it over
@noble/hashes/argon2.js. ThePBKDF2arm is removed with it: no shipped
parameter set names it. TheKEYRING_RECORD_VERSIONframe version is
unchanged: the KDF's ownversionrecords the parameter set.- Breaking: every credential-class
keyAgreementverification method in the
account did:webvh document now carriesladderCommitment, a plain JSON member
holdinghash(rung 0)of the credential's ladder in the multihash form
nextKeyHashesalready carries.unlockKeyVerificationMethod(unlock) is
the one builder every bind site writes it through, now requires the property,
and returns the typedCredentialKeyAgreementMethod; an enrolled client's
marked key-agreement twin never carries it.credentialLadderAnchor
(clientAnnex) anchors a credential's seedless ladder walk by reading the
property off the entry that introduced the member (a member re-bound after a
retirement anchors on its fresh ladder; one whose value changes while it
stands is refused as retargeted) instead of inferring an anchor from the shape
of that entry; it now returns{ anchorHash }only. The three retired
bind-shape readings and thekeyAgreementrelation-order rule they depended
on are removed. A credential-class member with noladderCommitmentnames no
anchor and is reported unclaimed. There is no fallback for a log written
before this change.
Removed
- The
requestsubpath and itsrequest/matchingleaf subpath move to
@interop/wallet-request0.1.0. The@interop/vc,
@interop/data-integrity-proof, and@interop/security-document-loader
dependencies go with it. A wallet now hands the new package's classifier
isWasLinkPayload(space) andisConnectCode(enrollment) as
recognizers.
v0.69.0
Added
collectionDescriptorLogStore(keys): the log-governed, sealable descriptor
store over ONE encrypted collection's governing history log -- the
collection's ownmeta/logsub-resource, whose verified head state is its
encryptiondescriptor. It takes the collection handle, a controller
resolver, a chain-head pin store, and the sameResourceLogSignerthe
roster's store takes; it derives its own pin slot from the handle, and carries
thecollection-descriptorlog class.ResourceLogClass/controllerForLogClass(resourceLog): the log-class
dispatch a log-governed store applies to every controller view it resolves, so
read-side verification and write-side admission run under one rule. The
ceremony-tail license binds theuser-key-rosterclass; a
collection-descriptorlog admits a ladder-signed append onassertionMethod
membership at the anchored version alone (decisions/0013, app-connect-spec
decisions/0003).COLLECTION_HISTORY_LOG_SUBRESOURCE(space),'meta/log': where every
encrypted wallet collection's governing history log lives.collectionStoreFor, required onensureAccountGenesis
(({ did }) => (collectionId) => store) and on
ensureCredentialAnchoredAccountGenesis/
establishCredentialAnchoredAccount/mendCredentialAnchoredAccount
(({ did, log }) => (collectionId) => store). Breaking: every caller of those
four supplies it. The mend also takes an optionalcollectionStore
((collectionId) => store), the per-collection sibling ofrosterStorethat
its post-promotion roster-and-epochs arm lands epochs through; the arm runs
only when both are supplied.controlleroncascadeCollectionsToUserKey: the ceremony's post-edit
controller view, set as every sealable collection store's minimum controller
version before that collection's first append. Absent, each store's injected
resolver decides its own freshness.
Changed
isResourceLogRefusal(resourceLog) is now@interop/vh-resource-log's
(0.5.0), re-exported fromresourceLog/errors.tsin place of the local copy.- The
descriptorssubpath shrinks to the log-governed descriptor source
(logGovernedDescriptorSource,collectionDescriptorLogPinId). The
acquisition and unknown-epoch refresh policy moved to
@interop/was-client/edv(0.54.0) unchanged:acquireDescriptor/
acquireDescriptors,wasDescriptorSource, theEncryptionDescriptorSource
/EncryptionDescriptorCacheseams,DescriptorRefreshPolicy,
createRefreshingEdvDocCipher, andisKeyUnwrapError. Breaking: import them
from there; this subpath re-exports none of them. The truth table is
unchanged: integrity refusals and every continuity reason butrollback, with
the license class deliberately outside the set. - Followed
@interop/was-client's./syncport contract update: the server's
ETagis opaque and can no longer be rebuilt from a bare revision number.
SyncedRowgains anetagfield persisted alongsideversion;
SyncStore.markPushed/markDeletedPushedtake the write's ackedetag
(from itsWriteAck) and record it;runPushbuilds every conditional
write'sifMatchfrom the storedetagstring instead of formatting one from
version. Pull ingestion andadoptLatestrecordetag(andmetaEtag,
where a collection syncs metadata) fromMasterState/WireDoc. - The log-governed descriptor store moved down a layer:
@interop/was-client/edvnow owns the genericlogGovernedDescriptorStore
(verified read, signedreplace/create, the conflict translation,
seal()), thereadGovernedEpochConfigurationhelper, the
WasEpochConfigurationstate type constant, andtoEpochConfigurationState.
The roster'slogGovernedDescriptorStore(keys/rosterLogStore.ts) is now
that store wrapped with the post-edit minimum controller version, and keeps
itsSealableEncryptionDescriptorStoreshape;descriptors/logSource.ts
re-exports the two names it exported before. No behavior change. Requires
@interop/was-client0.52.0. - Every
edvroster collection is now created bare and declared by its own
governing history log.provisionWalletSpacecreates such a collection with
encryption: 'governed', so its Description carries no client-written
descriptor, and the epoch[0] install is the guarded create
(If-None-Match: *) that both declares the collection governed and lands its
first epoch. The server derives the Description'sencryptionmember from the
log head, so the wallet writes the log alone, and a Description already
carrying a client-written descriptor cannot be governed. Breaking for a caller
that relied on provisioning to declare the descriptor. An account whose
collections were provisioned with a client-written descriptor is refused by
every provisioning re-run rather than adopted (the declared descriptor is
immutable server-side, so no conversion exists); such an account is
re-provisioned from scratch. - The provision-time epoch install is store-shaped rather than handle-shaped,
the lookup shape the rotation cascade already took.ensureIndexedFirstEpoch
takesstore,ensureWalletSpaceEpochsandwalletSpaceProvisionertake
storeFor: (collectionId) => EncryptionDescriptorStore, and
ensureRosterDeliveredEpochstakes the samestoreFor. Each store's own
wiring decides the authority its requests ride and, on a governed collection,
the signer its genesis append is proved by. Breaking.spaceIdon
ensureWalletSpaceEpochsis now optional and names the Space in the
per-collection failure messages alone. logGovernedDescriptorStore(keys/rosterLogStore.ts) takes a required
logClass, stated once at construction, and narrows every controller view it
resolves throughcontrollerForLogClass. A store cannot be built without
saying which class of log it governs. Breaking.collectionDescriptorLogPinIdresolves to
space/<spaceId>/<collectionId>/meta/log, over@interop/vh-resource-log's
collectionLogPinId, following the log's move out of thekey-mapcollection
and into the subtree of the collection it governs. The read capability a share
grantee or a connected app already holds covers the log.logGovernedDescriptorSourcetakes aresolveControllerreturning a
WebvhResourceLogController(wasResourceLogController) and reads under the
collection-descriptorclass, so a reader no longer inherits the roster's
license and refuses a served log its own wallet wrote. Breaking for a caller
passing a bare controller view.anchorRosterStoreAtreturns the controller view it built. Both cascade entry
points (rotateRosterToDocumentAndCascade,retireRosterRecipientAndCascade)
thread that view into the collection fan-out, so each log-governed collection
store takes the same post-edit minimum controller version the roster store
takes. Breaking for a caller typed on thevoidreturn.userKeyRosterLogSigneralso signs the per-collection descriptor logs; the
name says which log it was built for first.cascadeCollectionsToUserKeyandensureWalletSpaceEpochscarry a
verified-log refusal (isResourceLogRefusal) infailedverbatim, the
refusal itself as the entry'serrorrather than the wrapped per-collection
message, so a caller reading the report tells it byerr.name. The fan-out
never throws for it: the ceremonies past their pivot keep their later stages
and their resumable-success contract, and the settled collections still reach
walletSpaceProvisioner'sonSettled.ensureWalletSpaceEpochsandwalletSpaceProvisionerrefuse a missing
storeForwith a synchronousTypeErrorbefore any write, the way the
geneses and the mend refuse a missingcollectionStoreFor. A caller on the
former handle-shaped signature fails loudly instead of landing every
collection infailedon a resolving call.controllerForLogClassis an exhaustive switch that throws aTypeErroron a
class it does not name, so a caller outside the type system never gets the
membership-only rule on the user key roster log.- The mend's completion probe and roster-mint precondition decide epoch presence
from each collection's VERIFIED governing log (throughcollectionStore)
rather than from the Description's server-derivedencryptionmember. The
Description read stays, as the readability check under the caller's authority.
A host omitting the derived member can no longer license a fresh roster
genesis over collections keyed under the real user key. - Requires
@interop/vh-resource-log0.5.0 (collectionLogPinId,
isResourceLogRefusal) and@interop/was-client0.54.0 (the'governed'
provisioning value, themeta/logtransport seam, and the moved descriptor
policy). was-client 0.54.0 is unpublished today.
Removed
formatEtag, re-exported frompush.ts/ thesyncsubpath barrel. A
SyncStoreimplementation now persists the opaqueetagstring itself rather
than reconstructing a validator fromversion.readGovernedEpochConfigurationandEPOCH_CONFIGURATION_STATE_TYPEfrom the
descriptorsbarrel, andlogGovernedDescriptorStoreand
EPOCH_CONFIGURATION_STATE_TYPEfrom thekeysbarrel. Every governed store
states its log class at construction, so the class-stating builders
(userKeyRosterDescriptorStore,collectionDescriptorLogStore,
logGovernedDescriptorSource) are the public surface; a bare-controller
reader or a caller-chosen-class wrapper would read or write a collection log
under the roster's license. The functions themselves stay, module-internal.wasandcapabilityonensureWalletSpaceEpochs,collectionon
ensureIndexedFirstEpoch,capabilityonwalletSpaceProvisioner, andwas
andcapabilityonensureRosterDeliveredEpochs. Each collection's transport
and invocation authorit...
v0.68.0
Changed
- The account log's chain-head pin is a property of the store rather than an
optional argument of every ceremony.WebvhIdStoregains a required
pin: { store, logId }member;wasWebvhIdStore,wasWebvhLogStore,
delegatedWebvhLogStore, andclientAnnexLogStoretake a requiredpinStore
at construction and derive the slot themselves.readPublishedLog/
readPublishedLogOrThrowcheck and advance the store's pin on every read, and
putLogResource(sopublishEntryPinnedand every ceremony entry's publish)
advances it on every write. Enrollment approval, client revocation, credential
retirement, and self-enrollment therefore refuse a served truncated prefix of
the log before any entry publishes, where the approver's and the revoker's
reads used to run unpinned.
Removed
- The
pinStore/logIdoptions of every account-log and annex-log ceremony
(signAccountEntry,ladderSignedAccountEntry,enrollWebvhClient,
revokeWebvhClient,publishUnlockKey/removeUnlockKey,
rotateWebvhUpdateKey,ensureDidWebvh, the recovery continuations, the
annex ceremonies, and their orchestrators) and the half-suppliedTypeError,
together withadvanceLogPin.verifyAccountLogkeeps its optional
pinStore, since it fetches by URL and holds no store. A ceremony that builds
stores from a client still takes apinStore, now required. - The
pinStoreoption of the annex orchestrators that already take an
idStore(establishCredentialAnchoredAccount,
ensurePointedClientAnnexGeneration,mendCredentialAnchoredAccount,
ensureCredentialClientAnnexGeneration,runClientAnnexGc,
swapClientAnnexGeneration): the annex logs pin in the account-log store's
pin store, read offidStore.pin.store, so the two log families can no longer
be handed two stores. clientAnnexLogPinId. Every log store derives its slot through the new
logResourcePinId({ spaceId, collectionId })in./webvh, which
accountLogPinIdis now defined over.ensureAccountGenesis,ensureCredentialAnchoredAccountGenesis, and
selfEnrollClientCore'saccountLogPinStoreoption, and
ensurePointedClientAnnexGeneration'saccountSpaceIdoption (it only named
a pin slot the store now derives).
Fixed
- The three reads that tell a dead or absent annex generation from a live one
(the transient readiness ensure's pointed read, the last-client transition's
generation stage, the GC's orphan and pointed reads) go through
readClientAnnexLogOrAbsent: an absent log under a pin this client still
holds is read as absence, so a collected generation is re-pointed, skipped as
log-unreadable, or collected rather than refused as a rollback, while a
served prefix of a pinned generation log stays refused. The helper is exported
from./clientAnnex, and it is one read:readPublishedLoggains an
absentUnderPin: 'absent'mode instead of the helper re-fetching after a
rollbackrefusal. swapClientAnnexGenerationresolves to{ clientAnnexDid, revoke }rather
than the bare DID:revokereports whether the old generation's delegation
wasrevoked, or skipped because the old log embedsno-delegationor is
log-absent, so a swap that could not revoke is no longer reported as one
that did.
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 (...
v0.66.0
Added
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.
v0.65.0
Added
- The ceremony-tail license gains a third admitted shape (
/resourceLog,
assertLadderAppendLicensed). A ladder-signed roster append is now also
admitted at a document version whose enrolled-client set differs from the
previous version's, in either direction, AND whose entry was signed by a rung
of the appending ladder. One-shot per version under the same head comparison
as shape 2, and under the same single-ladder-proof rule; shapes 1 and 2 are
unchanged. It is what lets a client enrollment or a client disconnect
published by a standing credential's ladder rotate the roster as its own tail. - The signer conjunct is what keeps the shape out of the
any-keyAgreement-change predicate the clause rejects: a client's own
enrollment or revocation entry is client-signed and mints no shot for any
ladder. - Rollout is verifier-first. A reader without shape 3 refuses the WHOLE roster
log, since the license throws from the admission hook and the verifier
propagates it, so every roster-log reader ships this release before any writer
emits an append that needs it. attributeLadderRungsPerVersion(/resourceLog), the seedless ladder-rung
attribution the new shape reads: which update keys the account log names as
rungs of which standing ladder, per version. A ladder is anchored at the entry
that introduces its verification method: by the rung that entry reveals and is
signed by, or by the single rung-0 hash that entry commits, taken up when a
later entry authorizes its pre-image. A ladder-branch bind entry is the one
exception -- it reveals the ACTING credential's rung while introducing ANOTHER
credential's ladder VM -- so the walk resolves the revealed key's ownership
first: a key another ladder already holds anchors nothing, the introduced VM
anchors on its own committed hash instead, and the acting ladder climbs; an
entry that also publishes an enrolled client anchors no ladder either, since
the one key it authorizes is that client's. Anchoring also excludes
co-introduction and foreign inventory: an entry introducing another
credential'skeyAgreementmember hands its ladder VM to the signing
credential's ladder only when it introduces no other credential-class member
(or introduces this credential's own), and an entry that merely signs a second
VM, or another credential'skeyAgreementmember, into the document past a
ladder's own standing VM contributes neither that VM nor its rung hashes to
the signing ladder. Once anchored, a ladder then CLIMBS with the log, by
decisions/0007-ladder-reveal-hash-order.md's last-position rule read
forward: an entry authorizing exactly one new update key that signed it
reveals the next rung of the ladder whose rung both authorized and signed the
entry that committed the key's hash LAST among its additions, while that
ladder's VM still stands. Without the climb a self-enrollment would freeze the
attribution at the rung it retires, so no later ladder-signed enrollment or
disconnect of that credential could satisfy shape 3 -- which a passkey
account, self-enrolling by construction, hits from its first login. An
enrollment approval reuses its rung and so authorizes no key of its own, and
the client update key its add entry authorizes was committed first rather than
last, so no client key is ever read as a rung; a recovery spend's handover is
excluded by the struck ladder VM. A snapshot names the rungs the version still
authorizes plus any that signed its own entry. Every other shape leaves the
ladder unattributed, and an unattributed ladder never satisfies shape 3.ControllerInventory(/resourceLog) gains three members for it:
enrolledClientKeys(the version'scapabilityInvocationkey multibases),
entrySignerKeys(the update keys that signed the version's own entry), and
ladderRungKeys(each attributed ladder VM to its rung keys).
webvhResourceLogControllersupplies all three; a hand-built controller view
must too.convergeUserKeyRosterToDocument(/keys) now converges in two directions.
Beside retiring a current-epoch recipient the document no longer keys, it
escrows into every epoch an enrolled client the document DOES key that holds
no wrap -- the mender for a ceremony torn between the entry that published a
client and the append that was to wrap the user key to it. It takes an
optionalownerKeyAgreementKey, which the escrow direction needs to unwrap
each epoch; without one the retire direction runs alone. The result gains
escrowedRecipientIds. Every missing recipient escrows in ONE append: on a
ladder-signed store only the first append per version is licensed, so
escrowing two wraps separately threwResourceLogLicenseErrorafter the
caller's pivot, and the fix runs was-client'saddRecipientagainst a
buffering descriptor store, writing the staged result back once under the
compare-and-swap token of the read the whole batch was built on. A pure escrow
mints no epoch, and a healthy roster still writes nothing.enrolledClientRosterRecipients(/keys): the enrolled clients a document
keys, as roster recipients, rebuilt from thedid:keycontroller marker and
the key-agreement method the document carries between them. A standing unlock
credential is not among them -- its roster kid names a signing key no document
publishes -- so a credential's missing wrap stays the business of the ceremony
holding the credential.signAccountEntryandAccountLogSigner(/webvh,/clientAnnex): the
account-log signer seam.AccountLogSigneris the discriminated union
{ kind: 'client', updateKeys }|{ kind: 'ladder', ladderSeed }, and one
buildcallback describes a ceremony's document delta once for both arms. The
client arm is the enrolled-client path unchanged (the active-key precondition,
the entry's own stated parameters,did.jsonlbeside itsdid:web
projection); the ladder arm is the standing credential's (the rung attributed
from the log, the acting rung unioned back intoupdateKeys, its carry-over
hash before the build's own commit hashes,did.jsonlalone).
ladderSignedAccountEntryis now that arm under the annex's name and is
exported from/clientAnnexbesiderevealLadderRungWebvh.enrollWebvhClientmoved to its own module (webvh/enrollClient.ts); its
export from/webvhis unchanged. It gains optionalexpectedDid,pinStore
andlogId.publishUnlockKey(/unlock) andpublishRecoveryKey(/recovery) take an
optionalpart('all'by default, or'key'/'authority'), which splits
a bind across two entries where a ceremony must land a credential's decryption
material before its authority.'key'publishes thekeyAgreementmember
alone;'authority'installs the ladder VM and commits the rung-0 hash.recoveryClientFromCode(/recovery) returns the code'sladderSeedand
ladderVmKeyMultibasebeside its existing members.updateSeedand
updateKeyMultibaseare now rung 0 of that ladder.UnclaimedLadderVmRetirementError(/unlock) carries an optional
anchorKeyMultibase, the update key the refused walk was anchored on, named
in the message beside the unclaimed verification methods.disconnectEligibility(/clients) takes an optional
signerKind: 'client' | 'ladder'(default'client', so an existing caller
is unchanged). The ladder branch lifts theselfandlast-clientrefusals
and keepsunattributed-update-key.wasClientLabelsStore(/keys) takes an optionalcapability, the option
userKeyRosterDescriptorStoreandwasWebvhLogStorealready take. The labels
read and the labels write both ride it, so a session holding only a delegated
Space-subtree zcap can read and set client labels; absent, both invoke the
root capability as before.- An optional
projectionStoreon every ceremony body whose entry strikes
inventory:revokeWebvhClient(/webvh) andrevokeAccountClient
(/clients),removeUnlockKeyandretireUnlockCredential(/unlock), and
removeRecoveryKey(/recovery). It is the narrowid-collection seam the
forget ceremony'sclientLogStorealready takes
(Pick<WebvhIdStore, 'getIdResourceRaw' | 'putIdResource'>). Supplied, the
post-strikedid:webprojection is PUT through it immediately BEFORE the
entry publishes, on both signer arms, which is what keeps a ladder-signed
removal (it writesdid.jsonlalone through the credential's bridge) from
leavingdid.jsonnaming the struck client or credential. A failed PUT is
warned and the entry still publishes: a projection under-listing a key the log
still carries is the safe residue, and the next visit's
ensureDidWebProjectionmends it. Omitted, behavior is unchanged -- the
client arm keeps its own post-entry projection PUT.
Changed
- Breaking. The sealable roster store's
setControllerFloor(/keys) is
renamedsetMinimumControllerVersion. Same contract: a ceremony that just
extended the account log hands the store the controller view built from its
post-edit log, and the store's later operations never resolve to anything
staler (a fresher resolved view still wins). enrollWebvhClient(/webvh) returns the post-addlogbeside thedid--
the head it published, or the already-enrolled head it found.- Breaking. Every ceremony body that publishes an account-log entry takes
signer: AccountLogSignerin place ofupdateKeys: ClientWebvhUpdateKeys:
publishUnlockKey/removeUnlockKey/retireUnlockCredential(/unlock),
revokeWebvhClient(/webvh) andrevokeAccountClient(/clients),
enrollWebvhClient(/webvh) andapproveEnrollment(/enrollment),
publishRecoveryKey/removeRecoveryKey(/recovery), and
setDelegatedClientsPointer/setDelegatedClientsPointerOnce/
swapClientAnnexGeneration(`/cli...
v0.62.0
Added
credentialKeyAgreementMethods(/webvh), the credential-class subset of
resolvedKeyAgreementMethods: everykeyAgreemententry the account DID
itself controls, matching a passphrase'sMultikeyCommitmentand a passkey's
or recovery code's verbatimMultikeyalike, since the two publishing entries
are indistinguishable by construction. Freewallet FW-356.attributeUnlockLadderInventory(/unlock), wrapping the annex attribution
helper for a standing credential's own inventory: resolves which ladder rungs
and which ladder VM currently stand in the published log for a given
credential. Freewallet FW-356.rosterRecipientsToRetire(/keys), naming the roster kids a set of retired
credentials leave behind, forreplaceUserKeyRosterRecipients'
retireRecipientIds. Freewallet FW-356.strikeLadderVmWebvh(/clientAnnex), the removal half of a ladder VM's life
cycle, exported alongside the existinginstallLadderVmWebvh. Freewallet
FW-356.credentialLadderAnchor,attributeRetiredCredentialRungsand
retiredCredentialRungsBeforeKey(/clientAnnex): log-only ladder anchoring.
The entry that first introduced a credential'skeyAgreementmember names
rung 0, either as the one key that entry revealed and signed with or as the
one hash it newly committed, so a caller holding no registry entry and no
ladder seed can still resolve a credential's standing rungs. A bind entry that
is ambiguous, or that introduces an enrolled client, is reported rather than
acted on. WC-154.attributeLadderInventoryacceptsanchorHash, and an anchor-free mode that
derives the anchor fromcredentialVmId. The existinganchorKeyMultibase
results are unchanged. WC-154.NextKeyHashesEmptyErrorandassertNextKeyHashesRemain(/clientAnnex):
the refusal an entry raises rather than publishing an emptynextKeyHashes,
which would switch prerotation off. WC-154.
Changed
- The last-client transition's strike-and-reinstall pair mints two
ceremony-tail license shots, one at each inventory-changing version. The
strike version's shot is accepted and the license predicate is unchanged. A
run foreclosed by a sibling ladder's spend at the reinstall version refuses
its rotation and converges on a re-run. WC-156. - Zcap delegation proofs are signed with
eddsa-jcs-2022(EddsaJcs2022from
@interop/ed25519-signature/eddsa-jcs-2022) instead of
Ed25519Signature2020, at all fourZcapClientconstruction sites: the
enrolled client's root client (agentsFromKeyAgent), the did:webvh-keyId and
bare did:key clients (webvhZcapClient/didKeyZcapClient), and the ladder
VM client (ladderVmZcapClient). Every grant this wallet mints -- App Connect
app grants, share grants, the generation delegation, the bridge delegations in
unlock and recovery records -- canonicalizes with JCS rather than URDNA2015,
so signing runs no JSON-LD canonicalization and needs no document loader. The
suite is hard-coded at each site rather than threaded as an option. Requires a
storage server that verifies both suites and ships first. A client that
re-delegates one of these grants must be on this suite too: an
Ed25519Signature2020client cannot re-delegate a JCS-signed parent on its
default loader. Invocations (HTTP signatures) and the VP/credential suites are
unchanged. Freewallet FW-395. - A standing credential's ladder VM now lives and dies with the credential
rather than with enrollment: installed in the same document entry as the
credential'skeyAgreementmember and rung-0 commitment, struck only at the
credential's own retirement.selfEnrollWebvhClient's add entry no longer
strips ladder VMs.publishUnlockKeygains a required
ladderSeed: Uint8Array | nullparameter (nullfor a recovery code, which
carries no ladder) and installs the ladder VM underassertionMethodand
capabilityDelegationin that same entry; it is idempotent on a re-run with
the same seed.removeUnlockKeystrikes the retiring credential's ladder VM
with or without the seed in hand:attributeLadderInventorygains a third
return member,ladderVmIds, attributed either by entry signer or by guarded
co-introduction (the entry that published the credential'skeyAgreement
member also introduced exactly one ladder VM). An attribution ambiguity or a
seed/log disagreement fails closed withLadderAttributionError; an unclaimed
VM is left standing.removeUnlockKey's result and
UnlockCredentialRetirementResultboth gain
ladderVm: LadderVmRemovalReport { struck, unclaimed }, naming the ladder VMs
the edit struck and, separately, any ladder VM still standing that this
credential's attribution could not claim. Freewallet FW-356. retireUnlockCredentialgains stage 0, an optional fail-closed
remintDependentRecords({ document, retiringKeyMultibases })closure run
BEFORE the document edit, naming the ladder VM the retirement is about to
strike so sibling credentials' records and bridge delegations it signed are
re-minted first. The result gainsdependentRecords. It also takes optional
pinStore/logIdparameters, threaded to both its stage-0 read and the
document edit, and passes stage 0's attributed ladder VM ids to
removeUnlockKeyasexpectedLadderVmIds: the edit refuses before writing
when its own attribution resolves a different set
(LadderInventoryDriftError). Freewallet FW-356.- Both recovery continuations (
recoverWebvhLadderAnchored, and the remembered
continuation in/recovery) now retire every pre-recovery standing credential
in the add-and-retire entry: every credential-classkeyAgreementmember and
every ladder VM. Their results gainretiredCredentialVmIds, and
withoutRemovedno longer filters out a resumed run's own freshly minted
ladder VM. A resumed run (the add-and-retire entry already published) derives
retiredCredentialVmIdsoff the log instead of the document it would
otherwise edit again, through the newly exported
retiredCredentialVmIdsFromLog. Freewallet FW-356. forgetLastEnrolledClientstrikes its own ladder VM and reinstalls it
(strikeLadderVmWebvhtheninstallLadderVmWebvh), anchoring the roster
rotation at the reinstall entry so the ceremony-tail license admits it on an
account whose VM already stands; the pair is skipped when the rotation is not
owed. The result memberinstalledis renamedreinstalled. The function now
requires aclientLogStoreparameter -- the enrolled client's root-invoked
account-log store -- since the credential's own bridge delegation may be
signed by the very ladder VM the strike removes, and the strike-and-reinstall
pair publishes through it rather than through the bridge. Freewallet FW-356.ensureCredentialClientAnnexGeneration's pointer-moving arms reveal a merely
committed rung (revealLadderRungWebvh) inside one conflict retry before the
pointer entry publishes; the revealed rung then stands inupdateKeys, an
accepted consequence.setDelegatedClientsPointerOnceis now exported, with
an optionalpublishedparameter: a caller that already read and attributed
the head it is building on (movePointerAsLadder) passes it through, so the
entry is built on exactly that head and a racing entry landing first surfaces
as aWebvhLogConflictErrorfor the outer retry to re-attribute from, rather
than as a plain not-authorized refusal. Freewallet FW-356.ensureCredentialClientAnnexGenerationrenews the unlock record's bridge
delegation, the fifth durable state a transient visit can mend. On a
client-less account no remembered login runs the login-time bridge refresh, so
the credential's one write path into the account log would simply lapse; the
ladder VM stands on exactly those accounts and signs the replacement. The
renewal runs before every arm and asks the same two-axis staleness predicate
thedelegatedClientssibling asks (expiry viazcapExpiring, signer rot via
delegationKeyInDocumentundercapabilityDelegation), now one shared helper
rather than a copy per delegation. API changes: the ensure takes a required
delegation(the record's current bridge);idStoreis replaced by an
idStoreFor({ delegation })factory, so a pointer entry in either minting arm
rides a bridge the server still accepts;onRebindRecordtakes
{ delegation, delegatedClients }and is called whenever either was freshly
minted; the outcome gainsdelegation(the usable bridge) and
bridgeReminted. A re-seal that fails when only the bridge was renewed is
reported on the outcome'sbridgeResealErrorrather than thrown: the fresh
bridge already served the visit, so a remote hiccup must not deny a login.
Freewallet FW-389.
Fixed
attributeLadderInventory(/clientAnnex) now attributes a ladder from any
rung the recorded anchor has climbed to. A seedless walk recovers the rungs
behind the anchor from the log's own positional rules (decision 0007, read
backwards), so a ladder VM the transient recovery's add-and-retire entry
installed is claimed after a self-enrollment advanced the anchor past rung 0.
A third attribution arm claims a VM anestablishStandingUnlockre-run
reinstalled under a fresh ladder seed.removeUnlockKeystrikes both instead
of reporting themunclaimed. One shape stays out of seedless reach: a VM the
last-client transition reinstalled, whose acting rung a later self-enrollment
spends, since the entry committing the next rung's hash authorizes no key for
the backward walk to read (WC-158). WC-155.- Both recovery continuations (
recoverWebvhClientand
recoverWebvhLadderAnchored) now strike each retired pre-recovery
credential's committed rung hashes, and any rung of its own left revealed in
updateKeys, in the same add-and-retire entry that strikes its ladder VM and
...