The wallet's behaviour lives in one package, @wellknown-id/wallet, as a service: unlocking, personas, bonds between a holder's devices, sync, contacts, custodians and legacy, sharing, delegation, signing in for another device, and checking the pages wellknown.id serves. An app makes it once, from adapters for its platform, and draws screens over it. kivi, on Android, iOS and the web, is such an app: it keeps no state and makes no decisions of its own, and a lint rule stops its screens from importing anything from the package but the service, its types and a few pure functions.
Status: the package is TypeScript and runs in browsers, on Hermes (React Native) and in Node. It isn't published to npm yet, and its API may still change. SDKs in kettu, for other languages, are planned.
Making the service
import { createWalletService, type ServicePorts } from '@wellknown-id/wallet/service';
const ports: ServicePorts = { keys, credentials, gate, gateStatus, holderState, bonds, relay, store, drops, wake, passkeys, device, say };
const wallet = createWalletService(ports);
wallet.open((ask) => showToHolder(ask)); // the app is in front: listen for bonded devices; put to the holder what only they can answer
wallet.on('bonds', redraw); // what changed: 'log', 'bonds', 'offers', 'pages', 'holder', 'records'
// …
wallet.close(); // the app went to the background
Make it once per app: a second service over the same storage would answer the same bonds twice. Two services over the same storage behave like the same device restarted, which is how the package's own tests kill a device mid-step and check that nothing is lost.
The ports
Everything the service needs from the platform, and nothing else (ServicePorts):
| Port | What it is | kivi's adapters |
|---|---|---|
keys | the device's identity key and credential keys | Keystore, the Secure Enclave; WebCrypto |
credentials | documents, secrets' notes, consents | files; IndexedDB |
gate, gateStatus() | the biometric gate (a browser: a non-extractable key, no prompt) | kivi-keys; WebCrypto |
quiet? | what's sealed without a prompt, on a phone | kivi-keys |
holderState | the holder's state on this device | a file; localStorage |
bonds | bonds (with an atomic update), the sync log, offers held | files; IndexedDB |
relay | the relay's mailboxes, over WebSocket | a WebSocket relay |
store, drops | the encrypted store and drop boxes | HTTPS to the relay |
wake(bond) | a content-free push to a bonded phone | the relay's wake-up |
passkeys | unlock, create, and tell providers which passkeys open the keyring | Credential Manager, AuthenticationServices; WebAuthn |
push? | this device's push token | expo-notifications |
device | what this device calls itself | |
release? | the release watch's setup, fetch and saved state | |
registry? | where sites find the wallet's credentials (the Digital Credentials API) | Credential Manager on Android |
say | the service's words, in the holder's language | the app's translations |
clock?, fetch? | the platform's own unless given |
packages/wallet/src/adapters has in-memory, WebCrypto, IndexedDB and WebSocket adapters to start from.
What it offers
The service's commands are grouped by what they're about: holder (unlocking, personas, recovery codes, records), bonds (pairing, joining, giving and taking back personas), sync, offers (secrets kept on another device), contacts, custodians, legacy, shares, grants, groups, signIn and give (for another device), pages (the release watch), witnesses, and wallet, the core: documents, credentials, consents and secrets. Each is in the API reference, and the pure helpers a screen may use are in @wellknown-id/wallet/view.
All of the holder's data the service keeps off the device is end to end encrypted, under keys only the holder's devices can derive, filed under ids nobody can link to them; the relay in between sees only ciphertext.