Shared wallet-domain logic (WAS sync engine core and wallet Space layout contracts) for Interop wallet apps.
@interop/wallet-core is the shared, correctness-critical code two WAS-enabled
wallet apps (a React Native mobile wallet and a browser wallet) hold in common:
the cross-replica byte-compatibility surface both must agree on to converge on
identical bytes. It is isomorphic (browser, Node.js, React Native) and has no UI
or storage dependencies -- side effects are injected, and the dep-heavier
protocol subpaths are import-directly-only.
The subpaths:
-
@interop/wallet-core/sync-- the Wallet Attached Storage (WAS) replication engine core: theSyncEngineorchestration (single-flight, migrate-once, memoized provisioning invalidated on demand viainvalidateProvisioning, backoff), therunPull/runPushalgorithms, the replica-sideSyncStoreseam, and the genericSyncedCollectionSpecshape. The wire contract and port (PwsSyncPort,WireDoc,DocCipher, ...) are re-exported from@interop/pws-clientso an engine consumer imports one package. -
@interop/wallet-core/space-- the wallet Space layout contract: the shared collection ids and descriptive specs (private-credentials,public-credentials,wallet-activity,app-keys), thewallet-activitywire shape with its pureaddHistory*payload builders, thepublicCredentialUrlderivation, and thepws-linkQR hand-off contract (buildPwsLinkPayload/parsePwsLinkPayload/encodePwsLinkSecret) with its structural recognizer,isPwsLinkPayload. It also carries theinboxcollection spec and the inbox message envelope (inboxGrantMessage/parseInboxGrantMessage). Contacts collection specs live in@interop/social-core. -
@interop/wallet-core/webvh-- the account's did:webvh identity: the hosted DID log, its per-client update-key rotation, the client enrollment entries, the one-entry client-revocation edit (verification methods, update key, and standing commitments out in a single entry, the staged commitment recovered by log attribution), the enrolled-client listing over a caller-verified log (for a "your wallets" surface), the fetch-and-verify-the-published-log step those ceremonies share (and its discovery twin, which takes a Space's owner from the log published there), the PWS-backed store they write through, ZCap signing under the did:webvh verification-method id, and a standing credential's update-key ladder (the rung and ladder-VM derivation, and the attribution walks that recover a credential's acting rung and standing inventory from the log). -
@interop/wallet-core/resourceLog-- the wallet-domain half of the Resource Log Profile, the hash-linked log format governing key resources co-managed between a wallet's clients and the storage server. Three pieces. The did:webvh controller adapter, which answers with the verified account document at a given version (the verifier dereferences each proof's method against it) and supplies the admission hook every verifier consults. The ceremony-tail license that hook carries, which bounds what a standing credential's ladder key may append -- above all refusing a silent rekey against an unchanged document. And a re-export of@interop/vh-resource-log'sisResourceLogRefusal, the shared reading of the refusal taxonomy: which refusals a reader must not paper over with a cached copy, and which one it may (a chain-head rollback, reconcilable divergence). The adapter and the license read the account document through one leaf beside them: ladder-VM recognition, the credential class, and the two key-class readers, over the library's relation resolution. They therefore answer exactly as the client listing does. That leaf's public home is thewebvhsubpath. The generic half lives in@interop/vh-resource-log-- chain verification against an adversarial host, the chain-head pin, the entry builders, the read/append/create path, and the sealing sweep. Transport is in@interop/pws-client's/logsubpath, and the hashing and proof kernel in@interop/did-method-webvh. -
@interop/wallet-core/keys-- the user key and its wrap-set roster, governed by thekey-map/user-key.jsonlresource log: minting, the roster's init/read/rotate primitives with their client-side guards (the verified log itself, the latest-seen epoch pin, the document-backed recipient resolver), the log-governed (and sealable) descriptor store those primitives drive, and the user key rotation cascade's per-collection op (re-epoch a collection onto the roster's current user key, staleness detected from durable state alone, history escrowed -- also the completion sweep's building block). The decisions behind that op and the roster convergence the login sweep runs are exported as pure predicates,userKeyRosterDriftandcollectionEpochsCurrent. AlsoensureWalletSpaceEpochs, the provision-time install of each encrypted wallet collection's key epoch[0] (a fresh random epoch key wrapped to the user key) -- the EDV-bearing second step ofprovisionWalletSpace. AlsowalletSpaceProvisioner, which builds the sync engine'sensureProvisionedclosure over both steps, single-flight across concurrent callers. Also the client-key record codec: the contents and strict validation of the local record each wallet client keeps its own key material in (storage and wrapping stay app-side). -
@interop/wallet-core/clients-- the enrolled-client management surface: the listing over the locally verified did:webvh log with names joined from theconnectionsdirectory's wallet-client entries, the disconnect-eligibility policy as pure functions, the revocation cascade orchestrator (document edit, roster rotation with its seal backstop, collection fan-out, optional recovery re-mints), and the login-time roster policy (which now also seals a converged roster's governing log). -
@interop/wallet-core/descriptors-- the log-governed descriptor source: every read re-verifies the collection's governing resource log, keyed per collection bycollectionDescriptorLogPinId. It implements theEncryptionDescriptorSourceseam of@interop/pws-client/edv, where the acquisition, cache fallback, unknown-epoch refresh policy, and self-refreshing EDV document cipher live. -
@interop/wallet-core/keyring-- the unlock layer: the unlock derivation, the signed{ version, encryption, wrapped, proof }account-pointer record codec (the proof is verified before the record is decrypted), and the unlock Space lifecycle. -
@interop/wallet-core/connections-- theconnectionsdirectory, one encrypted entry per party the wallet has dealt with (an app, an agent, a wallet client, a contact, a replica server): the entry codec, which reads leniently and keeps unknown members; the resource id keyed by the collection's blinded-index key; the PWS-backed store over the documents feed; the grant index a revocation reads, with the checks that drop a grant delegated to another party or targeting another Space; the compare-and-swap upsert helpers (recordGrants,removeGrants,retireConnection,unretireConnection,setConnectionLabel); the replica-server helpers over areplicaslist (recordReplicas,recordReplicaGrant,settleReplicaRegistration,setReplicaMarker,clearReplicaMarker,removeReplica); and the writer arm on a wallet client's entry (lazy registration, the throttled touch, the per-entry sweep, and the join a history view resolves a revision'swriterIdthrough). An agent's entry also carries the wallet's side of the connection: the pairwise seed and its tag (connectionDidKeyderives the did:key), the grants the agent handed the wallet, and the outbox of pending messages. Their readers (receivedGrants,liveInboxChannel,agentGrantDue, ...) and writers (recordReceivedGrants,recordRenewedGrants,settleOutboxItem,pruneSupersededGrants,dropReceivedGrant,clearReceivedGrants,markDeclined) live here too, as does the proof check a renewal runs over a recorded grant (verifyRecordedGrantProof, which verifies the delegation signature under a key the current document lists). The wallet writes every entry; no party ever writes one. -
@interop/wallet-core/audiences-- the audience collection: a plaintext collection in the owner's Space whose posts are its Resources. It carries the provisioning attributes every audience shares (AUDIENCE_PROVISION_ATTRIBUTES) and the member grant (delegateAudienceGrant, GET and HEAD on the container, with no key epoch to escrow). Nothing marks an audience on the wire. -
@interop/wallet-core/enrollment-- the client enrollment ceremony (connect code, approval, completion) plus the onboarding-response envelope that carries a connect code back over an exchange. -
@interop/wallet-core/genesis-- the account-genesis ceremony: the local mint of a brand-new account's complete key set (mintAccountKeySet: Space id, client identity seed, user key, did:webvh update keys) and the staged provisioning both apps must encode identically (ensureAccountGenesis: Space provisioning, the optional KMS key-map acquisition, did:webvh genesis, user-key roster genesis after DID publication, epoch[0] on every encrypted roster collection, and the Space-controller promotion, also exported standing alone asensurePromotedSpaceController). Idempotent end to end: a torn run heals by re-running. The keyring bind is deliberately not a stage, so a wallet with no unlock method bound at creation drives the same ceremony. -
@interop/wallet-core/unlock-- standing unlock credentials: every unlock method (passphrase, passkey PRF, recovery code) as a standing credential in the recovery-code configuration, with self-enrolling login. The credential-derived client identity and binding MAC key, the update-key ladder (latent-and-consumed did:webvh update authority from a random seed carried in the unlock record, the current rung recovered from the log itself), the unlock record codec (shell / bridge delegation / ladder members under a credential-authenticated binding, with the bridge-only re-mint), the merged document-inventory edit (a verbatimkeyAgreemententry or apublicKeyCommitmententry for a low-entropy-derived key, either naming its ladder's rung-0 hash asladderCommitment), and the self-enrolling continuation with its composed completion. -
@interop/wallet-core/recovery-- recovery codes on the roster identity model, over theunlockmachinery: a code as a minimal always-enrolled wallet client (format and derivation, the document half of issuance / revocation / recovery with its spend-on-use continuation, the pre-minteddid.jsonldelegation builder and the revocation cascade's bridge re-mint core; the record codec is theunlocksubpath's, re-exported here). -
@interop/wallet-core/clientAnnex-- the client annex, the sibling did:webvh log holding per-visit transient client keys in garbage-collected generations, published in the account's auxiliary annex Space. This is the authoring and maintenance surface of everything anchored on an unlock credential's update-key ladder: a credential's static annex rung (the ladder's derivation and attribution walks are thewebvhsubpath's), the annex log and its GC, ZCap signing under a ladder VM, the ladder-anchored account-log ceremonies (genesis, self-enrollment, forget, and the last-client transition to a ladder-anchored account), the credential-anchored account genesis with its mend and its per-visit readiness ensure, the transient-recovery continuation, and the replica grants (the pull delegation a replica server copies a Space under and the maintenance grant on a promoted replica Space's replicas route). It sits on top of the other subpaths and none of them import from it. -
@interop/wallet-core/menders-- the mender registry keyed by invariant: theInvariantDeclarationandRegistrationtypes, the closed vocabularies (authorities, triggers, account shapes, evidence, mend outcomes, gap kinds), theINVARIANT_IDScensus, themenderRegistryreaders withheldAuthorities, therunMenderBlockrunner with itsmendReportAccumulator, and the derived sets a wallet's audit tests pin (transientReachableInvariants,deriveGaps,undeclaredGaps,undeclaredInvariants). A wallet declares its own invariant table and its own registrations; nothing here executes a ceremony, and the subpath imports nothing from the signing or KMS graph.
The request pipeline (input classification, VPR parsing, cryptosuite
negotiation, VP composition, the App Connect app-key credential) now lives in
@interop/wallet-request.
The byte-for-byte WAS identity derivation (agentsFromSecret / agentsFromSeed
/ agentsFromKeyAgent, singleKeyResolver, zcapClientForSigner) now lives at
the @interop/pws-client/identity
subpath.
- Node.js 24+ is recommended.
pnpm install @interop/wallet-core
git clone https://github.com/interop-alliance/wallet-core.git
cd wallet-core
pnpm install
import { SyncEngine, runPull, runPush } from '@interop/wallet-core/sync'
import {
PRIVATE_CREDENTIALS_COLLECTION,
publicCredentialUrl,
buildPwsLinkPayload,
parsePwsLinkPayload,
addHistoryCredentialCreated
} from '@interop/wallet-core/space'The sync and space subpaths are re-exported from the package root as well.
Every other subpath (webvh, resourceLog, keys, clients, descriptors,
keyring, connections, audiences, enrollment, genesis, unlock,
recovery, clientAnnex, menders) is import-directly-only, so consumers of
the root never pull the signing / KMS / document-loader dependency graph.
Nine further exports are leaves of that same isolation, carved out to stay
dependency-light, for an offline consumer such as @interop/wallet-backup that
derives clients and opens records without ever touching the account log:
keys/clientKeyRecord-- the client-key record codec alone.keyring/kdf-- the unlock KDF.keyring/recordEnvelope-- the record envelope's descriptor mint, ciphers, addressed id, and frame parsers.keys/userKey-- the user key's key-agreement half.keys/userKeyGenerations-- the user-key-generation unwrap helpers.unlock/standingClient-- the standing client derivation.webvh/ladderDerivation-- the update-key rung and ladder-VM derivation.recovery/recoveryCode-- the recovery code codec and its client derivation.space/collections-- the wallet Space collection ids, specs and resource names, without thespacebarrel's transport graph.
None of these leaves reach wallet-core's own webvh, resourceLog, or
clientAnnex modules. The module barrels still re-export the same names.
The testing subpath holds test fixtures only: the recorded-grant signer
fixture, FAST_KDF (an HKDF stand-in for the shipped Argon2id unlock KDF), and
the shared account builder, which takes a booted WAS server's URL and produces a
real account through the package's own ceremonies in four named shapes (one
passphrase, a second standing credential, an issued recovery code, one enrolled
client). It imports no server package. Consumers restrict
@interop/wallet-core/testing to their test globs with a lint rule.
PRs accepted. See CONTRIBUTING.md for editor setup (Prettier, ESLint, and EditorConfig) and how it maps to CI.
If editing the Readme, please conform to the standard-readme specification.
MIT License © 2026 Interop Alliance.