passkey-kit 0.12.1 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/README.md +408 -227
  2. package/{types → dist}/base.d.ts +1 -0
  3. package/dist/base.d.ts.map +1 -0
  4. package/{src/base.ts → dist/base.js} +8 -10
  5. package/dist/base.js.map +1 -0
  6. package/dist/base64url.d.ts +18 -0
  7. package/dist/base64url.d.ts.map +1 -0
  8. package/dist/base64url.js +18 -0
  9. package/dist/base64url.js.map +1 -0
  10. package/dist/constants.d.ts +91 -0
  11. package/dist/constants.d.ts.map +1 -0
  12. package/dist/constants.js +115 -0
  13. package/dist/constants.js.map +1 -0
  14. package/dist/contract-errors.d.ts +69 -0
  15. package/dist/contract-errors.d.ts.map +1 -0
  16. package/dist/contract-errors.js +132 -0
  17. package/dist/contract-errors.js.map +1 -0
  18. package/dist/errors.d.ts +163 -0
  19. package/dist/errors.d.ts.map +1 -0
  20. package/dist/errors.js +244 -0
  21. package/dist/errors.js.map +1 -0
  22. package/dist/events.d.ts +94 -0
  23. package/dist/events.d.ts.map +1 -0
  24. package/dist/events.js +92 -0
  25. package/dist/events.js.map +1 -0
  26. package/dist/index.d.ts +22 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +36 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/indexer/codec.d.ts +18 -0
  31. package/dist/indexer/codec.d.ts.map +1 -0
  32. package/dist/indexer/codec.js +32 -0
  33. package/dist/indexer/codec.js.map +1 -0
  34. package/dist/indexer/index.d.ts +25 -0
  35. package/dist/indexer/index.d.ts.map +1 -0
  36. package/dist/indexer/index.js +34 -0
  37. package/dist/indexer/index.js.map +1 -0
  38. package/dist/indexer/mercury.d.ts +94 -0
  39. package/dist/indexer/mercury.d.ts.map +1 -0
  40. package/dist/indexer/mercury.js +245 -0
  41. package/dist/indexer/mercury.js.map +1 -0
  42. package/dist/indexer/stellar-indexer.d.ts +115 -0
  43. package/dist/indexer/stellar-indexer.d.ts.map +1 -0
  44. package/dist/indexer/stellar-indexer.js +279 -0
  45. package/dist/indexer/stellar-indexer.js.map +1 -0
  46. package/dist/indexer/types.d.ts +73 -0
  47. package/dist/indexer/types.d.ts.map +1 -0
  48. package/dist/indexer/types.js +9 -0
  49. package/dist/indexer/types.js.map +1 -0
  50. package/dist/kit/auth-payload.d.ts +106 -0
  51. package/dist/kit/auth-payload.d.ts.map +1 -0
  52. package/dist/kit/auth-payload.js +227 -0
  53. package/dist/kit/auth-payload.js.map +1 -0
  54. package/dist/kit/deploy-ops.d.ts +45 -0
  55. package/dist/kit/deploy-ops.d.ts.map +1 -0
  56. package/dist/kit/deploy-ops.js +69 -0
  57. package/dist/kit/deploy-ops.js.map +1 -0
  58. package/dist/kit/tx-ops.d.ts +95 -0
  59. package/dist/kit/tx-ops.d.ts.map +1 -0
  60. package/dist/kit/tx-ops.js +125 -0
  61. package/dist/kit/tx-ops.js.map +1 -0
  62. package/dist/kit/wallet-ops.d.ts +59 -0
  63. package/dist/kit/wallet-ops.d.ts.map +1 -0
  64. package/dist/kit/wallet-ops.js +122 -0
  65. package/dist/kit/wallet-ops.js.map +1 -0
  66. package/dist/kit/webauthn-ops.d.ts +55 -0
  67. package/dist/kit/webauthn-ops.d.ts.map +1 -0
  68. package/dist/kit/webauthn-ops.js +79 -0
  69. package/dist/kit/webauthn-ops.js.map +1 -0
  70. package/dist/kit.d.ts +132 -0
  71. package/dist/kit.d.ts.map +1 -0
  72. package/dist/kit.js +290 -0
  73. package/dist/kit.js.map +1 -0
  74. package/dist/managers/credential-manager.d.ts +39 -0
  75. package/dist/managers/credential-manager.d.ts.map +1 -0
  76. package/dist/managers/credential-manager.js +47 -0
  77. package/dist/managers/credential-manager.js.map +1 -0
  78. package/dist/managers/index.d.ts +7 -0
  79. package/dist/managers/index.d.ts.map +1 -0
  80. package/dist/managers/index.js +7 -0
  81. package/dist/managers/index.js.map +1 -0
  82. package/dist/managers/signer-manager.d.ts +62 -0
  83. package/dist/managers/signer-manager.d.ts.map +1 -0
  84. package/dist/managers/signer-manager.js +111 -0
  85. package/dist/managers/signer-manager.js.map +1 -0
  86. package/dist/managers/submission-manager.d.ts +52 -0
  87. package/dist/managers/submission-manager.d.ts.map +1 -0
  88. package/dist/managers/submission-manager.js +77 -0
  89. package/dist/managers/submission-manager.js.map +1 -0
  90. package/dist/relayer.d.ts +61 -0
  91. package/dist/relayer.d.ts.map +1 -0
  92. package/dist/relayer.js +122 -0
  93. package/dist/relayer.js.map +1 -0
  94. package/dist/rpc-data.d.ts +45 -0
  95. package/dist/rpc-data.d.ts.map +1 -0
  96. package/dist/rpc-data.js +61 -0
  97. package/dist/rpc-data.js.map +1 -0
  98. package/dist/sac.d.ts +31 -0
  99. package/dist/sac.d.ts.map +1 -0
  100. package/dist/sac.js +47 -0
  101. package/dist/sac.js.map +1 -0
  102. package/dist/server.d.ts +79 -0
  103. package/dist/server.d.ts.map +1 -0
  104. package/dist/server.js +152 -0
  105. package/dist/server.js.map +1 -0
  106. package/dist/signers.d.ts +83 -0
  107. package/dist/signers.d.ts.map +1 -0
  108. package/dist/signers.js +124 -0
  109. package/dist/signers.js.map +1 -0
  110. package/dist/storage/index.d.ts +13 -0
  111. package/dist/storage/index.d.ts.map +1 -0
  112. package/dist/storage/index.js +12 -0
  113. package/dist/storage/index.js.map +1 -0
  114. package/dist/storage/indexeddb.d.ts +26 -0
  115. package/dist/storage/indexeddb.d.ts.map +1 -0
  116. package/dist/storage/indexeddb.js +100 -0
  117. package/dist/storage/indexeddb.js.map +1 -0
  118. package/dist/storage/localStorage.d.ts +22 -0
  119. package/dist/storage/localStorage.d.ts.map +1 -0
  120. package/dist/storage/localStorage.js +86 -0
  121. package/dist/storage/localStorage.js.map +1 -0
  122. package/dist/storage/memory.d.ts +18 -0
  123. package/dist/storage/memory.d.ts.map +1 -0
  124. package/dist/storage/memory.js +41 -0
  125. package/dist/storage/memory.js.map +1 -0
  126. package/dist/types.d.ts +136 -0
  127. package/dist/types.d.ts.map +1 -0
  128. package/dist/types.js +39 -0
  129. package/dist/types.js.map +1 -0
  130. package/dist/utils.d.ts +72 -0
  131. package/dist/utils.d.ts.map +1 -0
  132. package/dist/utils.js +253 -0
  133. package/dist/utils.js.map +1 -0
  134. package/dist/validation.d.ts +42 -0
  135. package/dist/validation.d.ts.map +1 -0
  136. package/dist/validation.js +75 -0
  137. package/dist/validation.js.map +1 -0
  138. package/dist/version.d.ts +7 -0
  139. package/dist/version.d.ts.map +1 -0
  140. package/dist/version.js +9 -0
  141. package/dist/version.js.map +1 -0
  142. package/package.json +47 -20
  143. package/.claude/settings.local.json +0 -20
  144. package/.cursorrules +0 -170
  145. package/.gitattributes +0 -2
  146. package/.github/workflows/release.yml +0 -21
  147. package/.vscode/settings.json +0 -6
  148. package/PROPOSAL.md +0 -397
  149. package/cheatsheet.txt +0 -18
  150. package/clone-js-sdk.sh +0 -21
  151. package/packages/passkey-kit-sdk/README.md +0 -54
  152. package/packages/passkey-kit-sdk/package.json +0 -21
  153. package/packages/passkey-kit-sdk/src/index.ts +0 -171
  154. package/packages/passkey-kit-sdk/tsconfig.json +0 -98
  155. package/packages/passkey-kit-sdk/types/index.d.ts +0 -190
  156. package/packages/sac-sdk/README.md +0 -54
  157. package/packages/sac-sdk/package.json +0 -21
  158. package/packages/sac-sdk/src/index.ts +0 -586
  159. package/packages/sac-sdk/tsconfig.json +0 -98
  160. package/packages/sac-sdk/types/index.d.ts +0 -532
  161. package/pnpm-workspace.yaml +0 -2
  162. package/src/index.ts +0 -5
  163. package/src/kit.ts +0 -823
  164. package/src/sac.ts +0 -29
  165. package/src/server.ts +0 -179
  166. package/src/types.ts +0 -32
  167. package/tsconfig.json +0 -28
  168. package/types/index.d.ts +0 -5
  169. package/types/kit.d.ts +0 -95
  170. package/types/sac.d.ts +0 -13
  171. package/types/server.d.ts +0 -29
  172. package/types/types.d.ts +0 -22
package/README.md CHANGED
@@ -1,339 +1,520 @@
1
1
  # Passkey Kit
2
2
 
3
- > [!TIP]
4
- > **Looking for the latest smart wallet SDK?**
5
- >
6
- > This package is the **legacy precursor** to [OpenZeppelin Smart Accounts](https://docs.openzeppelin.com/stellar-contracts/accounts/smart-account). For new projects, use **[smart-account-kit](https://github.com/kalepail/smart-account-kit)** — a comprehensive SDK built on top of the audited [OpenZeppelin stellar-contracts](https://github.com/OpenZeppelin/stellar-contracts) library.
7
- >
8
- > Smart Account Kit includes:
9
- > - Context rules with fine-grained authorization scopes
10
- > - Policy support (threshold multisig, spending limits, custom policies)
11
- > - Session management with automatic credential persistence
12
- > - External wallet adapter support
13
- > - Built-in indexer for contract discovery
14
- >
15
- > See the [OpenZeppelin Smart Account package](https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/accounts) and [multisig example](https://github.com/OpenZeppelin/stellar-contracts/tree/main/examples/multisig-smart-account/account) for more details.
3
+ A TypeScript SDK for creating and using **smart-wallet accounts on Stellar with WebAuthn passkeys**. A wallet is a Soroban smart contract whose signers can be secp256r1 passkeys, Ed25519 keys, or policy contracts. The kit handles the WebAuthn ceremonies, deterministic wallet-address derivation, the flat multi-signer signing pipeline, fee-sponsored submission, and signer discovery.
16
4
 
17
- > [!WARNING]
18
- > Code in this repo is demo material only. It has not been audited. Do not use to hold, protect, or secure anything.
19
-
20
- A TypeScript SDK for creating and managing Stellar smart wallets using passkeys. Works with [OpenZeppelin Relayer](https://docs.openzeppelin.com/relayer/1.3.x/guides/stellar-channels-guide) for submitting passkey-signed transactions onchain.
5
+ - **Client (`PasskeyKit`)** — runs in the browser: create/connect wallets, sign transactions, build signer-management transactions. Holds no secrets.
6
+ - **Server (`PasskeyServer`)** runs server-side: submits transactions through a relayer (fee sponsorship), plus convenience signer-discovery helpers over the keyless Mercury indexer. Holds the relayer secret.
21
7
 
22
- **Demo:** [passkey-kit-demo.pages.dev](https://passkey-kit-demo.pages.dev/)
8
+ > [!IMPORTANT]
9
+ > **Security.** Review the contract and SDK yourself before holding meaningful value, and read [Caveats](#caveats).
10
+
11
+ > [!NOTE]
12
+ > **Looking for context rules, thresholds, and spending-limit policies?** [smart-account-kit](https://github.com/kalepail/smart-account-kit) is a sibling SDK built on the audited [OpenZeppelin stellar-contracts](https://github.com/OpenZeppelin/stellar-contracts) account. It uses a different on-chain authorization model (context rules + an auth digest) than passkey-kit's flat `Signatures` map, so the two are not drop-in compatible — pick the model that fits your app.
13
+
14
+ ## Contents
15
+
16
+ - [Installation](#installation)
17
+ - [Packaging & exports](#packaging--exports)
18
+ - [Quick start](#quick-start)
19
+ - [`PasskeyKit` (client)](#passkeykit-client)
20
+ - [Signers](#signers)
21
+ - [Signer management](#signer-management)
22
+ - [`PasskeyServer` (server)](#passkeyserver-server)
23
+ - [Submission (relayer)](#submission-relayer)
24
+ - [Discovery (indexer)](#discovery-indexer)
25
+ - [Tokens (`SACClient`)](#tokens-sacclient)
26
+ - [Storage adapters](#storage-adapters)
27
+ - [Errors](#errors)
28
+ - [Types](#types)
29
+ - [Caveats](#caveats)
30
+ - [Contract interface](#contract-interface)
31
+ - [Repository layout & development](#repository-layout--development)
32
+ - [Resources](#resources)
23
33
 
24
34
  ## Installation
25
35
 
26
36
  ```bash
27
- pnpm i passkey-kit
28
- ```
29
-
30
- ## Exports
31
-
32
- ```ts
33
- import {
34
- PasskeyKit, // Client-side wallet management
35
- PasskeyServer, // Server-side utilities
36
- SACClient, // Stellar Asset Contract helper
37
- PasskeyClient, // Low-level contract client (from passkey-kit-sdk)
38
- SignerKey, // Signer key type constructor
39
- SignerStore, // Storage type enum
40
- type Signer, // Signer type
41
- type SignerLimits // Signer limits type
42
- } from 'passkey-kit'
37
+ pnpm add passkey-kit
38
+ # peer dependency
39
+ pnpm add @stellar/stellar-sdk
43
40
  ```
44
41
 
45
- ---
42
+ `@stellar/stellar-sdk` is a **peer dependency** (`>=16.0.0`); the kit targets Protocol 27 smart accounts, which earlier SDKs cannot express.
46
43
 
47
- ## PasskeyKit (Client)
44
+ ## Packaging & exports
48
45
 
49
- Handles wallet creation, connection, and transaction signing.
46
+ The package ships **compiled ESM + type declarations** from `dist/` (not raw TypeScript — no bundler transpile step is required). It exposes three entry points, split so server secrets can never be pulled into a browser bundle:
50
47
 
51
- ### Constructor
48
+ | Import | Contents | Where it runs |
49
+ |---|---|---|
50
+ | `passkey-kit` | `PasskeyKit`, signers, types, errors, validation, crypto helpers, the keyless `MercuryIndexer` + indexer types | Browser or server |
51
+ | `passkey-kit/storage` | `MemoryStorage`, `LocalStorageAdapter`, `IndexedDBStorage` | Browser (persistence) |
52
+ | `passkey-kit/server` | `PasskeyServer`, `RelayerClient` — **holds the relayer secret** | Server only |
52
53
 
53
54
  ```ts
54
- const account = new PasskeyKit({
55
- rpcUrl: string, // Stellar RPC URL
56
- networkPassphrase: string, // Network passphrase
57
- walletWasmHash: string, // Smart wallet WASM hash
58
- timeoutInSeconds?: number, // Transaction timeout (default: 30)
59
- WebAuthn?: { // Optional WebAuthn override
60
- startRegistration,
61
- startAuthentication
62
- }
63
- })
55
+ // Browser
56
+ import { PasskeyKit, PasskeySigner, Ed25519Signer, SACClient, SignerKey, SignerStore } from "passkey-kit";
57
+ import { IndexedDBStorage } from "passkey-kit/storage";
58
+
59
+ // Server ONLY (never import from browser code — it carries the relayer secret)
60
+ import { PasskeyServer } from "passkey-kit/server";
64
61
  ```
65
62
 
66
- ### Properties
63
+ ## Quick start
67
64
 
68
- | Property | Type | Description |
69
- |----------|------|-------------|
70
- | `keyId` | `string \| undefined` | Current passkey ID (base64url) |
71
- | `wallet` | `PasskeyClient \| undefined` | Connected wallet client |
72
- | `networkPassphrase` | `string` | Network passphrase |
65
+ ### 1. Configure the client (browser)
73
66
 
74
- ### Methods
67
+ ```ts
68
+ import { PasskeyKit } from "passkey-kit";
69
+
70
+ const kit = new PasskeyKit({
71
+ rpcUrl: "https://soroban-testnet.stellar.org",
72
+ networkPassphrase: "Test SDF Network ; September 2015",
73
+ // Canonical v1 smart-wallet WASM hash (testnet); see docs/deployments-*.md
74
+ walletWasmHash: "fdefad64b96837147e1c333e51f537b696eab925e9f147e63d597c04e3c903f0",
75
+ });
76
+ ```
75
77
 
76
- #### `createWallet(app, user, settings?)`
77
- Creates a new passkey and deploys a smart wallet.
78
+ ### 2. Create a wallet
79
+
80
+ `createWallet` runs the passkey registration ceremony and builds a **signed** deploy transaction. Submission is a separate, server-side step (below).
78
81
 
79
82
  ```ts
80
- const { rawResponse, keyId, keyIdBase64, contractId, signedTx } = await account.createWallet(
81
- 'My App', // App name shown in passkey prompt
82
- 'user@example.com', // User identifier
83
- {
84
- rpId?: string, // Relying party ID
85
- authenticatorSelection?: AuthenticatorSelectionCriteria
86
- }
87
- )
83
+ const { keyIdBase64, contractId, signedTx } = await kit.createWallet(
84
+ "My App", // shown in the passkey prompt
85
+ "user@example.com", // user identifier
86
+ );
87
+ // `signedTx` is a base64 XDR string ready to submit; `contractId` is the wallet address (C…).
88
88
  ```
89
89
 
90
- #### `createKey(app, user, settings?)`
91
- Creates a new passkey without deploying a wallet.
90
+ ### 3. Submit through the server
91
+
92
+ `PasskeyServer` submits via the relayer, which pays the fees. It never throws for expected failures — branch on `result.success`.
92
93
 
93
94
  ```ts
94
- const { rawResponse, keyId, keyIdBase64, publicKey } = await account.createKey(
95
- 'My App',
96
- 'user@example.com',
97
- { rpId?: string, authenticatorSelection?: AuthenticatorSelectionCriteria }
98
- )
95
+ import { PasskeyServer } from "passkey-kit/server";
96
+
97
+ const server = new PasskeyServer({
98
+ networkPassphrase: "Test SDF Network ; September 2015",
99
+ rpcUrl: "https://soroban-testnet.stellar.org",
100
+ relayer: {
101
+ baseUrl: process.env.RELAYER_BASE_URL!, // e.g. https://channels.openzeppelin.com/testnet
102
+ apiKey: process.env.RELAYER_API_KEY!,
103
+ },
104
+ });
105
+
106
+ const result = await server.send(signedTx);
107
+ if (result.success) {
108
+ console.log("deployed in tx", result.hash);
109
+ } else {
110
+ console.error(`[${result.error.code}] ${result.error.message}`);
111
+ }
99
112
  ```
100
113
 
101
- #### `connectWallet(opts?)`
102
- Connects to an existing wallet using a passkey.
114
+ ### 4. Reconnect later
103
115
 
104
116
  ```ts
105
- const { rawResponse, keyId, keyIdBase64, contractId } = await account.connectWallet({
106
- rpId?: string,
107
- keyId?: string | Uint8Array, // Skip passkey prompt if provided
108
- getContractId?: (keyId: string) => Promise<string | undefined>, // Lookup function
109
- walletPublicKey?: string // For backwards compatibility
110
- })
117
+ const { contractId } = await kit.connectWallet();
118
+ // Resolves the wallet from the passkey and VERIFIES the passkey is a live signer on it.
111
119
  ```
112
120
 
113
- #### `sign(txn, options?)`
114
- Signs all auth entries for the connected wallet in a transaction.
121
+ ### 5. Sign & submit a transfer
122
+
123
+ Build any Soroban transaction, sign its wallet auth entries with the connected passkey, then submit.
115
124
 
116
125
  ```ts
117
- const signedTxn = await account.sign(
118
- txn, // AssembledTransaction | Tx | string (XDR)
119
- {
120
- rpId?: string,
121
- keyId?: 'any' | string | Uint8Array, // 'any' allows any passkey
122
- keypair?: Keypair, // Sign with Ed25519 instead
123
- policy?: string, // Sign with policy instead
124
- expiration?: number // Ledger expiration
125
- }
126
- )
126
+ import { SACClient } from "passkey-kit";
127
+
128
+ const sac = new SACClient({
129
+ rpcUrl: "https://soroban-testnet.stellar.org",
130
+ networkPassphrase: "Test SDF Network ; September 2015",
131
+ });
132
+ const token = sac.getSACClient("C…nativeSacId");
133
+
134
+ const tx = await token.transfer({ from: kit.contractId!, to: "C…recipient", amount: 10_000_000n });
135
+ await kit.sign(tx); // default signer = the connected passkey
136
+ const res = await server.send(tx);
127
137
  ```
128
138
 
129
- #### `signAuthEntry(entry, options?)`
130
- Signs a single authorization entry. Same options as `sign()`.
139
+ ## `PasskeyKit` (client)
140
+
141
+ Browser-side facade for wallet lifecycle and signing. Holds no secrets.
142
+
143
+ ### Configuration
144
+
145
+ | Option | Type | Required | Description |
146
+ |---|---|---|---|
147
+ | `rpcUrl` | `string` | Yes | Stellar RPC URL. |
148
+ | `networkPassphrase` | `string` | Yes | Network passphrase. |
149
+ | `walletWasmHash` | `string` (hex) | Yes | Smart-wallet WASM hash used to deploy new wallets. |
150
+ | `rpId` | `string` | No | WebAuthn Relying Party id (domain). Defaults to the current origin. |
151
+ | `deploySource` | `string` (`S…`) | No | Secret key for the fee-paying deployer. Defaults to the canonical deterministic deployer. **Overriding it changes derived wallet addresses** (see [Deterministic derivation](#deterministic-derivation)). |
152
+ | `timeoutInSeconds` | `number` | No | Transaction time bound (default `30`; the relayer requires `<= 30`). |
153
+ | `storage` | `StorageAdapter` | No | Passkey-record persistence (see [Storage adapters](#storage-adapters)). |
154
+ | `WebAuthn` | `WebAuthnClient` | No | Custom `startRegistration`/`startAuthentication` (for testing). |
155
+
156
+ ### Properties
157
+
158
+ | Property | Type | Description |
159
+ |---|---|---|
160
+ | `keyId` | `string \| undefined` | Connected passkey's base64url credential id. |
161
+ | `wallet` | `PasskeyClient \| undefined` | Connected wallet's generated contract client. |
162
+ | `contractId` | `string \| undefined` | Connected wallet address (getter). |
163
+ | `deployerPublicKey` | `string` | The fee-paying deployer's `G…` address (getter). |
164
+ | `networkPassphrase` / `rpcUrl` / `walletWasmHash` / `rpId` | `string` | The resolved config. |
165
+ | `events` | `PasskeyEventEmitter` | Lifecycle events: `walletCreated`, `walletConnected`, `walletDisconnected`. |
166
+
167
+ ### Lifecycle methods
168
+
169
+ | Method | Returns | Description |
170
+ |---|---|---|
171
+ | `createKey(appName, userName, options?)` | `CreatedPasskey` | Run a passkey registration ceremony **without** deploying a wallet. |
172
+ | `createWallet(appName, userName, options?)` | `CreateWalletResult` | Register a passkey and build a signed deploy transaction (initializes the wallet via `__constructor` with the passkey as the first signer). Submit the returned `signedTx` via `PasskeyServer`. |
173
+ | `connectWallet(options?)` | `ConnectWalletResult` | Resolve a wallet from a passkey (derivation → storage → injected indexer lookup) and **verify** the passkey is a live signer on it. |
174
+ | `disconnect()` | `void` | Clear the connected wallet/keyId. |
175
+ | `requireWallet()` | `PasskeyClient` | Return the connected wallet or throw `WalletNotConnectedError`. |
176
+
177
+ `options` for `createKey`/`createWallet` is `{ authenticatorSelection?: AuthenticatorSelectionCriteria }`.
178
+
179
+ `connectWallet` options:
180
+
181
+ | Option | Type | Description |
182
+ |---|---|---|
183
+ | `keyId` | `string \| Uint8Array` | Connect a specific credential, skipping the discovery ceremony. |
184
+ | `getContractId` | `(keyId: string) => Promise<string \| undefined>` | Indexer-backed keyId → wallet lookup, used only when derivation and storage both miss. |
185
+ | `verifyWasmHash` | `boolean` | Also assert the wallet's on-chain WASM hash equals `walletWasmHash`. Off by default (an upgraded wallet legitimately runs a different hash). |
186
+
187
+ ### Signing methods
188
+
189
+ | Method | Returns | Description |
190
+ |---|---|---|
191
+ | `sign(txn, signer?, options?)` | `AssembledTransaction<T>` | Sign every wallet auth entry of an assembled transaction. `signer` defaults to a `PasskeySigner()` for the connected passkey. |
192
+ | `signAuthEntry(entry, signer?, options?)` | `xdr.SorobanAuthorizationEntry` | Sign a single auth entry. |
193
+
194
+ `options` is `{ expiration?: number }` — the signature-expiration ledger (defaults to the configured timeout window).
195
+
196
+ > [!IMPORTANT]
197
+ > `sign`/`signAuthEntry` now take a **`Signer` instance** as the second argument, replacing the old `sign(txn, { keyId | keypair | policy })` option trio. See [Signers](#signers).
198
+
199
+ ## Signers
200
+
201
+ A `Signer` authenticates a signature payload and produces the on-chain `(SignerKey, Signature)` pair the wallet's `__check_auth` expects. Three concrete signers cover the three signer kinds:
202
+
203
+ | Signer | Constructs | Notes |
204
+ |---|---|---|
205
+ | `new PasskeySigner(keyId?)` | secp256r1 WebAuthn assertion | `keyId` selects a credential; `"any"` lets the authenticator pick a discoverable one; omit to use the kit's connected passkey. **Default** for `sign`. |
206
+ | `new Ed25519Signer(keypair)` / `Ed25519Signer.fromSecret("S…")` | Ed25519 signature | Local Stellar keypair. `.address` returns the `G…` key. |
207
+ | `new PolicySigner(policyAddress)` | policy authorization | No signature bytes; the wallet invokes the policy's `policy__` during `__check_auth`. |
131
208
 
132
209
  ```ts
133
- const signedEntry = await account.signAuthEntry(entry, options)
134
- ```
210
+ import { PasskeySigner, Ed25519Signer, PolicySigner } from "passkey-kit";
135
211
 
136
- #### Signer Management
212
+ await kit.sign(tx); // connected passkey
213
+ await kit.sign(tx, new Ed25519Signer(keypair)); // Ed25519
214
+ await kit.sign(tx, new PolicySigner("C…policyAddr")); // policy co-sign
215
+ await kit.sign(tx, new PasskeySigner("any")); // any discoverable passkey
216
+ ```
137
217
 
138
- Add, update, or remove signers from the wallet.
218
+ Multi-sign by signing the same transaction with several signers in turn — each merges its entry into the flat `Signatures` map (host-ordered):
139
219
 
140
220
  ```ts
141
- // Add signers
142
- await account.addSecp256r1(keyId, publicKey, limits, store, expiration?)
143
- await account.addEd25519(publicKey, limits, store, expiration?)
144
- await account.addPolicy(policy, limits, store, expiration?)
145
-
146
- // Update signers
147
- await account.updateSecp256r1(keyId, publicKey, limits, store, expiration?)
148
- await account.updateEd25519(publicKey, limits, store, expiration?)
149
- await account.updatePolicy(policy, limits, store, expiration?)
150
-
151
- // Remove signer
152
- await account.remove(signerKey)
221
+ await kit.sign(tx, new PasskeySigner());
222
+ await kit.sign(tx, new Ed25519Signer(cosignerKeypair));
153
223
  ```
154
224
 
155
- **Parameters:**
156
- - `keyId` - Passkey ID (string or Uint8Array)
157
- - `publicKey` - Public key (string or Uint8Array for Secp256r1, Stellar public key for Ed25519)
158
- - `policy` - Policy contract address
159
- - `limits` - `SignerLimits` (see Types below)
160
- - `store` - `SignerStore.Persistent` or `SignerStore.Temporary`
161
- - `expiration` - Optional ledger expiration
225
+ ## Signer management
226
+
227
+ Each method builds an `AssembledTransaction` (`WalletTx`) that wraps one contract admin function submit it via `PasskeyServer.send`. A wallet must be connected.
162
228
 
163
- ---
229
+ | Method | Wrapped contract fn | Description |
230
+ |---|---|---|
231
+ | `addSecp256r1(keyId, publicKey, limits, store, expiration?)` | `add_signer` | Add a passkey signer. |
232
+ | `updateSecp256r1(keyId, limits, store, expiration?)` | `update_signer` | Update a passkey signer's limits/storage/expiration. The public key is re-read from the ledger, never caller-supplied. |
233
+ | `addEd25519(publicKey, limits, store, expiration?)` | `add_signer` | Add an Ed25519 signer (`publicKey` = `G…`). |
234
+ | `updateEd25519(publicKey, limits, store, expiration?)` | `update_signer` | Update an Ed25519 signer. |
235
+ | `addPolicy(policy, limits, store, expiration?)` | `add_signer` | Add a policy signer (`policy` = `C…`). Invokes the policy's `install` hook. |
236
+ | `updatePolicy(policy, limits, store, expiration?)` | `update_signer` | Update a policy signer. |
237
+ | `remove(signerKey)` | `remove_signer` | Remove a signer. No policy code runs on this path. |
238
+ | `upgrade(newWasmHash)` | `upgrade` | Replace the wallet's WASM (`Buffer`/`Uint8Array`, 32 bytes). |
239
+ | `getSigner(signerKey)` | `get_signer` | Read a signer entry from the ledger (temporary before persistent). Returns `SignerVal \| null`. |
164
240
 
165
- ## PasskeyServer (Server)
241
+ Parameters:
166
242
 
167
- Server-side utilities for Mercury indexing and OpenZeppelin Relayer.
243
+ - `keyId` passkey credential id (base64url `string` or raw `Uint8Array`).
244
+ - `publicKey` — 65-byte secp256r1 key (`string`/`Uint8Array`) for passkeys, or a `G…` Stellar public key for Ed25519.
245
+ - `policy` — policy contract address (`C…`).
246
+ - `limits` — [`SignerLimits`](#signerlimits) (`undefined` = fully unlimited).
247
+ - `store` — `SignerStore.Persistent` or `SignerStore.Temporary`.
248
+ - `expiration` — optional UNIX-timestamp (seconds) after which the signer is invalid.
168
249
 
169
- ### Constructor
250
+ > [!IMPORTANT]
251
+ > **Signer key material comes from the ledger or the authenticator, not from
252
+ > an indexer.** Indexer rows (`getSigners`, `WalletSigner.publicKey`) are for
253
+ > display/lookup only. The kit's `updateSecp256r1` re-reads the key from the
254
+ > ledger, and `addSecp256r1` should only ever receive the key from the
255
+ > WebAuthn `createKey`/`connectWallet` attestation. If you build
256
+ > `update_signer`/`add_signer` transactions yourself (e.g. via
257
+ > `buildSecp256r1SignerTx`), source key material from the ledger
258
+ > (`getSigner`) or the authenticator.
170
259
 
171
260
  ```ts
172
- const server = new PasskeyServer({
173
- rpcUrl?: string,
174
- relayerUrl?: string, // OpenZeppelin Relayer URL
175
- relayerApiKey?: string, // Relayer API key
176
- mercuryProjectName?: string, // Mercury project name
177
- mercuryUrl?: string, // Mercury URL
178
- mercuryJwt?: string, // Mercury JWT (use either JWT or Key)
179
- mercuryKey?: string // Mercury API key
180
- })
261
+ import { SignerStore, SignerKey } from "passkey-kit";
262
+
263
+ // Add an unlimited Ed25519 co-signer, stored persistently.
264
+ const tx = await kit.addEd25519("G…", undefined, SignerStore.Persistent);
265
+ await kit.sign(tx); // authorize with the connected passkey
266
+ await server.send(tx);
267
+
268
+ // Remove it.
269
+ const rm = await kit.remove(SignerKey.Ed25519("G…"));
270
+ await kit.sign(rm);
271
+ await server.send(rm);
181
272
  ```
182
273
 
274
+ ## `PasskeyServer` (server)
275
+
276
+ Server-only facade (`passkey-kit/server`). Holds the relayer secret — never import it from browser code.
277
+
278
+ ### Configuration
279
+
280
+ | Option | Type | Required | Description |
281
+ |---|---|---|---|
282
+ | `networkPassphrase` | `string` | Yes | Network passphrase. |
283
+ | `rpcUrl` | `string` | No | Stellar RPC URL. Enables the temporary-signer eviction probe in `getSigners`. |
284
+ | `relayer` | `RelayerClientConfig` | No | Fee-sponsored submission (below). |
285
+ | `mercury` | `MercuryConfig` | No | Mercury's keyless hosted passkey-indexer (below). |
286
+
287
+ `RelayerClientConfig`: `{ baseUrl: string, apiKey: string, adminSecret?: string, timeout?: number }` (default timeout 6 min).
288
+ `MercuryConfig`: `{ url?: string }` — the keyless passkey-indexer base URL; **defaults to the network's hosted endpoint** (`https://{testnet,mainnet}.mercurydata.app/rest/passkey-indexer`), so it can be omitted.
289
+
183
290
  ### Methods
184
291
 
185
- #### `getSigners(contractId)`
186
- Get all signers for a wallet from Mercury.
292
+ | Method | Returns | Description |
293
+ |---|---|---|
294
+ | `send(input, options?)` | `TransactionResult` | Submit an `AssembledTransaction \| Transaction \| string` via the relayer. Picks the `{ func, auth }` Soroban path for wallet invocations and the `{ xdr }` fee-bump path for deploys / source-account auth. **Never throws.** |
295
+ | `getTransaction(transactionId)` | `TransactionResult` | Poll a `skipWait` submission by its relayer id. |
296
+ | `getSigners(contractId)` | `WalletSigner[]` | Enumerate a wallet's signers via the indexer (flags evicted temporary signers when `rpcUrl` is set). |
297
+ | `getContractId(options, index?)` | `string \| undefined` | Reverse lookup: the wallet address for a signer. `options` = exactly one of `{ keyId }`, `{ publicKey }`, `{ policy }`. |
298
+
299
+ `options` for `send`/`getTransaction` is `{ skipWait?: boolean, fundRelayerId?: string }`. `getSigners`/`getContractId` delegate to a [`MercuryIndexer`](#discovery-indexer) over the keyless hosted endpoint.
300
+
301
+ ## Submission (relayer)
302
+
303
+ All wallet writes are fee-sponsored by the [OpenZeppelin Relayer Channels](https://docs.openzeppelin.com/relayer/stellar) service: a channel account builds/pays for the transaction so the wallet holds no XLM. `PasskeyServer.send` routes to the relayer and returns a discriminated [`TransactionResult`](#errors).
304
+
305
+ Two submission modes are chosen automatically:
306
+
307
+ - **`{ func, auth }`** (`submitSorobanTransaction`) — for wallet invocations (transfers, signer management) whose auth is carried by Address credentials. The relayer builds the envelope.
308
+ - **`{ xdr }`** (`submitTransaction`) — for an already-signed envelope that needs a fee bump (deploys / source-account auth).
309
+
310
+ ### Browsers: the relayer-proxy worker
311
+
312
+ The relayer API key is a secret, so a browser must never hold it. The [`relayer-proxy/`](./relayer-proxy) Cloudflare Worker fronts the relayer and **mints one API key per client IP** (keyless, cached in a per-IP Durable Object). The browser POSTs `{ func, auth }` / `{ xdr }` to the worker with **zero secrets in the bundle**. See [relayer-proxy/README.md](./relayer-proxy/README.md).
313
+
314
+ ## Discovery (indexer)
315
+
316
+ Because every wallet address is derived deterministically from its passkey credential id (see [Deterministic derivation](#deterministic-derivation)), the primary "reconnect" path needs no indexer — `connectWallet` re-derives the address and confirms ownership on-chain. An indexer is for **richer discovery**: enumerating a wallet's full signer set, and reverse-looking-up which wallets a given signer belongs to.
317
+
318
+ The SDK abstracts discovery behind a `SignerIndexer` interface (`getSigners` / `findWallets` / `health`), implemented by the **keyless** `MercuryIndexer` — exported from the main `passkey-kit` entry (no secret, so it runs in the browser):
319
+
320
+ | Backend | Config | Wire | Status |
321
+ |---|---|---|---|
322
+ | `MercuryIndexer` | `MercuryIndexerConfig` (`url?`, `rpc?`) | Keyless REST (`GET /api/wallet/*`, `/api/lookup/*`) | **Live on testnet + mainnet** — both signer generations, full history. Resolve with `MercuryIndexer.forNetwork(...)`. |
323
+
324
+ > [!NOTE]
325
+ > Mercury's hosted passkey-indexer is public and **keyless**, covering **testnet and mainnet** with full history across both signer generations (legacy `("sw_v1", …)` tuples and the v1 typed `#[contractevent]`s). It returns fully-decoded signers, so the client maps JSON straight onto `WalletSigner`. Resolve the base URL per network with `MercuryIndexer.forNetwork({ rpc? }, networkPassphrase)` (returns `null` off testnet/mainnet); passing an `rpc` lets it flag evicted temporary signers and confirm reverse-lookup candidates on-chain.
187
326
 
188
327
  ```ts
189
- const signers: Signer[] = await server.getSigners('C...')
328
+ const indexer = MercuryIndexer.forNetwork({ rpc }, networkPassphrase);
329
+ const wallets = await lookupWithRetry(() => indexer!.findWallets(SignerKey.Secp256r1(keyId)));
190
330
  ```
191
331
 
192
- #### `getContractId(options, index?)`
193
- Reverse lookup a wallet address from a signer.
332
+ `lookupWithRetry(fn, { attempts?, delayMs?, predicate? })` (browser-safe) polls a lookup until it returns a non-empty result — useful right after a write, while the indexer catches up to the ledger.
333
+
334
+ ## Tokens (`SACClient`)
335
+
336
+ Helper for [Stellar Asset Contracts](https://developers.stellar.org/docs/tokens/stellar-asset-contract) (SEP-41): balances, metadata, and transfers.
194
337
 
195
338
  ```ts
196
- const contractId = await server.getContractId({
197
- keyId?: string, // Passkey ID (Secp256r1)
198
- publicKey?: string, // Ed25519 public key
199
- policy?: string // Policy address
200
- }, index) // If multiple wallets, select by index (default: 0)
339
+ import { SACClient, buildTokenTransferHostFunction } from "passkey-kit";
340
+
341
+ const sac = new SACClient({ rpcUrl, networkPassphrase });
342
+ const token = sac.getSACClient("C…tokenId");
343
+
344
+ const tx = await token.transfer({ from: kit.contractId!, to, amount: 1_000_000n });
345
+ await kit.sign(tx);
346
+ await server.send(tx);
201
347
  ```
202
348
 
203
- #### `send(txn)`
204
- Submit a transaction via OpenZeppelin Relayer.
349
+ `buildTokenTransferHostFunction(token, from, to, amountInStroops)` builds a raw `transfer` host function for the low-level relayer `{ func, auth }` path when you don't need a full client.
350
+
351
+ ## Storage adapters
352
+
353
+ Persist the passkey → wallet association so `connectWallet` can resolve a wallet from a keyId without an indexer. Import from `passkey-kit/storage` and pass to the kit's `storage` config.
354
+
355
+ | Adapter | Backing store | Use |
356
+ |---|---|---|
357
+ | `IndexedDBStorage` | IndexedDB | Browser (recommended). |
358
+ | `LocalStorageAdapter` | `localStorage` | Browser (simple/synchronous). |
359
+ | `MemoryStorage` | in-memory | Tests / SSR. |
205
360
 
206
361
  ```ts
207
- const result = await server.send(txn) // AssembledTransaction | Tx | string
362
+ import { IndexedDBStorage } from "passkey-kit/storage";
363
+ const kit = new PasskeyKit({ rpcUrl, networkPassphrase, walletWasmHash, storage: new IndexedDBStorage() });
208
364
  ```
209
365
 
210
- ---
366
+ All adapters implement `StorageAdapter` (`save` / `get` / `getByContract` / `getAll` / `delete` / `update` / `clear`) over `StoredPasskey` records.
211
367
 
212
- ## SACClient
368
+ ## Errors
213
369
 
214
- Helper for interacting with Stellar Asset Contracts.
370
+ Every error the kit throws is a `PasskeyKitError` (or subclass) carrying a numeric `code`, optional `context`, and `cause`. Branch on `error.code` (or `instanceof`) — never on message strings.
215
371
 
216
- ```ts
217
- const sac = new SACClient({
218
- networkPassphrase: string,
219
- rpcUrl: string
220
- })
372
+ **One deliberate exception:** submission methods (`server.send`, `getTransaction`) **do not throw** for expected on-chain/relayer failures. They return a discriminated `TransactionResult`:
221
373
 
222
- const tokenClient = sac.getSACClient('C...') // SAC contract ID
374
+ ```ts
375
+ const result = await server.send(tx);
376
+ if (result.success) {
377
+ // TransactionSuccess: { success: true, hash, ledger?, transactionId? }
378
+ console.log(result.hash);
379
+ } else {
380
+ // TransactionFailure: { success: false, error: PasskeyKitError, hash? }
381
+ if (result.error instanceof ContractError && result.error.contractErrorName === "SignerExpired") {
382
+ // handle an on-chain contract failure by its decoded name
383
+ }
384
+ }
223
385
  ```
224
386
 
225
- ---
387
+ Error classes: `ConfigurationError`, `WalletNotConnectedError`, `WalletOwnershipError`, `WebAuthnError`, `SigningError`, `SignerNotFoundError`, `SimulationError`, `SubmissionError`, `ValidationError`, `IndexerError`, `RelayerError`, and `ContractError`. Codes are grouped by concern (`1xxx` config, `2xxx` wallet, `3xxx` WebAuthn, `4xxx` signing, `5xxx` transaction, `6xxx` indexer, `7xxx` relayer, `8xxx` validation, `9xxx` storage, `10000` contract).
388
+
389
+ ### Contract error decoding
390
+
391
+ On-chain failures surface as `Error(Contract, #N)` in diagnostics. `decodeContractError`, `contractErrorFromCode`, and `CONTRACT_ERROR_REGISTRY` map the code to a typed `ContractError` with its enum name. The v1 contract renumbered its error space to **100–129** (disjoint from the legacy 1–9 range, which is still decoded as family `SmartWalletLegacy`):
392
+
393
+ | Code | Name | Meaning |
394
+ |---|---|---|
395
+ | 100 | `SignerNotFound` | The requested signer does not exist. |
396
+ | 101 | `SignerAlreadyExists` | `add_signer` on an existing key. |
397
+ | 102 | `SignerExpired` | Expiration timestamp is in the past. |
398
+ | 110 | `MissingContext` | No signer in the map may authorize a requested context. |
399
+ | 111 | `SignatureKeyValueMismatch` | A signature's variant doesn't match its stored signer. |
400
+ | 120 | `ClientDataJsonTooLarge` | `clientDataJSON` exceeds the 1024-byte parse buffer. |
401
+ | 121 | `ClientDataJsonParseError` | `clientDataJSON` is not parseable. |
402
+ | 122 | `ClientDataJsonChallengeIncorrect` | WebAuthn challenge ≠ signature payload (binding). |
403
+ | 123 | `InvalidWebAuthnType` | `type` is not `"webauthn.get"`. |
404
+ | 124 | `InvalidAuthenticatorData` | `authenticatorData` shorter than 37 bytes. |
405
+ | 125 | `UserPresenceRequired` | Authenticator did not set the User Present (UP) flag. |
226
406
 
227
407
  ## Types
228
408
 
229
- ### SignerKey
409
+ ### `SignerKey`
410
+
411
+ Identifies a signer. The `value` is the string you work with.
230
412
 
231
413
  ```ts
232
- SignerKey.Policy(contractAddress) // Policy signer
233
- SignerKey.Ed25519(publicKey) // Ed25519 signer
234
- SignerKey.Secp256r1(keyId) // Passkey signer
414
+ SignerKey.Secp256r1(keyId) // base64url passkey credential id
415
+ SignerKey.Ed25519(publicKey) // G… public key
416
+ SignerKey.Policy(address) // C… policy contract
235
417
  ```
236
418
 
237
- ### SignerLimits
419
+ ### `SignerLimits`
238
420
 
239
421
  ```ts
240
- type SignerLimits = Map<string, SignerKey[] | undefined> | undefined
241
-
242
- // Example: Limit signer to specific contract, requires co-signer
243
- const limits = new Map([
244
- ['C...contractAddress', [SignerKey.Ed25519('G...')]]
245
- ])
422
+ type SignerLimits = Map<string, SignerKey[] | undefined> | undefined;
246
423
  ```
247
424
 
248
- ### SignerStore
425
+ - `undefined` (whole map) — **fully unlimited**: may authorize anything, including deploys and this wallet's own admin functions.
426
+ - `Map` present but a contract → `undefined` — may authorize any call to that contract, no co-signers.
427
+ - `Map` present, contract → `[keys]` — may authorize calls to that contract **only if every listed key also approves** (required co-signers).
249
428
 
250
429
  ```ts
251
- enum SignerStore {
252
- Persistent = 'Persistent', // Permanent storage
253
- Temporary = 'Temporary' // Expires, cheaper
254
- }
430
+ // This signer may only call C…token, and only alongside a passkey co-signer.
431
+ const limits = new Map([["C…token", [SignerKey.Secp256r1(keyId)]]]);
255
432
  ```
256
433
 
257
- ### Signer
434
+ > [!IMPORTANT]
435
+ > **v1 breaking change.** `Some(empty map)` now means **no permissions (fail-closed)** — pre-1.0 an empty map meant *unlimited*. Deploy permission is no longer grantable through a limits entry: `CreateContract*` contexts require a fully unlimited (`undefined`-limits) signer.
436
+
437
+ ### `SignerStore`
258
438
 
259
439
  ```ts
260
- type Signer = {
261
- kind: string // 'Secp256r1' | 'Ed25519' | 'Policy'
262
- key: string // Signer identifier
263
- val: string // Public key or empty
264
- expiration: number | null
265
- storage: 'Persistent' | 'Temporary'
266
- limits: string // JSON stringified limits
267
- evicted?: boolean // True if temporary signer was evicted
268
- }
440
+ enum SignerStore { Persistent = "Persistent", Temporary = "Temporary" }
269
441
  ```
270
442
 
271
- ---
443
+ `Temporary` entries are cheaper but **can be evicted** when their ledger TTL lapses — see [Caveats](#caveats).
272
444
 
273
- ## Deploy the Mercury Indexer
445
+ ### Expiration
274
446
 
275
- To track signers and reverse lookup wallet addresses, deploy the Zephyr program:
447
+ Signer and signature expiration are **UNIX timestamps in seconds** (inclusive: valid while `now <= expiration`). Pre-1.0 these were ledger sequence numbers; timestamps don't drift as ledger close-time changes.
276
448
 
277
- ```bash
278
- cd ./zephyr
279
- cargo install mercury-cli
280
- # Get a JWT from https://test.mercurydata.app
281
- export MERCURY_JWT="<YOUR.MERCURY.JWT>"
282
- # Requires Rust 1.79.0+
283
- mercury-cli --jwt $MERCURY_JWT --local false --mainnet false deploy
284
- ```
449
+ ## Caveats
285
450
 
286
- ---
451
+ > [!WARNING]
452
+ > These are inherent to the wallet model. The SDK does not guard against them — handle them in your app.
287
453
 
288
- ## TypeScript Configuration
454
+ - **Keep at least one durable admin signer.** The contract rejects any change that would remove or demote its last durable (`Persistent`, non-expiring) admin signer (`LastAdminSigner = 103`) or leave it without any durable signer (`LastSigner = 104`), so a wallet always retains one signer that cannot evict or expire. Signers outside that guard — `Temporary` storage or with an expiration — lapse on their own: add a replacement *before* removing or demoting an existing signer.
455
+ - **The default deployer is a shared, public keypair.** It only pays fees and salts the deploy (it never controls the wallet), but its determinism is load-bearing for discovery. Overriding `deploySource` changes every derived address and breaks keyId → wallet lookup. See [Deterministic derivation](#deterministic-derivation).
456
+ - **Deploy front-running.** Because the deployer is public and the WASM is not part of the address preimage, anyone who learns a `keyId` before the wallet is deployed could deploy other code at the derived address. `connectWallet` mitigates this by verifying the keyId is a live signer (and, with `verifyWasmHash: true`, checking the on-chain WASM hash) — never trust a bare derived/looked-up address without that check.
457
+ - **WebAuthn requires User Presence (UP), not User Verification (UV).** The contract requires the UP flag but not UV (biometric/PIN), so it stays compatible with non-UV authenticators. Enforce UV at the client/relayer layer if you need it.
458
+ - **Value-moving policies need a cumulative cap or a co-signer.** A `Signature::Policy` carries no secret, so a per-transfer cap alone is trivially drained by repeated capped transfers. See the [contract interface](#contract-interface) and `sample-policy`.
289
459
 
290
- This library exports TypeScript only to avoid bundling `@stellar/stellar-sdk` twice. Configure your bundler to transpile it.
460
+ ## Contract interface
291
461
 
292
- **Next.js** (`next.config.mjs`):
293
- ```mjs
294
- /** @type {import('next').NextConfig} */
295
- const nextConfig = {
296
- transpilePackages: [
297
- 'passkey-kit',
298
- 'passkey-factory-sdk',
299
- 'passkey-kit-sdk',
300
- 'sac-sdk',
301
- ]
302
- }
462
+ The wallet is a Soroban smart contract (`soroban-sdk 27`, `wasm32v1-none`). Every user wallet is a separate instance deployed with a `Signer` constructor argument.
303
463
 
304
- export default nextConfig
305
- ```
464
+ **Functions:** `__constructor(signer)` · `add_signer(signer)` · `update_signer(signer)` · `remove_signer(signer_key)` · `upgrade(new_wasm_hash)` · `get_signer(signer_key) -> Option<SignerVal>`. Admin functions require wallet auth (`__check_auth`).
306
465
 
307
- ---
466
+ **Signer kinds:** `Policy(Address)` · `Ed25519(BytesN<32>)` · `Secp256r1(Bytes keyId)`, each with a `SignerExpiration`, `SignerLimits`, and `SignerStorage`.
308
467
 
309
- ## Contributing
468
+ **Auth (`__check_auth`):** a flat `Signatures` map (`SignerKey → Signature`) signed over the plain signature payload. Pass 1 checks every requested context is covered by some permitted, unexpired signer; pass 2 verifies **every** entry in the map (existence, expiration, crypto/policy). Include only the signatures you need.
310
469
 
311
- ```bash
312
- # Install dependencies
313
- pnpm i
470
+ **Policy lifecycle:** policy signers get an `install(wallet)` hook on add (a hard call — a panic aborts the add) and a permissionless `uninstall(wallet)` self-clean entrypoint. `policy__` is publicly callable — stateful policies must authenticate the caller (`source.require_auth()`).
471
+
472
+ **Events** (`#[contractevent]`, SEP-48 schema in the WASM): `signer_added` · `signer_updated` · `signer_removed` · `upgraded`. These replace the legacy `("sw_v1", …)` tuple events; indexers consume them directly.
473
+
474
+ See [`contracts/smart-wallet-interface/src/`](./contracts/smart-wallet-interface/src) for the canonical trait and types.
475
+
476
+ ### Deterministic derivation
314
477
 
315
- # Build
316
- pnpm run build
478
+ Every wallet address is derived from its passkey credential id (`keyId`) alone — this is what lets `connectWallet` and the indexers resolve a wallet without a lookup table:
317
479
 
318
- # Run demo
319
- cd ./demo && pnpm i && pnpm run start
480
+ ```text
481
+ contractId = sha256(XDR(HashIdPreimage::EnvelopeTypeContractId {
482
+ networkId: sha256(networkPassphrase),
483
+ contractIdPreimage: ContractIdPreimageFromAddress {
484
+ address: G-address of the canonical deployer keypair,
485
+ salt: sha256(keyId),
486
+ },
487
+ }))
320
488
  ```
321
489
 
322
- **Directory structure:**
323
- - `./src` - TypeScript SDK source
324
- - `./demo` - Demo application
325
- - `./contracts` - Rust Soroban smart contracts
326
- - `./zephyr` - Mercury Zephyr indexer program
490
+ - The canonical deployer keypair is `Keypair.fromRawEd25519Seed(sha256("kalepail"))`. It only pays fees and salts the deploy — it never controls the wallet — but its determinism is **load-bearing**: overriding `deploySource` changes every derived address and breaks keyId → wallet discovery.
491
+ - The WASM hash is deliberately **not** in the preimage, so an `upgrade` never moves a wallet's address.
327
492
 
328
- > [!IMPORTANT]
329
- > If modifying contracts in `./contracts`, run the make commands. Update `SMART_WALLET_FACTORY` and `SMART_WALLET_WASM` values from `make deploy` before running `make init`.
493
+ This tuple is normative and must never change. See [`docs/deployments-testnet-2026-07-11.md`](./docs/deployments-testnet-2026-07-11.md) for the canonical WASM hashes, the deployer `G…` address, and the full derivation spec (including the deploy-front-running consequence in [Caveats](#caveats)).
330
494
 
331
- > [!IMPORTANT]
332
- > The bindings in `./packages` have been heavily modified. When rebuilding, prefer updating only the `src/index.ts` files in each package.
495
+ ## Repository layout & development
496
+
497
+ | Path | Contents |
498
+ |---|---|
499
+ | `src/` | The `passkey-kit` SDK (client, server, signers, indexer, storage). |
500
+ | `packages/passkey-kit-sdk` | Generated smart-wallet contract bindings (do not hand-edit — see [releasing](./docs/releasing.md)). |
501
+ | `packages/sac-sdk` | Generated SEP-41 SAC bindings. |
502
+ | `contracts/` | Rust Soroban contracts: `smart-wallet`, `smart-wallet-interface`, `sample-policy`, `example-contract`. |
503
+ | `relayer-proxy/` | Cloudflare Worker for keyless, fee-sponsored submission. |
504
+ | `demo/` | Svelte 5 demo exercising the full client API. |
505
+
506
+ ```bash
507
+ pnpm install
508
+ pnpm build # regenerate bindings, compile to dist/, verify Node-ESM import
509
+ pnpm test # vitest (co-located src/*.test.ts)
510
+ pnpm verify:bindings # assert the committed bindings match the canonical WASM
511
+ ```
333
512
 
334
- ---
513
+ - **[CHANGELOG.md](./CHANGELOG.md)** — the v1 overhaul, by component.
514
+ - **[docs/migration-v1.md](./docs/migration-v1.md)** — upgrading from 0.12.x, with Before/After and a gap analysis.
515
+ - **[docs/releasing.md](./docs/releasing.md)** — the dependency-ordered publish flow.
335
516
 
336
517
  ## Resources
337
518
 
338
- - [Super Peach](https://github.com/kalepail/superpeach) - Real-world implementation example
339
- - [Discord #passkeys](https://discord.gg/stellardev) - Questions and showcase
519
+ - [Super Peach](https://github.com/kalepail/superpeach) a real-world implementation example.
520
+ - [Stellar Developers Discord `#passkeys`](https://discord.gg/stellardev) questions and showcase.