The ultimate web3 experience: unlock wallets and sign transactions with recoverable passkeys in a breeze.
Organigram Passkey Wallet is a lightweight Ethereum wallet powered by WebAuthn passkeys. It gives users the day-to-day experience they expect from modern passwordless apps while preserving the core property that matters in web3: fully and safely owning a standard Ethereum address.
No cumbersome smart account deployment. No per-network account ceremony. No custodial key service. No protocol lock-in. Just a standard EOA, encrypted client-side, unlocked with rotative passkeys, and exposed via Wagmi and Rainbowkit.
pnpm add @organigram/passkey-wallet
Most web3 onboarding still asks users to understand browser extensions, seed phrases, hardware devices, account abstraction, gas sponsorship, recovery contracts, network-specific deployments, signer permissions... before they have even used the product. Organigram Passkey Wallet takes a simpler path.
The user creates a wallet in the browser. The wallet is a normal Ethereum EOA derived from a recovery phrase. That recovery phrase is encrypted locally with a key derived from a WebAuthn passkey PRF output, then stored only as an encrypted vault envelope. When the user returns, their passkey unlocks the vault and the EOA signs messages or transactions like any other wallet.
The result is a wallet experience that feels native to the web:
Users unlock the wallet with a WebAuthn passkey. Once unlocked, the wallet signs messages, typed data, and transactions with the underlying EOA.
The wallet exposes the same interface dapps already expect from injected wallets, WalletConnect wallets, and RainbowKit connectors. It supports account requests, chain switching, message signing, typed data signing, and transaction submission like any other standard wallet.
Users can add additional passkeys for the same address. A backup phone, laptop, password manager, or security key can unlock the same encrypted wallet vault.
The address is a standard EOA, so it exists everywhere EVM accounts exist. There is no need to deploy account contracts across networks before the user can start.
Passkeys optimize daily use, while the capacity to export seed phrases preserves portability and gives users a disaster prevention path for the case where every passkey is lost.
The app exports an encrypted vault that can be saved to and restored from anywhere. Use remote servers, save locally or in a password manager without fear to leak sensitive information.
This package builds a production-ready app used to serve the wallet locally or on custom domains.
pnpm devpnpm build:artifact
build:artifact builds the TypeScript library to app-dist, and packages a deterministic
artifacts/passkey-wallet-app-v<version>.tar.gz with a .sha256 checksum and
manifest.
pnpm dev serves the wallet at https://localhost:3002, behind an HTTPS proxy
in front of Vite (port 3003). The first run creates a local certificate with
mkcert.
Any other hostname can be passed as an argument (pnpm dev my.host.test): the
certificate is regenerated to include it.
You do not have to trust the servers of wallet.organigram.ai, nor the code they
serve, to use your Organigram accounts. The local-organigram profile serves
your own copy of the wallet, built from this source code, on your machine:
pnpm dev local-organigram
(or pnpm passkey-wallet dev local-organigram from the repository root), then
open https://local.wallet.organigram.ai:3002.
local.wallet.organigram.ai is a subdomain of wallet.organigram.ai, so this
copy unlocks the same passkeys, hence the same accounts: the profile sets the
passkey RP ID to wallet.organigram.ai. Its browser storage is separate, so
restore your encrypted vault there once (see Recoverable vaults):
your passkey then unlocks it as usual. Your keys never leave your browser, and
signing in no longer depends on wallet.organigram.ai being online or unaltered.
The name already resolves to 127.0.0.1 through public DNS, so no setup is
needed. For a stronger guarantee, also resolve it in your hosts file: a tampered
DNS record could otherwise send it to a server able to request the PRF output of
your passkeys (macOS and Linux):
grep -q 'local.wallet.organigram.ai' /etc/hosts || echo '127.0.0.1 local.wallet.organigram.ai' | sudo tee -a /etc/hosts
To use it from any app, turn on Settings → Your own wallet on wallet.organigram.ai. Requests then go to your copy, and once it has answered, apps open it directly: wallet.organigram.ai is no longer loaded at all. If your copy stops answering, for instance because it is not running, the request goes back to wallet.organigram.ai after a few seconds.
Apps that use this package accept the local copy of the official wallet without any setting, on their pages and on their wallet endpoints (sign-in challenge, vault backups, RPC).
A wallet deployed elsewhere can offer the same to its users with its own local copy, served under a subdomain of its host so that it unlocks the same passkeys:
localWalletOrigin in
passkey-wallet.config.json (or PASSKEY_WALLET_LOCAL_WALLET_ORIGIN at build
time), for example https://local.wallet.example.com:3002;PASSKEY_WALLET_PASSKEY_RP_ID=wallet.example.com pnpm dev local.wallet.example.com;NEXT_PUBLIC_ORGANIGRAM_PASSKEY_WALLET_LOCAL_URL, next to
NEXT_PUBLIC_ORGANIGRAM_PASSKEY_WALLET_URL. The value is ignored unless it is
an HTTPS subdomain of the wallet host.Each production build of https://wallet.organigram.ai is published as a release of this repository, with the SHA-256 of every served file. To check that the live wallet serves one of these releases unchanged, run from a clone of this repository:
node scripts/verify-runtime-dist.mjs --url https://wallet.organigram.ai
With Node.js 24 and pnpm 10.28, rebuilding a release commit reproduces its files:
pnpm install --frozen-lockfilepnpm run buildnode scripts/verify-runtime-dist.mjs --dist app-dist --manifest <release manifest>
Browser WebAuthn passkey with PRF | v HKDF-derived vault key | v AES-GCM encrypted wallet vault envelope | v Recovery phrase -> EOA -> EIP-1193 providerServer WebAuthn challenges Credential public keys Credential counters Encrypted vault envelopes
In practice, the flow is:
residentKey: "required",
userVerification: "required", and the prf extension.The private wallet material is generated and decrypted in the browser. The server stores only WebAuthn verification material and encrypted vault envelopes. The server never needs the seed phrase, the private key, or the decrypted user vault.
Create and unlock vaults in the UI with wallet helpers:
import { createGeneratedRecoveryPhrase, registerBrowserPasskeyVault, unlockBrowserPasskeyVault} from '@organigram/passkey-wallet/browser-wallet'const registration = await registerBrowserPasskeyVault({ recoveryPhrase: createGeneratedRecoveryPhrase(), name: 'Account 1'})const unlocked = await unlockBrowserPasskeyVault({ records: [registration.record]})const signature = await unlocked.wallet.account.signMessage({ message: 'gm'})
The passkey ceremony and vault decryption happen in the wallet origin. Dapps should integrate through the remote connector.
import { connectorsForWallets } from '@rainbow-me/rainbowkit'import { createConfig, http } from 'wagmi'import { mainnet, sepolia } from 'wagmi/chains'import { createOrganigramRemotePasskeyWallet} from '@organigram/passkey-wallet/remote-rainbowkit'const passkeyWallet = createOrganigramRemotePasskeyWallet({ walletOrigin: 'https://wallet.organigram.ai'})export const wagmiConfig = createConfig({ chains: [mainnet, sepolia], connectors: connectorsForWallets( [ { groupName: 'Recommended', wallets: [() => passkeyWallet] } ], { appName: 'Your App', projectId: 'walletconnect-project-id' } ), transports: { [mainnet.id]: http(), [sepolia.id]: http() }})
From the dapp's perspective, this behaves like a wallet. Calling
personal_sign, eth_signTypedData_v4, or eth_sendTransaction goes through
the wallet origin, where the user approves the request.
The wallet app is static. A server is optional and should only receive encrypted backup packages or dapp sign-in challenges. It should not create WebAuthn credentials, unlock vaults, or receive decrypted wallet material.
A passkey wallet should never depend on a single authenticator.
Organigram Passkey Wallet supports adding another passkey to the same wallet by re-encrypting the same vault payload for a new credential. The address does not change. The user can keep using the same EOA while adding a laptop passkey, a phone passkey, a password manager passkey, or a hardware security key.
Rotation is the same idea in reverse: remove obsolete credentials only when another recovery method remains. The package gives you the wallet and envelope building blocks; your product should decide the UX policy for deletion, confirmation, and minimum backup requirements.
Passkeys make daily wallet use simple, but users still need an escape hatch.
Organigram Passkey Wallet keeps the wallet portable by deriving the EOA from a standard recovery phrase. If every passkey is lost, the user can recover the account with the seed phrase, or use it in any other wallet.
The recovery phrase should always be exported before the user loses all passkeys. The package provides a helper to export a seed phrase after a successful WebAuthn ceremony, but cannot do so after all access to the vault is lost.
Organigram Passkey Wallet is non-custodial by design.
The browser creates the wallet. The browser encrypts the vault. The browser decrypts the vault after a successful WebAuthn ceremony. The server stores encrypted envelopes and verifies that WebAuthn responses are valid for the expected challenge, origin, RP ID, and credential public key.
This model assumes:
Passkeys are created for the exact hostname serving the wallet (for example
wallet.organigram.ai). Any page served under the passkey RP ID can request
the PRF output that decrypts the vault, so no other application should be
hosted under it. Dapps never need the wallet RP ID: they reach the wallet
through its popup and postMessage.
A wallet served from a subdomain can reuse the passkeys of its parent wallet
host by setting passkeyRpId in passkey-wallet.config.json (or
PASSKEY_WALLET_PASSKEY_RP_ID) to that host. The value must be the current
hostname or one of its parent domains. pnpm dev local-organigram does so for
wallet.organigram.ai (see Run your own wallet).
The wallet sends a Content-Security-Policy that forbids third-party scripts
and any framing of the wallet origin.
The passkey vault depends on the WebAuthn prf extension. Browsers or password
managers that do not return PRF output cannot unlock the encrypted vault.
Apps should present a clear fallback when PRF is unavailable:
Organigram Passkey Wallet is not a smart account framework. It does not require 4337 infrastructure, paymasters, bundlers, module registries, or per-chain deployments.
It is not a custodial wallet. The server does not receive the recovery phrase or private key in clear text.
It is not a hosted wallet service. The package is open source; the host app owns persistence, authentication, and backup policy.
It is not a replacement for hardware wallets in high-assurance custody flows. It is designed for the safest mainstream web UX possible while staying portable and EVM-compatible.
MIT
Apps reach the wallet through createOrganigramRemotePasskeyWallet(), a RainbowKit wallet whose wagmi connector opens the wallet in a popup. Each connection, signature, transaction or encryption is a request that the user approves in the popup. Apps exchange requests and answers with the popup through postMessage, and only accept answers from the wallet origin and its local copy. Signatures are checked against the connected address and chain before they reach the app. Private keys never leave the wallet origin.
Input of createOrganigramRemotePasskeyWallet().
export type CreateOrganigramRemotePasskeyWalletInput = { walletOrigin?: string; requestConnect?: RemoteRequestConnect; requestPersonalSign?: RemoteRequestPersonalSign; requestSignTypedData?: RemoteRequestSignTypedData; requestSendTransaction?: RemoteRequestSendTransaction;};
A wagmi connector, identified by its id.
export type HydrateOrganigramRemoteWalletConnectionConnector = { id: string;};
An address already known to the app, for example from its session.
export type HydrateOrganigramRemoteWalletConnectionInput = { address: `0x${string}`; chainId?: number;};
Input of hydrateOrganigramRemoteWalletConnection().
export type HydrateOrganigramRemoteWalletConnectionOptions<TConnector extends HydrateOrganigramRemoteWalletConnectionConnector> = HydrateOrganigramRemoteWalletConnectionInput & { connectors: readonly TConnector[]; connectAsync: (input: { connector: TConnector; }) => Promise<unknown>; defaultChainId?: number;};
A connection to the wallet remembered in the local storage of the app.
It only holds the public address and chain, never a key: every signature still asks the user in the wallet popup.
export type OrganigramRemotePasskeyWalletConnection = { version: 1; address: `0x${string}`; chainId: number; connectedAt: string;};
Adds presentation-only context to the next wallet approvals. This lets an app tell the wallet that one user action requires several signatures, without broadening any approval or altering its signed payload.
beginOrganigramWalletApprovalSequence: (input: { count: number; label?: string;}) => (() => void)
Forget the remembered connection to the wallet.
clearOrganigramRemotePasskeyWalletConnection: () => void
Create the RainbowKit wallet of the passkey wallet, which opens the wallet in a popup.
The connector exposes an EIP-1193 provider. Account and signing requests open the wallet popup, and every signature is checked against the connected address and chain. Other JSON-RPC requests go to the default RPC URL of the active chain.
const connectors = connectorsForWallets( [{ groupName: 'Organigram', wallets: [() => createOrganigramRemotePasskeyWallet()] }], { appName: 'My app', projectId })
createOrganigramRemotePasskeyWallet: (input?: CreateOrganigramRemotePasskeyWalletInput) => Wallet
Parameters
| Name | Type | Description |
|---|---|---|
input | CreateOrganigramRemotePasskeyWalletInput | The origin of the wallet, and replacements of its requests. |
Read the remembered connection to the wallet.
getOrganigramRemotePasskeyWalletConnection: () => OrganigramRemotePasskeyWalletConnection | null
Returns
| Type | Description |
|---|---|
OrganigramRemotePasskeyWalletConnection | null | The connection, or null if none is valid. |
Connect wagmi to the wallet for an address the app already knows, without opening the wallet popup.
Remembers the connection, then connects the wallet connector, which restores it. If connecting fails, the connection is forgotten.
hydrateOrganigramRemoteWalletConnection: <TConnector extends HydrateOrganigramRemoteWalletConnectionConnector>(input: HydrateOrganigramRemoteWalletConnectionOptions<TConnector>) => Promise<void>
Remember a connection to the wallet in local storage.
rememberOrganigramRemotePasskeyWalletConnection: (input: { address: `0x${string}`; chainId: number;}) => void
Parameters
| Name | Type | Description |
|---|---|---|
input | { address: `0x${string}`; chainId: number; } | The connected address and chain. |
Identifier of the wagmi connector of the wallet, organigramPasskeyWalletId.
const organigramRemotePasskeyWalletId = "organigram-passkeys"
Input of requestPasskeyWalletConnect().
export type PasskeyWalletConnectPopupInput = PasskeyWalletRemotePopupInput;
A request to connect an app to the wallet, which answers with the selected address.
export type PasskeyWalletConnectRequest = PasskeyWalletRemoteRequestBase & { type: typeof passkeyWalletConnectType;};
The answer to a connect request, with the address the user selected.
export type PasskeyWalletConnectResult = PasskeyWalletRemoteResultBase & { type: typeof passkeyWalletConnectResultType;};
A request to decrypt a text encrypted for the origin of the app.
export type PasskeyWalletDecryptPayloadRequest = PasskeyWalletRemoteRequestBase & { type: typeof passkeyWalletDecryptPayloadType; address: `0x${string}`; encryptedPayload: WalletEncryptedPayload;};
The answer to a decrypt payload request.
export type PasskeyWalletDecryptPayloadResult = PasskeyWalletRemoteResultBase & { type: typeof passkeyWalletDecryptPayloadResultType; plaintext: string;};
A request to encrypt a text with the wallet encryption key, for the origin of the app.
export type PasskeyWalletEncryptPayloadRequest = PasskeyWalletRemoteRequestBase & { type: typeof passkeyWalletEncryptPayloadType; address: `0x${string}`; plaintext: string;};
The answer to an encrypt payload request.
export type PasskeyWalletEncryptPayloadResult = PasskeyWalletRemoteResultBase & { type: typeof passkeyWalletEncryptPayloadResultType; encryptedPayload: WalletEncryptedPayload;};
The answer to a request the wallet could not serve.
export type PasskeyWalletRemoteErrorResult = Omit<PasskeyWalletRemoteResultBase, 'address'> & { type: typeof passkeyWalletRemoteErrorResultType; address?: undefined; error: string;};
Common input of the requests to the wallet popup.
export type PasskeyWalletRemotePopupInput = { walletOrigin?: string; appOrigin?: string; chainId: number; requestId?: string; requestedAt?: string; approvalStep?: number; approvalCount?: number; approvalLabel?: string;};
A request an app can send to the wallet.
export type PasskeyWalletRemoteRequest = PasskeyWalletConnectRequest | PasskeyWalletSignMessageRequest | PasskeyWalletSignTypedDataRequest | PasskeyWalletSendTransactionRequest | PasskeyWalletEncryptPayloadRequest | PasskeyWalletDecryptPayloadRequest;
The message by which an app posts a request to the wallet popup.
export type PasskeyWalletRemoteRequestPayloadMessage = { type: typeof passkeyWalletRemoteRequestPayloadType; version: 1; request: PasskeyWalletRemoteRequest;};
Types of the requests an app can send to the wallet.
export type PasskeyWalletRemoteRequestType = typeof passkeyWalletConnectType | typeof passkeyWalletSignMessageType | typeof passkeyWalletSignTypedDataType | typeof passkeyWalletSendTransactionType | typeof passkeyWalletEncryptPayloadType | typeof passkeyWalletDecryptPayloadType;
An answer of the wallet to a request.
export type PasskeyWalletRemoteResult = PasskeyWalletConnectResult | PasskeyWalletSignMessageResult | PasskeyWalletSignTypedDataResult | PasskeyWalletSendTransactionResult | PasskeyWalletEncryptPayloadResult | PasskeyWalletDecryptPayloadResult | PasskeyWalletRemoteErrorResult;
A request to sign and send a transaction.
export type PasskeyWalletSendTransactionRequest = PasskeyWalletRemoteRequestBase & { type: typeof passkeyWalletSendTransactionType; address: `0x${string}`; transaction: PasskeyWalletTransaction;};
The answer to a send transaction request.
export type PasskeyWalletSendTransactionResult = PasskeyWalletRemoteResultBase & { type: typeof passkeyWalletSendTransactionResultType; transactionHash: `0x${string}`;};
Input of requestPasskeyWalletSignMessage().
export type PasskeyWalletSignMessagePopupInput = PasskeyWalletRemotePopupInput & { address: `0x${string}`; message: string;};
A request to sign a message with EIP-191 personal_sign.
export type PasskeyWalletSignMessageRequest = PasskeyWalletRemoteRequestBase & { type: typeof passkeyWalletSignMessageType; address: `0x${string}`; message: string;};
The answer to a sign message request.
export type PasskeyWalletSignMessageResult = PasskeyWalletRemoteResultBase & { type: typeof passkeyWalletSignMessageResultType; message: string; signature: `0x${string}`;};
Input of a request to sign typed data.
export type PasskeyWalletSignTypedDataPopupInput = PasskeyWalletRemotePopupInput & { address: `0x${string}`; typedData: PasskeyWalletTypedData;};
A request to sign EIP-712 typed data.
export type PasskeyWalletSignTypedDataRequest = PasskeyWalletRemoteRequestBase & { type: typeof passkeyWalletSignTypedDataType; address: `0x${string}`; typedData: PasskeyWalletTypedData;};
The answer to a sign typed data request.
export type PasskeyWalletSignTypedDataResult = PasskeyWalletRemoteResultBase & { type: typeof passkeyWalletSignTypedDataResultType; typedData: PasskeyWalletTypedData; signature: `0x${string}`;};
A transaction to send, with quantities in hex.
export type PasskeyWalletTransaction = { from: `0x${string}`; to: `0x${string}`; data?: `0x${string}`; value?: `0x${string}`; gas?: `0x${string}`; gasPrice?: `0x${string}`; maxFeePerGas?: `0x${string}`; maxPriorityFeePerGas?: `0x${string}`; nonce?: `0x${string}`;};
EIP-712 typed data.
export type PasskeyWalletTypedData = { domain: Record<string, unknown>; types: Record<string, ReadonlyArray<{ name: string; type: string; }>>; primaryType: string; message: Record<string, unknown>;};
Check that a signed message comes from the expected address and chain.
Recovers the signer from the signature, so that a forged answer is rejected. This only works for EOA signatures, which the wallet produces.
assertPasskeyWalletSignMessageResult: (input: { result: PasskeyWalletSignMessageResult; address: `0x${string}`; chainId: number; message: string;}) => Promise<void>
Parameters
| Name | Type | Description |
|---|---|---|
input | { result: PasskeyWalletSignMessageResult; address: `0x${string}`; chainId: number; message: string; } | The answer, and the address, chain and message of the request. |
Check that signed typed data comes from the expected address and chain.
Recovers the signer from the signature, so that a forged answer is rejected.
assertPasskeyWalletSignTypedDataResult: (input: { result: PasskeyWalletSignTypedDataResult; address: `0x${string}`; chainId: number; typedData: PasskeyWalletTypedData;}) => Promise<void>
Parameters
| Name | Type | Description |
|---|---|---|
input | { result: PasskeyWalletSignTypedDataResult; address: `0x${string}`; chainId: number; typedData: PasskeyWalletTypedData; } | The answer, and the address, chain and typed data of the request. |
Build the URL of the wallet popup for a request.
Every request other than connect is then delivered by postMessage. The wallet reads the requesting origin from the browser-authenticated MessageEvent instead of trusting the appOrigin written in the URL, and no message, transaction or secret lands in a URL.
buildPasskeyWalletRemotePopupUrl: (input: PasskeyWalletRemotePopupInput & { type: PasskeyWalletRemoteRequestType; address?: `0x${string}`;}) => URL
Parameters
| Name | Type | Description |
|---|---|---|
input | PasskeyWalletRemotePopupInput & { type: PasskeyWalletRemoteRequestType; address?: `0x${string}`; } | The request, with its type and address. |
Check that a posted message is a valid request to the wallet.
The domain of the request must be the host of its appOrigin. The wallet must still compare appOrigin with the origin of the MessageEvent, which the browser authenticates.
isPasskeyWalletRemoteRequestPayloadMessage: (value: unknown, expectedRequestId?: string) => value is PasskeyWalletRemoteRequestPayloadMessage
Parameters
| Name | Type | Description |
|---|---|---|
value | unknown | The data of the message. |
expectedRequestId | string | The identifier of the expected request, if known. |
Check that a value is a transaction with valid addresses and hex quantities.
isPasskeyWalletTransaction: (value: unknown) => value is PasskeyWalletTransaction
Parameters
| Name | Type | Description |
|---|---|---|
value | unknown | The value to check. |
Read and check a request from the URL parameters of the wallet popup.
Rejects encrypt and decrypt requests, which must be delivered by postMessage, and requests whose domain is not the host of their appOrigin.
parsePasskeyWalletRemoteRequest: (params: URLSearchParams) => PasskeyWalletRemoteRequest
Parameters
| Name | Type | Description |
|---|---|---|
params | URLSearchParams | The URL parameters. |
Read typed data.
parsePasskeyWalletTypedData: (value: unknown) => PasskeyWalletTypedData
Parameters
| Name | Type | Description |
|---|---|---|
value | unknown | The typed data, or its JSON. |
Ask the wallet, in a popup, to connect the app.
requestPasskeyWalletConnect: (input: PasskeyWalletConnectPopupInput & { timeoutMs?: number;}) => Promise<PasskeyWalletConnectResult>
Parameters
| Name | Type | Description |
|---|---|---|
input | PasskeyWalletConnectPopupInput & { timeoutMs?: number; } | The request, and a timeout, 5 minutes by default. |
Returns
| Type | Description |
|---|---|
Promise<PasskeyWalletConnectResult> | The answer, with the address the user selected. |
Ask the wallet, in a popup, to decrypt a text encrypted for the origin of the app.
requestPasskeyWalletDecryptPayload: (input: PasskeyWalletRemotePopupInput & { address: `0x${string}`; encryptedPayload: WalletEncryptedPayload; timeoutMs?: number;}) => Promise<PasskeyWalletDecryptPayloadResult>
Parameters
| Name | Type | Description |
|---|---|---|
input | PasskeyWalletRemotePopupInput & { address: `0x${string}`; encryptedPayload: WalletEncryptedPayload; timeoutMs?: number; } | The request, the address, the encrypted payload, and a timeout, 5 minutes by default. |
Returns
| Type | Description |
|---|---|
Promise<PasskeyWalletDecryptPayloadResult> | The answer, with the decrypted text. |
Ask the wallet, in a popup, to encrypt a text for the origin of the app.
requestPasskeyWalletEncryptPayload: (input: PasskeyWalletRemotePopupInput & { address: `0x${string}`; plaintext: string; timeoutMs?: number;}) => Promise<PasskeyWalletEncryptPayloadResult>
Parameters
| Name | Type | Description |
|---|---|---|
input | PasskeyWalletRemotePopupInput & { address: `0x${string}`; plaintext: string; timeoutMs?: number; } | The request, the address, the text, and a timeout, 5 minutes by default. |
Returns
| Type | Description |
|---|---|
Promise<PasskeyWalletEncryptPayloadResult> | The answer, with the encrypted payload. |
Ask the wallet, in a popup, to sign a message.
The answer is checked with assertPasskeyWalletSignMessageResult().
requestPasskeyWalletSignMessage: (input: PasskeyWalletSignMessagePopupInput & { timeoutMs?: number;}) => Promise<PasskeyWalletSignMessageResult>
Parameters
| Name | Type | Description |
|---|---|---|
input | PasskeyWalletSignMessagePopupInput & { timeoutMs?: number; } | The request, the address, the message, and a timeout, 5 minutes by default. |
Returns
| Type | Description |
|---|---|
Promise<PasskeyWalletSignMessageResult> | The answer, with the signature. |
Serialize typed data in JSON, writing bigints as decimal strings.
stringifyPasskeyWalletTypedData: (typedData: PasskeyWalletTypedData) => string
Parameters
| Name | Type | Description |
|---|---|---|
typedData | PasskeyWalletTypedData | The typed data. |
Type of the answer to a connect request.
const passkeyWalletConnectResultType = "organigram:wallet:connect-result"
Type of a request to connect an app to the wallet.
const passkeyWalletConnectType = "organigram:wallet:connect"
Type of the answer to a decrypt payload request.
const passkeyWalletDecryptPayloadResultType = "organigram:wallet:decrypt-payload-result"
Type of a request to decrypt a text with the wallet encryption key.
const passkeyWalletDecryptPayloadType = "organigram:wallet:decrypt-payload"
Type of the answer to an encrypt payload request.
const passkeyWalletEncryptPayloadResultType = "organigram:wallet:encrypt-payload-result"
Type of a request to encrypt a text with the wallet encryption key.
const passkeyWalletEncryptPayloadType = "organigram:wallet:encrypt-payload"
Type of the answer to a request the wallet could not serve.
const passkeyWalletRemoteErrorResultType = "organigram:wallet:remote-error"
Type of the message by which an app posts a request to the wallet popup.
const passkeyWalletRemoteRequestPayloadType = "organigram:wallet:request-payload"
Type of the answer to a send transaction request.
const passkeyWalletSendTransactionResultType = "organigram:wallet:send-transaction-result"
Type of a request to send a transaction.
const passkeyWalletSendTransactionType = "organigram:wallet:send-transaction"
Type of the answer to a sign message request.
const passkeyWalletSignMessageResultType = "organigram:wallet:sign-message-result"
Type of a request to sign a message.
const passkeyWalletSignMessageType = "organigram:wallet:sign-message"
Type of the answer to a sign typed data request.
const passkeyWalletSignTypedDataResultType = "organigram:wallet:sign-typed-data-result"
Type of a request to sign EIP-712 typed data.
const passkeyWalletSignTypedDataType = "organigram:wallet:sign-typed-data"
The origins whose messages an app expecting walletOrigin accepts.
getAcceptedWalletOrigins: (walletOrigin: string) => string[]
The local copy of walletOrigin, if it has one.
getLocalWalletOrigin: (walletOrigin: string) => string | null
Only an HTTPS subdomain of the wallet host can unlock its passkeys.
isLocalWalletOriginOf: (localWalletOrigin: string, walletOrigin: string) => boolean
Sends a message to the wallet popup, whichever accepted origin it shows: the browser only delivers it to the one the popup is on.
postToWalletPopup: (popup: Window, message: unknown, walletOrigin: string) => void
Remembers which of the accepted origins answered for walletOrigin.
rememberWalletOrigin: (walletOrigin: string, answeringOrigin: string) => void
Opens requests on the local copy once it has answered on this device, so that the official wallet is no longer loaded at all.
toPreferredWalletUrl: (url: URL) => URL
Brings the popup back to the wallet when its local copy does not answer, for instance because it is not running. Returns the cleanup.
watchLocalWalletPopup: (input: { popup: Window; popupUrl: URL; walletOrigin: string; requestId: string; timeoutMs?: number;}) => (() => void)
Asks a wallet to serve a request itself.
const localWalletOptOutParam = "localWallet"
Origin of the local copy of the official wallet.
A wallet can have a local copy: the same wallet run on the user's own machine, served under a subdomain of the wallet host, so that it unlocks the same passkeys. An app that trusts the wallet then also trusts that copy. The official wallet's copy is pnpm dev local-organigram; another deployment declares its own through NEXT_PUBLIC_ORGANIGRAM_PASSKEY_WALLET_LOCAL_URL.
const officialLocalWalletOrigin = "https://local.wallet.organigram.ai:3002"
Origin of the official Organigram passkey wallet.
const officialWalletOrigin = "https://wallet.organigram.ai"
Posted by a wallet before it hands a request to its local copy.
const passkeyWalletLocalHandoffType = "organigram:wallet:local-handoff"
Posted by the local copy's request page as soon as it loads.
const passkeyWalletReadyType = "organigram:wallet:ready"
Users sign in to an app with Sign-In with Ethereum. The app issues a message with a nonce from its server, the wallet signs it in a popup, and the server verifies the signature, the domain and the nonce before opening a session. With NextAuth, signInWithPasskeyWallet() runs the whole flow in the browser, and authorizePasskeyWalletCredentials() verifies the result in a credentials provider.
Input of a sign-in request through the wallet popup.
export type PasskeyWalletPopupInput = { walletOrigin?: string; appOrigin?: string; nonce: string; chainId: number; requestId?: string; requestedAt?: string; popup?: Window | null;};
A message posted by the wallet to the app that opened it when the user revokes the session of that app.
export type PasskeyWalletStackSessionChangedMessage = { type: typeof passkeyWalletStackSessionChangedType; version: 1; event: 'revoked'; appOrigin: string; address: `0x${string}` | null;};
Input of signInWithPasskeyWallet().
export type SignInWithPasskeyWalletInput = { chainId?: number; walletOrigin?: string; getCsrfToken: () => Promise<string | null | undefined>; requestWalletSignIn?: (input: WalletSignInInput) => Promise<PasskeyWalletSignInResult>; submitCredentials?: SubmitCredentials;};
Build the URL of the wallet popup for a sign-in request, with the challenge URL /api/auth/wallet/challenge of the app.
buildPasskeyWalletSignInPopupUrl: (input: PasskeyWalletPopupInput) => URL
Parameters
| Name | Type | Description |
|---|---|---|
input | PasskeyWalletPopupInput | The sign-in request. |
Create a session change message.
createPasskeyWalletStackSessionChangedMessage: (input: { event: "revoked"; appOrigin: string; address?: `0x${string}` | null;}) => PasskeyWalletStackSessionChangedMessage
Parameters
| Name | Type | Description |
|---|---|---|
input | { event: "revoked"; appOrigin: string; address?: `0x${string}` | null; } | The event, the origin of the app and the address of the session. |
Tell NextAuth in the current tab that the session changed, so that it reloads it.
notifyCurrentTabNextAuthSessionChanged: () => void
Post a session change message to the window that opened the wallet.
notifyPasskeyWalletStackSessionChanged: (input: { event: "revoked"; appOrigin: string; address?: `0x${string}` | null; targetOrigin?: string;}) => void
Parameters
| Name | Type | Description |
|---|---|---|
input | { event: "revoked"; appOrigin: string; address?: `0x${string}` | null; targetOrigin?: string; } | The event, the origin of the app, the address of the session, and the origin to post to, the app origin by default. |
Read a session change message.
Also check the origin of the MessageEvent carrying it: only trust messages from the wallet.
parsePasskeyWalletStackSessionChangedMessage: (value: unknown) => PasskeyWalletStackSessionChangedMessage | null
Parameters
| Name | Type | Description |
|---|---|---|
value | unknown | The data of the message. |
Returns
| Type | Description |
|---|---|
PasskeyWalletStackSessionChangedMessage | null | The message, or null if it is invalid. |
Ask the wallet, in a popup, to sign a Sign-In with Ethereum message for the app.
Only accepts answers from the wallet origin and its local copy, for this request. The result is not verified: the server must verify it with verifyPasskeyWalletSiwe().
requestPasskeyWalletSignIn: (input: PasskeyWalletPopupInput & { timeoutMs?: number;}) => Promise<PasskeyWalletSignInResult>
Parameters
| Name | Type | Description |
|---|---|---|
input | PasskeyWalletPopupInput & { timeoutMs?: number; } | The sign-in request, and a timeout, 5 minutes by default. |
Returns
| Type | Description |
|---|---|
Promise<PasskeyWalletSignInResult> | The signed message. |
Sign in to a NextAuth app with the wallet.
Opens the wallet popup, uses the CSRF token as the nonce of the Sign-In with Ethereum message, and submits the signed message to the NextAuth credentials provider, which verifies it with authorizePasskeyWalletCredentials().
import { getCsrfToken } from 'next-auth/react'await signInWithPasskeyWallet({ getCsrfToken })
signInWithPasskeyWallet: (input: SignInWithPasskeyWalletInput) => Promise<PasskeyWalletSignInResult>
Parameters
| Name | Type | Description |
|---|---|---|
input | SignInWithPasskeyWalletInput | The CSRF token getter, the chain and the wallet origin. |
Returns
| Type | Description |
|---|---|
Promise<PasskeyWalletSignInResult> | The signed message. |
Type of the message telling an app that its session changed in the wallet.
const passkeyWalletStackSessionChangedType = "organigram:wallet:stack-session-changed"
Submit a signed message to the NextAuth credentials callback of the app, at /api/auth/callback/credentials.
const submitPasskeyWalletCredentials: SubmitCredentials
Input of authorizePasskeyWalletCredentials().
export type AuthorizePasskeyWalletCredentialsInput = { credentials: PasskeyWalletNextAuthCredentials | undefined; domain: string; nonce: string | null | undefined; chain?: Chain; transportUrl: string; transportOptions?: Parameters<typeof import('viem').http>[1];};
Credentials submitted to the NextAuth credentials provider.
export type PasskeyWalletNextAuthCredentials = { message?: unknown; signature?: unknown; csrfToken?: unknown;};
Verify the credentials submitted to a NextAuth credentials provider by signInWithPasskeyWallet().
Rejects credentials without message, signature or nonce, then verifies them with verifyPasskeyWalletSiwe(). The nonce is the CSRF token of the session, which signInWithPasskeyWallet() puts in the message.
CredentialsProvider({ credentials: { message: {}, signature: {} }, authorize: async credentials => await authorizePasskeyWalletCredentials({ credentials, domain: 'app.example.com', nonce: csrfToken, transportUrl: rpcUrl })})
authorizePasskeyWalletCredentials: (input: AuthorizePasskeyWalletCredentialsInput) => Promise<PasskeyWalletVerifiedSiwe | null>
Parameters
| Name | Type | Description |
|---|---|---|
input | AuthorizePasskeyWalletCredentialsInput | The credentials, the domain and nonce of the app, and an RPC endpoint. |
Returns
| Type | Description |
|---|---|
Promise<PasskeyWalletVerifiedSiwe | null> | The verified sign-in, or null. |
A Sign-In with Ethereum message issued by an app for the wallet to sign.
export type PasskeyWalletChallenge = { domain: string; nonce: string; issuedAt: string; expiresAt: string; message: string;};
A verified Sign-In with Ethereum message.
export type PasskeyWalletVerifiedSiwe = { address: `0x${string}`; chainId: number; domain: string; uri: string; nonce: string; encryptionPublicKeyResource: WalletEncryptionPublicKeySiweResource | null;};
Input of verifyPasskeyWalletSiwe().
export type PasskeyWalletVerifySiweInput = { message: string; signature: Hex; domain: string; nonce: string; chain?: Chain; transportUrl: string; transportOptions?: Parameters<typeof http>[1];};
Build the URL of the wallet popup for a sign-in request.
buildPasskeyWalletPopupUrl: (input: { walletOrigin: string; domain: string; challengeUrl: string; requestId?: string; requestedAt?: string; returnUrl?: string;}) => URL
Parameters
| Name | Type | Description |
|---|---|---|
input | { walletOrigin: string; domain: string; challengeUrl: string; requestId?: string; requestedAt?: string; returnUrl?: string; } | The origin of the wallet, the domain of the app, its challenge URL, and optionally the request identifier, date and return URL. |
Create a Sign-In with Ethereum message for an address, valid for 5 minutes.
Store the nonce on the server, for example in the session, and check it with verifyPasskeyWalletSiwe().
createPasskeyWalletChallenge: (input: { request: Request; address: `0x${string}`; chainId: number; encryptionPublicKeyResource?: string; nonce?: string; now?: Date; statement?: string; uri?: string;}) => PasskeyWalletChallenge
Parameters
| Name | Type | Description |
|---|---|---|
input | { request: Request; address: `0x${string}`; chainId: number; encryptionPublicKeyResource?: string; nonce?: string; now?: Date; statement?: string; uri?: string; } | The request, the address and chain, and optionally the public key resource from createWalletEncryptionPublicKeySiweResource(), a nonce, the current date, a statement and the URI of the app, the origin of the request by default. |
Create the manifest of an app, with the challenge URL /api/auth/wallet/challenge on the origin of the request.
createPasskeyWalletManifest: (request: Request) => PasskeyWalletManifest
Parameters
| Name | Type | Description |
|---|---|---|
request | Request | The request for the manifest. |
List the wallet origins allowed to call the sign-in endpoints of an app.
Allows the official wallet and its local copy, the configured wallet and its local copy, other configured origins, and, when the app runs on localhost, the wallet served on port 3002 of the same host.
getPasskeyWalletCorsAllowedOrigins: (input: { request: Request; walletUrl?: string; allowedOrigins?: string;}) => string[]
Parameters
| Name | Type | Description |
|---|---|---|
input | { request: Request; walletUrl?: string; allowedOrigins?: string; } | The request, the URL of the configured wallet, and other allowed origins, separated by commas. |
Get the origin of a request if it is an allowed wallet origin.
getPasskeyWalletCorsOrigin: (input: { request: Request; walletUrl?: string; allowedOrigins?: string;}) => string | null
Parameters
| Name | Type | Description |
|---|---|---|
input | { request: Request; walletUrl?: string; allowedOrigins?: string; } | The request, the URL of the configured wallet, and other allowed origins, separated by commas. |
Returns
| Type | Description |
|---|---|
string | null | The origin, or null. |
Get the public origin of a request, from the X-Forwarded-Host and X-Forwarded-Proto headers when present.
Clients can set these headers. Only rely on the result behind a proxy that overwrites them.
getPasskeyWalletRequestOrigin: (request: Request) => string
Parameters
| Name | Type | Description |
|---|---|---|
request | Request | The request. |
Check whether an origin is one of a list of allowed origins.
isWalletSignInOriginAllowed: (origin: string, allowedOrigins: readonly string[]) => boolean
Parameters
| Name | Type | Description |
|---|---|---|
origin | string | The origin to check. |
allowedOrigins | readonly string[] | The allowed origins. |
Verify a Sign-In with Ethereum message signed by the wallet.
Checks the signature, the domain, the nonce and the validity dates of the message with viem. Never pass an empty domain or nonce: viem then skips their check. Check that the nonce is the one the app issued for this session, and use it only once.
const verified = await verifyPasskeyWalletSiwe({ message, signature, domain: 'app.example.com', nonce: session.nonce, transportUrl: rpcUrl})
verifyPasskeyWalletSiwe: (input: PasskeyWalletVerifySiweInput) => Promise<PasskeyWalletVerifiedSiwe | null>
Parameters
| Name | Type | Description |
|---|---|---|
input | PasskeyWalletVerifySiweInput | The message, signature, expected domain and nonce, and an RPC endpoint. |
Returns
| Type | Description |
|---|---|
Promise<PasskeyWalletVerifiedSiwe | null> | The verified sign-in, or null if the message is invalid. |
Add CORS headers with credentials to a response, if the request comes from an allowed wallet origin.
withPasskeyWalletCorsHeaders: <Response extends { headers: Headers;}>(request: Request, response: Response, options?: { allowedMethods?: string; walletUrl?: string; allowedOrigins?: string;}) => Response
Parameters
| Name | Type | Description |
|---|---|---|
request | Request | The request. |
response | Response | The response, returned with its headers changed. |
options | { allowedMethods?: string; walletUrl?: string; allowedOrigins?: string; } | Allowed methods, GET, OPTIONS by default, the URL of the configured wallet, and other allowed origins. |
The answer of the wallet to a request it could not serve.
export type PasskeyWalletErrorResult = { type: typeof passkeyWalletErrorType; version: 1; requestId: string; code: 'user_rejected' | 'challenge_unavailable' | 'domain_mismatch' | 'wallet_locked' | 'unsupported_origin' | 'internal_error'; message: string;};
How an app accepts sign-ins with the wallet.
export type PasskeyWalletManifest = { version: 1; domain: string; signInChallengeUrl: string; signInCompletionUrl?: string;};
A request to sign in to an app with the wallet, read from the URL of the wallet popup.
export type PasskeyWalletSignInRequest = { type: typeof passkeyWalletSignInType; version: 1; requestId: string; domain: string; challengeUrl: string; returnUrl?: string; requestedAt: string;};
The answer of the wallet to a sign-in request: a signed Sign-In with Ethereum message.
export type PasskeyWalletSignInResult = { type: typeof passkeyWalletSignInResultType; version: 1; requestId: string; domain: string; address: `0x${string}`; message: string; signature: `0x${string}`; completedAt: string;};
Check that a Sign-In with Ethereum message is for the expected domain, before signing it.
assertSignInDomainMatches: (message: string, expectedDomain: string) => void
Parameters
| Name | Type | Description |
|---|---|---|
message | string | The message. |
expectedDomain | string | The domain of the request. |
Read and check a sign-in request from the URL parameters of the wallet popup.
The challenge URL must use HTTPS and belong to the requested domain or one of its subdomains, and the return URL must use HTTPS, so that the wallet only signs messages fetched from the app asking for them.
parsePasskeyWalletSignInRequest: (params: URLSearchParams) => PasskeyWalletSignInRequest
Parameters
| Name | Type | Description |
|---|---|---|
params | URLSearchParams | The URL parameters. |
Read the domain of a Sign-In with Ethereum message from its first line.
resolveSiweDomain: (message: string) => string
Parameters
| Name | Type | Description |
|---|---|---|
message | string | The message. |
Check the manifest of an app.
The domain of the manifest must be the host serving it, and its URLs must use HTTPS and belong to that domain or its subdomains.
validatePasskeyWalletManifest: (manifest: unknown, servingHost: string) => PasskeyWalletManifest
Parameters
| Name | Type | Description |
|---|---|---|
manifest | unknown | The manifest, parsed from JSON. |
servingHost | string | The host the manifest was fetched from. |
Type of the message answering a request the wallet could not serve.
const passkeyWalletErrorType = "organigram:wallet:error"
Type of the message answering a sign-in request.
const passkeyWalletSignInResultType = "organigram:wallet:sign-in-result"
Type of a sign-in request to the wallet.
const passkeyWalletSignInType = "organigram:wallet:sign-in"
The wallet is a standard Ethereum account derived from a BIP-39 recovery phrase. The recovery phrase is encrypted in a vault with AES-256-GCM, under a key derived from the PRF output of a WebAuthn passkey, so that only that passkey can decrypt it. A wallet can have several passkeys, each with its own vault. These functions run in the wallet app: an unlocked wallet holds the recovery phrase and keys in memory, so keep it there only as long as needed, and never store, send or log it.
The secrets encrypted in a passkey vault.
Holds the recovery phrase in clear: never store, send or log it outside an encrypted envelope.
export type BrowserPasskeyWalletVaultPayload = { version: 1; recoveryPhrase: string; walletEncryptionKey?: WalletEncryptionKeyPairPayload;};
A passkey to evaluate with its PRF salt.
export type PasskeyPrfCredential = { credentialId: string; transports?: string[]; prfSalt: string;};
Create the payload of a new vault, with a new wallet encryption key pair.
createBrowserPasskeyWalletVaultPayload: (recoveryPhrase: string) => Promise<BrowserPasskeyWalletVaultPayload>
Parameters
| Name | Type | Description |
|---|---|---|
recoveryPhrase | string | The recovery phrase of the wallet. |
Generate a new 12-word English BIP-39 recovery phrase.
createGeneratedRecoveryPhrase: () => string
Decrypt a vault envelope with the PRF output of its passkey.
Does not zero the PRF output, so that the caller may try several envelopes with it. Zero it once done.
decryptVaultPayloadWithPrfOutput: (input: { envelope: PasskeyVaultEnvelopeData; prfOutput: Uint8Array;}) => Promise<BrowserPasskeyWalletVaultPayload>
Parameters
| Name | Type | Description |
|---|---|---|
input | { envelope: PasskeyVaultEnvelopeData; prfOutput: Uint8Array; } | The envelope and the PRF output. |
Switch an unlocked wallet to another of its accounts.
derivePasskeyWalletAccount: (input: { wallet: UnlockedPasskeyWallet; account: StoredVaultAccount;}) => UnlockedPasskeyWallet
Parameters
| Name | Type | Description |
|---|---|---|
input | { wallet: UnlockedPasskeyWallet; account: StoredVaultAccount; } | The unlocked wallet and the account to select. |
Derive the account of an address index from a recovery phrase.
deriveStoredVaultAccount: (input: { recoveryPhrase: string; addressIndex: number; name: string;}) => StoredVaultAccount
Parameters
| Name | Type | Description |
|---|---|---|
input | { recoveryPhrase: string; addressIndex: number; name: string; } | The recovery phrase, the address index, and the name of the account, Account <index + 1> by default. |
Get the Ethereum BIP-44 derivation path of an address index.
getDerivationPath: (addressIndex: number) => string
Parameters
| Name | Type | Description |
|---|---|---|
addressIndex | number | The address index. |
Encode a UTF-8 message in hex, as personal_sign requests carry it.
hexEncodeMessage: (message: string) => `0x${string}`
Parameters
| Name | Type | Description |
|---|---|---|
message | string | The message. |
Read a decrypted vault payload.
parseBrowserPasskeyWalletVaultPayload: (plaintext: string) => BrowserPasskeyWalletVaultPayload
Parameters
| Name | Type | Description |
|---|---|---|
plaintext | string | The decrypted payload, in JSON. |
Create another passkey able to unlock an unlocked wallet, for example as a backup.
registerAdditionalBrowserPasskeyVault: (input: { wallet: UnlockedPasskeyWallet; name: string; accounts: StoredVaultAccount[]; rpId?: string; rpName?: string;}) => Promise<{ record: StoredVaultRecord; wallet: UnlockedPasskeyWallet;}>
Parameters
| Name | Type | Description |
|---|---|---|
input | { wallet: UnlockedPasskeyWallet; name: string; accounts: StoredVaultAccount[]; rpId?: string; rpName?: string; } | The unlocked wallet, the name of the passkey, the accounts it unlocks, and the WebAuthn relying party. |
Returns
| Type | Description |
|---|---|
Promise<{ record: StoredVaultRecord; wallet: UnlockedPasskeyWallet; }> | The new vault record and the wallet, now attached to the new passkey. |
Create a passkey and a vault encrypting a recovery phrase with it.
Asks the browser to create a discoverable passkey with user verification and the PRF extension. The recovery phrase is encrypted with a key derived from the PRF output, which is then zeroed. Store the returned record, which holds no secret in clear, and keep the unlocked wallet in memory only.
const { record, wallet } = await registerBrowserPasskeyVault({ recoveryPhrase: createGeneratedRecoveryPhrase(), name: 'My wallet'})
registerBrowserPasskeyVault: (input: { recoveryPhrase: string; name: string; rpId?: string; rpName?: string;}) => Promise<{ record: StoredVaultRecord; wallet: UnlockedPasskeyWallet;}>
Parameters
| Name | Type | Description |
|---|---|---|
input | { recoveryPhrase: string; name: string; rpId?: string; rpName?: string; } | The recovery phrase, the name of the wallet, the WebAuthn relying party identifier, the current host name by default, and the relying party name. |
Returns
| Type | Description |
|---|---|
Promise<{ record: StoredVaultRecord; wallet: UnlockedPasskeyWallet; }> | The vault record and the unlocked wallet, on its first account. |
Runs one user-verified assertion restricted to the given passkeys, each evaluated with its own PRF salt, and returns the PRF output of the passkey the person selected. The caller owns the returned PRF output and must zero it once its key is derived.
requestPasskeyPrfOutput: (input: { credentials: PasskeyPrfCredential[]; rpId?: string;}) => Promise<{ credentialId: string; prfOutput: Uint8Array;}>
Sign a message with EIP-191 personal_sign.
signPersonalMessage: (input: { wallet: UnlockedPasskeyWallet; message: string;}) => Promise<`0x${string}`>
Parameters
| Name | Type | Description |
|---|---|---|
input | { wallet: UnlockedPasskeyWallet; message: string; } | The unlocked wallet and the message. |
Sign EIP-712 typed data.
signTypedDataMessage: (input: { wallet: UnlockedPasskeyWallet; typedData: PasskeyWalletTypedData;}) => Promise<`0x${string}`>
Parameters
| Name | Type | Description |
|---|---|---|
input | { wallet: UnlockedPasskeyWallet; typedData: PasskeyWalletTypedData; } | The unlocked wallet and the typed data. |
Unlock a wallet with one of the passkeys of its vaults.
Asks the user to pick one of the passkeys of the records, with user verification. The vaults of that passkey are tried in turn, and the first whose recovery phrase derives the address of its record is unlocked. The PRF output is zeroed.
const { wallet } = await unlockBrowserPasskeyVault({ records })const signature = await signPersonalMessage({ wallet, message: 'Hello' })
unlockBrowserPasskeyVault: (input: { records: StoredVaultRecord[]; rpId?: string;}) => Promise<{ record: StoredVaultRecord; wallet: UnlockedPasskeyWallet;}>
Parameters
| Name | Type | Description |
|---|---|---|
input | { records: StoredVaultRecord[]; rpId?: string; } | The vault records to choose from, and the WebAuthn relying party identifier, the current host name by default. |
Returns
| Type | Description |
|---|---|
Promise<{ record: StoredVaultRecord; wallet: UnlockedPasskeyWallet; }> | The record unlocked and the wallet, on the first account of the record. |
Unlock a vault and re-encrypt it with another wallet encryption key pair.
Asks the user for a passkey, and keeps the PRF salt of the vault. Store the returned record in place of the old one.
updateStaticPasskeyVaultEncryptionKey: (input: { records: StoredVaultRecord[]; walletEncryptionKey: WalletEncryptionKeyPairPayload; rpId?: string;}) => Promise<{ record: StoredVaultRecord; wallet: UnlockedPasskeyWallet;}>
Parameters
| Name | Type | Description |
|---|---|---|
input | { records: StoredVaultRecord[]; walletEncryptionKey: WalletEncryptionKeyPairPayload; rpId?: string; } | The vault records to choose from, the new key pair and the WebAuthn relying party identifier. |
Returns
| Type | Description |
|---|---|
Promise<{ record: StoredVaultRecord; wallet: UnlockedPasskeyWallet; }> | The re-encrypted record and the unlocked wallet. |
A secret encrypted with a key derived from a passkey, such as the recovery phrase of a wallet.
The envelope holds no secret: it can be stored anywhere, as only the passkey that created it can decrypt it.
export type PasskeyVaultEnvelopeData = { algorithm: typeof passkeyVaultAlgorithm; keyVersion: typeof passkeyVaultKeyVersion; ciphertext: string; salt: string; prfSalt: string; nonce: string;};
Decode unpadded base64url.
Accepts canonical unpadded base64url only. Lenient decoders silently skip unknown characters, so a tampered value could otherwise decode to bytes other than the ones it appears to encode.
base64UrlToBytes: (value: string) => Uint8Array
Parameters
| Name | Type | Description |
|---|---|---|
value | string | The value to decode. |
Encode bytes in unpadded base64url.
bytesToBase64Url: (bytes: Uint8Array) => string
Parameters
| Name | Type | Description |
|---|---|---|
bytes | Uint8Array | The bytes to encode. |
Decrypt the secret of a vault envelope.
decryptPasskeyVaultSecret: (input: DecryptPasskeyVaultSecretInput) => Promise<string>
Parameters
| Name | Type | Description |
|---|---|---|
input | ~DecryptPasskeyVaultSecretInput | The envelope and the key derived for it. |
Derive the AES-GCM key of a vault from the PRF output of a passkey.
The key cannot be exported. A copy of the PRF output made for the import is zeroed afterwards, but not the original: zero it once you no longer need it.
derivePasskeyVaultKey: (input: DerivePasskeyVaultKeyInput) => Promise<CryptoKey>
Parameters
| Name | Type | Description |
|---|---|---|
input | ~DerivePasskeyVaultKeyInput | The 32-byte PRF output, the 32-byte salt of the vault, and an optional HKDF info that separates keys derived from the same PRF output for different purposes. |
Encrypt a secret with a vault key into a vault envelope.
encryptPasskeyVaultSecret: (input: EncryptPasskeyVaultSecretInput) => Promise<PasskeyVaultEnvelopeData>
Parameters
| Name | Type | Description |
|---|---|---|
input | ~EncryptPasskeyVaultSecretInput | The secret, the key from derivePasskeyVaultKey(), the salt it was derived with, the 32-byte PRF salt, and an optional 12-byte nonce, random by default. Never reuse a nonce with the same key. |
Get the Web Crypto API of the environment.
getCrypto: () => Crypto
Check that a value is canonical base64url whose decoded length passes a test.
hasBase64UrlByteLength: (value: unknown, isValidLength: (byteLength: number) => boolean) => boolean
Parameters
| Name | Type | Description |
|---|---|---|
value | unknown | The value to check. |
isValidLength | (byteLength: number) => boolean | Test of the decoded length, in bytes. |
Check whether a newly created passkey enabled the PRF extension, which the wallet needs to derive its keys.
isPasskeyPrfSupported: (input: { credential: PrfSupportCredential | null;}) => Promise<boolean>
Parameters
| Name | Type | Description |
|---|---|---|
input | { credential: ~PrfSupportCredential | null; } | The credential returned by navigator.credentials.create(). |
Check that a value is a passkey vault envelope of the current algorithm and key version, with salts, nonce and ciphertext of valid lengths.
isPasskeyVaultEnvelope: (value: unknown) => value is PasskeyVaultEnvelopeData
Parameters
| Name | Type | Description |
|---|---|---|
value | unknown | The value to check. |
Generate cryptographically secure random bytes.
randomBytes: (length: number) => Uint8Array
Parameters
| Name | Type | Description |
|---|---|---|
length | number | Number of bytes. |
Copy the bytes of a view into a new ArrayBuffer.
toArrayBuffer: (bytes: Uint8Array) => ArrayBuffer
Parameters
| Name | Type | Description |
|---|---|---|
bytes | Uint8Array | The bytes to copy. |
Algorithm of passkey vaults: AES-256-GCM with a key derived by HKDF-SHA-256 from the passkey PRF output.
const passkeyVaultAlgorithm = "AES-GCM-HKDF-SHA-256"
Version of the key derivation of passkey vaults.
const passkeyVaultKeyVersion = 2
Shared UTF-8 decoder.
const textDecoder: TextDecoder
Shared UTF-8 encoder.
const textEncoder: TextEncoder
Events emitted by the EIP-1193 provider of the wallet.
export type PasskeyProviderEvent = 'accountsChanged' | 'chainChanged' | 'disconnect';
Listener of a provider event.
export type PasskeyProviderListener = (...args: unknown[]) => void;
The address and passkey of a newly registered wallet.
export type PasskeyRegistrationResult = { address: `0x${string}`; credentialId: string;};
A wallet unlocked with a passkey, ready to sign.
It holds the recovery phrase and the private keys in memory. Keep it in memory only for as long as needed, never store, send or log it.
export type UnlockedPasskeyWallet = { address: `0x${string}`; account: HDAccount; recoveryPhrase: string; walletEncryptionKey?: WalletEncryptionKeyPairPayload; credentialId: string;};
Path of the icon of the passkey wallet, relative to the app serving it.
const organigramPasskeyWalletIcon = "/png/logo-gradient.png"
Identifier of the passkey wallet connector.
const organigramPasskeyWalletId = "organigram-passkeys"
A vault record holds a vault envelope with the public details needed to unlock it: the accounts, the passkey and its device. Records hold no secret in clear, so that they can be stored in the browser or exported. These functions read, list and update records.
An account of a wallet, derived from its recovery phrase.
export type StoredVaultAccount = { address: `0x${string}`; name: string; addressIndex: number; derivationPath: string;};
A wallet vault encrypted for one passkey, with the public details needed to unlock it.
A wallet has one record per passkey that can unlock it. The record holds no secret in clear: only its passkey can decrypt the envelope.
export type StoredVaultRecord = { version: 1; address: `0x${string}`; accounts: StoredVaultAccount[]; credentialId: string; rawCredentialId?: string; name: string; transports: string[]; passkeyDevice: { kind: 'macos' | 'ios' | 'ipados' | 'windows' | 'android' | 'linux' | 'security-key' | 'cross-device' | 'platform' | 'passkey'; label: string; }; createdAt: string; lastUsedAt: string | null; envelope: PasskeyVaultEnvelopeData;};
A list of vault records, as stored or exported.
export type VaultRegistry = { version: 1; vaults: StoredVaultRecord[];};
Add an account to every vault of the wallet that holds another account.
addAccountToVaultRecords: (input: { vaults: StoredVaultRecord[]; sourceAddress: `0x${string}`; account: StoredVaultAccount;}) => StoredVaultRecord[]
Parameters
| Name | Type | Description |
|---|---|---|
input | { vaults: StoredVaultRecord[]; sourceAddress: `0x${string}`; account: StoredVaultAccount; } | The vault records, an account the wallet already holds, and the account to add. |
Wrap vault records in a registry.
createVaultRegistry: (vaults: StoredVaultRecord[]) => VaultRegistry
Parameters
| Name | Type | Description |
|---|---|---|
vaults | StoredVaultRecord[] | The vault records. |
Check that a value is a valid vault record.
isStoredVaultRecord: (value: unknown) => value is StoredVaultRecord
Parameters
| Name | Type | Description |
|---|---|---|
value | unknown | The value to check. |
Set the date of the last unlock of a vault.
markVaultRecordUsed: (input: { vaults: StoredVaultRecord[]; credentialId: string; usedAt?: string;}) => StoredVaultRecord[]
Parameters
| Name | Type | Description |
|---|---|---|
input | { vaults: StoredVaultRecord[]; credentialId: string; usedAt?: string; } | The vault records, the identifier of the passkey, and the date, now by default. |
Read a vault registry, dropping invalid records.
parseVaultRegistry: (value: unknown) => VaultRegistry
Parameters
| Name | Type | Description |
|---|---|---|
value | unknown | The registry, parsed from JSON. |
Remove an account from every vault, and the vaults left without accounts.
This only removes local records. Without a remaining vault or exported recovery phrase, the account can no longer be unlocked.
removeAccountFromVaultRecords: (input: { vaults: StoredVaultRecord[]; address: `0x${string}`;}) => StoredVaultRecord[]
Parameters
| Name | Type | Description |
|---|---|---|
input | { vaults: StoredVaultRecord[]; address: `0x${string}`; } | The vault records and the address of the account. |
Remove the vault record of a passkey.
removeVaultRecord: (input: { vaults: StoredVaultRecord[]; credentialId: string;}) => StoredVaultRecord[]
Parameters
| Name | Type | Description |
|---|---|---|
input | { vaults: StoredVaultRecord[]; credentialId: string; } | The vault records and the identifier of the passkey. |
Remove the vault records of several passkeys.
removeVaultRecords: (input: { vaults: StoredVaultRecord[]; credentialIds: Set<string>;}) => StoredVaultRecord[]
Parameters
| Name | Type | Description |
|---|---|---|
input | { vaults: StoredVaultRecord[]; credentialIds: Set<string>; } | The vault records and the identifiers of the passkeys. |
Serialize vault records as a registry in JSON.
serializeVaultRegistry: (vaults: StoredVaultRecord[], space?: number) => string
Parameters
| Name | Type | Description |
|---|---|---|
vaults | StoredVaultRecord[] | The vault records. |
space | number | Indentation of the JSON. |
Add a vault record, or replace the record of the same passkey, and put it first.
upsertVaultRecord: (input: { vaults: StoredVaultRecord[]; record: StoredVaultRecord;}) => StoredVaultRecord[]
Parameters
| Name | Type | Description |
|---|---|---|
input | { vaults: StoredVaultRecord[]; record: StoredVaultRecord; } | The vault records and the record to add. |
Check whether a vault holds an account, ignoring the case of the address.
vaultHasAccount: (vault: StoredVaultRecord, address: string) => boolean
Parameters
| Name | Type | Description |
|---|---|---|
vault | StoredVaultRecord | The vault record. |
address | string | The address of the account. |
Vault records holding the same accounts, usually the passkeys of one wallet.
export type VaultRegistryGroup = { id: string; accounts: StoredVaultAccount[]; passkeys: StoredVaultRecord[]; createdAt: string;};
An account with the passkeys able to unlock it.
export type WalletGroup = { address: `0x${string}`; name: string; addressIndex: number; derivationPath: string; passkeys: StoredVaultRecord[];};
Shorten an address for display, as 0x1234...abcd.
formatAddress: (address: string) => string
Parameters
| Name | Type | Description |
|---|---|---|
address | string | The address. |
Shorten a passkey identifier longer than 18 characters for display.
formatCredentialId: (credentialId: string) => string
Parameters
| Name | Type | Description |
|---|---|---|
credentialId | string | The passkey identifier. |
Get the address index of the next account to derive: one more than the highest index of the records, or 0.
getNextAddressIndex: (records: StoredVaultRecord[]) => number
Parameters
| Name | Type | Description |
|---|---|---|
records | StoredVaultRecord[] | The vault records. |
Find an account of a vault record, ignoring the case of the address.
getVaultAccount: (vault: StoredVaultRecord, address: string) => StoredVaultAccount | null
Parameters
| Name | Type | Description |
|---|---|---|
vault | StoredVaultRecord | The vault record. |
address | string | The address of the account. |
Returns
| Type | Description |
|---|---|
StoredVaultAccount | null | The account, or null. |
Identify the set of accounts of a vault record, by address index and address.
getVaultRegistryGroupId: (vault: StoredVaultRecord) => string
Parameters
| Name | Type | Description |
|---|---|---|
vault | StoredVaultRecord | The vault record. |
Group vault records holding the same accounts, oldest first.
groupVaultRecordsBySeed: (vaults: StoredVaultRecord[]) => VaultRegistryGroup[]
Parameters
| Name | Type | Description |
|---|---|---|
vaults | StoredVaultRecord[] | The vault records. |
Group vault records by account, ordered by address index.
groupVaultsByWallet: (vaults: StoredVaultRecord[]) => WalletGroup[]
Parameters
| Name | Type | Description |
|---|---|---|
vaults | StoredVaultRecord[] | The vault records. |
Merge imported vault records into local ones, newest first.
An imported record replaces the local record of the same passkey.
mergeVaults: (localVaults: StoredVaultRecord[], importedVaults: StoredVaultRecord[]) => StoredVaultRecord[]
Parameters
| Name | Type | Description |
|---|---|---|
localVaults | StoredVaultRecord[] | The local records. |
importedVaults | StoredVaultRecord[] | The imported records. |
Vault records can be backed up on a server. Each backup exposes only its vault envelope and passkey identifier: the rest of the record is encrypted with a key derived from the recovery phrase, so that the server learns nothing beyond the wallet address, and any passkey of the wallet can restore every backup.
The backups of a wallet, as exported or restored.
export type RemoteVaultBackupPackage = { version: 1; address: `0x${string}`; backups: RemoteVaultBackupRecord[]; exportedAt: string;};
A vault record encrypted for backup on a server.
A backup exposes its vault envelope, which only its own passkey can decrypt, with the identifier of that passkey. The rest of the vault record (account addresses, names, devices) is encrypted with a key derived from the recovery phrase, so that any passkey of the wallet can restore every backup while the server learns nothing beyond the wallet address.
export type RemoteVaultBackupRecord = { version: 2; backupId: string; credentialId: string; envelope: PasskeyVaultEnvelopeData; salt: string; iv: string; ciphertext: string; createdAt: string;};
Wrap the backups of a wallet in a package.
createRemoteVaultBackupPackage: (input: { address: `0x${string}`; backups: RemoteVaultBackupRecord[];}) => RemoteVaultBackupPackage
Parameters
| Name | Type | Description |
|---|---|---|
input | { address: `0x${string}`; backups: RemoteVaultBackupRecord[]; } | The address of the wallet and its backups. |
Encrypt a vault record for backup.
The backup key is derived with HKDF from a SHA-256 hash of the recovery phrase, a random salt, and the address. The recovery phrase itself is never part of the backup.
createRemoteVaultBackupRecord: (input: { address: `0x${string}`; record: StoredVaultRecord; recoveryPhrase: string;}) => Promise<RemoteVaultBackupRecord>
Parameters
| Name | Type | Description |
|---|---|---|
input | { address: `0x${string}`; record: StoredVaultRecord; recoveryPhrase: string; } | The address of the wallet, the vault record and the recovery phrase of the wallet. |
Hash backups with SHA-256, over a JSON serialization with sorted keys.
createVaultBackupDigest: (backups: RemoteVaultBackupRecord[]) => Promise<string>
Parameters
| Name | Type | Description |
|---|---|---|
backups | RemoteVaultBackupRecord[] | The backups. |
Returns
| Type | Description |
|---|---|
Promise<string> | The digest, in hex. |
Build the message the wallet signs to authorize storing backups.
createVaultBackupSignatureMessage: (input: { address: `0x${string}`; vaultCount: number; digest: string;}) => string
Parameters
| Name | Type | Description |
|---|---|---|
input | { address: `0x${string}`; vaultCount: number; digest: string; } | The address of the wallet, the number of backups and their digest from createVaultBackupDigest(). |
Decrypt a vault record from a backup.
The result is not validated: check it with isStoredVaultRecord() before use.
decryptRemoteVaultBackupRecord: (input: { address: `0x${string}`; backup: RemoteVaultBackupRecord; recoveryPhrase: string;}) => Promise<StoredVaultRecord>
Parameters
| Name | Type | Description |
|---|---|---|
input | { address: `0x${string}`; backup: RemoteVaultBackupRecord; recoveryPhrase: string; } | The address of the wallet, the backup and the recovery phrase of the wallet. |
Check and checksum an address typed by a user to restore backups.
normalizeBackupAddress: (value: string) => `0x${string}`
Parameters
| Name | Type | Description |
|---|---|---|
value | string | The address. |
Read a backup package.
parseRemoteVaultBackupPackage: (value: unknown) => RemoteVaultBackupPackage
Parameters
| Name | Type | Description |
|---|---|---|
value | unknown | The package, parsed from JSON. |
Each wallet has an ECDH P-256 key pair, stored in its vaults, to encrypt files and texts that only it can decrypt. Its public key can be shared with an app in a Sign-In with Ethereum resource. Texts encrypted for an app are bound to the origin of that app, so that another app cannot have them decrypted.
A decrypted file.
export type WalletDecryptedFile = { name: string; type: string; bytes: Uint8Array; lastModified?: number;};
A file encrypted for the wallet that encrypted it.
export type WalletEncryptedFilePackage = { version: 1; kind: 'organigram:wallet-encrypted-file'; algorithm: 'ECDH-P256+A256GCM'; recipient: { address: `0x${string}`; publicKey: JsonWebKey; keyVersion: number; }; file: { name: string; type: string; size: number; lastModified?: number; }; iv: string; ciphertext: string; encryptedAt: string;};
A text encrypted by a wallet for itself, which only one application origin may have decrypted.
export type WalletEncryptedPayload = { version: 1; kind: 'organigram:wallet-encrypted-payload'; algorithm: 'ECDH-P256+A256GCM'; appOrigin: string; recipient: { address: `0x${string}`; publicKey: JsonWebKey; keyVersion: number; }; iv: string; ciphertext: string; encryptedAt: string;};
The ECDH P-256 key pair of a wallet for end-to-end encryption, stored in its passkey vaults.
Holds the private key in clear: keep it in the encrypted vault and in memory only, never store, send or log it elsewhere.
export type WalletEncryptionKeyPairPayload = { version: 1; algorithm: 'ECDH-P256'; privateKey: JsonWebKey; publicKey: JsonWebKey; keyVersion: number;};
The public key of a wallet, read from a Sign-In with Ethereum resource.
export type WalletEncryptionPublicKeySiweResource = { kind: typeof encryptionPublicKeyResourceKind; address: string; publicKey: JsonWebKey; keyVersion: number;};
Generate a new ECDH P-256 key pair for a wallet.
The private key is exportable, so that it can be stored in the passkey vaults of the wallet. Keep it there.
createWalletEncryptionKeyPair: () => Promise<WalletEncryptionKeyPairPayload>
Encode the public key of a wallet as a resource of a Sign-In with Ethereum message.
Signing in with this resource lets the application learn the public key of the wallet, bound to its address by the signature.
createWalletEncryptionPublicKeySiweResource: (input: { address: `0x${string}`; keyPair: WalletEncryptionKeyPairPayload;}) => string
Parameters
| Name | Type | Description |
|---|---|---|
input | { address: `0x${string}`; keyPair: WalletEncryptionKeyPairPayload; } | The address of the wallet and its key pair. Only the public key is encoded. |
Decrypt a file encrypted with encryptFileWithWalletEncryptionKey().
decryptFileWithWalletEncryptionKey: (input: { encryptedPackage: WalletEncryptedFilePackage; keyPair: WalletEncryptionKeyPairPayload;}) => Promise<WalletDecryptedFile>
Parameters
| Name | Type | Description |
|---|---|---|
input | { encryptedPackage: WalletEncryptedFilePackage; keyPair: WalletEncryptionKeyPairPayload; } | The encrypted package and the key pair of the wallet. |
Decrypt a text encrypted with encryptPayloadWithWalletEncryptionKey().
decryptPayloadWithWalletEncryptionKey: (input: { encryptedPayload: WalletEncryptedPayload; appOrigin: string; keyPair: WalletEncryptionKeyPairPayload;}) => Promise<string>
Parameters
| Name | Type | Description |
|---|---|---|
input | { encryptedPayload: WalletEncryptedPayload; appOrigin: string; keyPair: WalletEncryptionKeyPairPayload; } | The encrypted payload, the origin of the application asking for it, and the key pair of the wallet. |
Encrypt a file so that only the wallet holding the key pair can decrypt it.
The AES-GCM key is derived by ECDH between the private and public keys of the same key pair. The name, type, size and date of the file stay in clear and are not authenticated with the ciphertext.
encryptFileWithWalletEncryptionKey: (input: { address: `0x${string}`; file: File; keyPair: WalletEncryptionKeyPairPayload;}) => Promise<WalletEncryptedFilePackage>
Parameters
| Name | Type | Description |
|---|---|---|
input | { address: `0x${string}`; file: File; keyPair: WalletEncryptionKeyPairPayload; } | The address of the wallet, the file and the key pair. |
Encrypt a text so that only the wallet holding the key pair can decrypt it, for one application origin.
The application origin, the address and the key version are authenticated with the ciphertext, so that the payload cannot be decrypted for another application or relabelled for it.
encryptPayloadWithWalletEncryptionKey: (input: { address: `0x${string}`; appOrigin: string; plaintext: string; keyPair: WalletEncryptionKeyPairPayload;}) => Promise<WalletEncryptedPayload>
Parameters
| Name | Type | Description |
|---|---|---|
input | { address: `0x${string}`; appOrigin: string; plaintext: string; keyPair: WalletEncryptionKeyPairPayload; } | The address of the wallet, the origin of the application, the text and the key pair. |
Read an encrypted file package.
parseWalletEncryptedFilePackage: (value: unknown) => WalletEncryptedFilePackage
Parameters
| Name | Type | Description |
|---|---|---|
value | unknown | The package, parsed from JSON. |
Read an encrypted payload.
parseWalletEncryptedPayload: (value: unknown) => WalletEncryptedPayload
Parameters
| Name | Type | Description |
|---|---|---|
value | unknown | The payload, parsed from JSON. |
Read a wallet encryption key pair.
parseWalletEncryptionKeyPair: (value: unknown) => WalletEncryptionKeyPairPayload
Parameters
| Name | Type | Description |
|---|---|---|
value | unknown | The key pair, parsed from JSON. |
Read the public key of a wallet from the resources of a Sign-In with Ethereum message.
Only trust the result once the signature of the message is verified.
parseWalletEncryptionPublicKeySiweResource: (input: { address: string; resources: string[] | undefined;}) => WalletEncryptionPublicKeySiweResource | null
Parameters
| Name | Type | Description |
|---|---|---|
input | { address: string; resources: string[] | undefined; } | The address that signed the message, and the resources of the message. |
Returns
| Type | Description |
|---|---|
WalletEncryptionPublicKeySiweResource | null | The public key, or null if no valid resource of this address is found. |
Edit this page
On this page:
© 2026 Organigram SAS
All Rights Reserved.