passkey-kit 0.12.1 → 0.13.1

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 +399 -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 +87 -0
  11. package/dist/constants.d.ts.map +1 -0
  12. package/dist/constants.js +111 -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 +129 -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 +88 -0
  39. package/dist/indexer/mercury.d.ts.map +1 -0
  40. package/dist/indexer/mercury.js +237 -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 +89 -0
  51. package/dist/kit/auth-payload.d.ts.map +1 -0
  52. package/dist/kit/auth-payload.js +203 -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 +92 -0
  59. package/dist/kit/tx-ops.d.ts.map +1 -0
  60. package/dist/kit/tx-ops.js +113 -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 +128 -0
  71. package/dist/kit.d.ts.map +1 -0
  72. package/dist/kit.js +276 -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 +51 -0
  83. package/dist/managers/signer-manager.d.ts.map +1 -0
  84. package/dist/managers/signer-manager.js +92 -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 +119 -0
  93. package/dist/relayer.js.map +1 -0
  94. package/dist/rpc-data.d.ts +35 -0
  95. package/dist/rpc-data.d.ts.map +1 -0
  96. package/dist/rpc-data.js +49 -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 +52 -0
  131. package/dist/utils.d.ts.map +1 -0
  132. package/dist/utils.js +153 -0
  133. package/dist/utils.js.map +1 -0
  134. package/dist/validation.d.ts +40 -0
  135. package/dist/validation.d.ts.map +1 -0
  136. package/dist/validation.js +73 -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 +45 -18
  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,511 @@
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.** The v1 smart-wallet contract underwent an internal multi-reviewer adversarial review and remediation (see the [CHANGELOG](./CHANGELOG.md)), but it has **not** been reviewed by a third-party security firm. Review it yourself before holding meaningful value, and read [Caveats & footguns](#caveats--footguns).
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 & footguns](#caveats--footguns)
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: "84924c53a413318df2ce753e30de53ec651404c916d30e861718ad155c94b319",
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
162
226
 
163
- ---
227
+ Each method builds an `AssembledTransaction` (`WalletTx`) that wraps one contract admin function — submit it via `PasskeyServer.send`. A wallet must be connected.
164
228
 
165
- ## PasskeyServer (Server)
229
+ | Method | Wrapped contract fn | Description |
230
+ |---|---|---|
231
+ | `addSecp256r1(keyId, publicKey, limits, store, expiration?)` | `add_signer` | Add a passkey signer. |
232
+ | `updateSecp256r1(keyId, publicKey, limits, store, expiration?)` | `update_signer` | Replace a passkey signer's value/storage. |
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`. |
166
240
 
167
- Server-side utilities for Mercury indexing and OpenZeppelin Relayer.
241
+ Parameters:
168
242
 
169
- ### Constructor
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.
170
249
 
171
250
  ```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
- })
251
+ import { SignerStore, SignerKey } from "passkey-kit";
252
+
253
+ // Add an unlimited Ed25519 co-signer, stored persistently.
254
+ const tx = await kit.addEd25519("G…", undefined, SignerStore.Persistent);
255
+ await kit.sign(tx); // authorize with the connected passkey
256
+ await server.send(tx);
257
+
258
+ // Remove it.
259
+ const rm = await kit.remove(SignerKey.Ed25519("G…"));
260
+ await kit.sign(rm);
261
+ await server.send(rm);
181
262
  ```
182
263
 
264
+ ## `PasskeyServer` (server)
265
+
266
+ Server-only facade (`passkey-kit/server`). Holds the relayer secret — never import it from browser code.
267
+
268
+ ### Configuration
269
+
270
+ | Option | Type | Required | Description |
271
+ |---|---|---|---|
272
+ | `networkPassphrase` | `string` | Yes | Network passphrase. |
273
+ | `rpcUrl` | `string` | No | Stellar RPC URL. Enables the temporary-signer eviction probe in `getSigners`. |
274
+ | `relayer` | `RelayerClientConfig` | No | Fee-sponsored submission (below). |
275
+ | `mercury` | `MercuryConfig` | No | Mercury's keyless hosted passkey-indexer (below). |
276
+
277
+ `RelayerClientConfig`: `{ baseUrl: string, apiKey: string, adminSecret?: string, timeout?: number }` (default timeout 6 min).
278
+ `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.
279
+
183
280
  ### Methods
184
281
 
185
- #### `getSigners(contractId)`
186
- Get all signers for a wallet from Mercury.
282
+ | Method | Returns | Description |
283
+ |---|---|---|
284
+ | `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.** |
285
+ | `getTransaction(transactionId)` | `TransactionResult` | Poll a `skipWait` submission by its relayer id. |
286
+ | `getSigners(contractId)` | `WalletSigner[]` | Enumerate a wallet's signers via the indexer (flags evicted temporary signers when `rpcUrl` is set). |
287
+ | `getContractId(options, index?)` | `string \| undefined` | Reverse lookup: the wallet address for a signer. `options` = exactly one of `{ keyId }`, `{ publicKey }`, `{ policy }`. |
288
+
289
+ `options` for `send`/`getTransaction` is `{ skipWait?: boolean, fundRelayerId?: string }`. `getSigners`/`getContractId` delegate to a [`MercuryIndexer`](#discovery-indexer) over the keyless hosted endpoint.
290
+
291
+ ## Submission (relayer)
292
+
293
+ 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).
294
+
295
+ Two submission modes are chosen automatically:
296
+
297
+ - **`{ func, auth }`** (`submitSorobanTransaction`) — for wallet invocations (transfers, signer management) whose auth is carried by Address credentials. The relayer builds the envelope.
298
+ - **`{ xdr }`** (`submitTransaction`) — for an already-signed envelope that needs a fee bump (deploys / source-account auth).
299
+
300
+ ### Browsers: the relayer-proxy worker
301
+
302
+ 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).
303
+
304
+ ## Discovery (indexer)
305
+
306
+ 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.
307
+
308
+ 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):
309
+
310
+ | Backend | Config | Wire | Status |
311
+ |---|---|---|---|
312
+ | `MercuryIndexer` | `MercuryIndexerConfig` (`url?`, `rpc?`) | Keyless REST (`GET /api/wallet/*`, `/api/lookup/*`) | **Live on testnet + mainnet** — both signer generations, full history. Resolve with `MercuryIndexer.forNetwork(...)`. |
313
+
314
+ > [!NOTE]
315
+ > 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
316
 
188
317
  ```ts
189
- const signers: Signer[] = await server.getSigners('C...')
318
+ const indexer = MercuryIndexer.forNetwork({ rpc }, networkPassphrase);
319
+ const wallets = await lookupWithRetry(() => indexer!.findWallets(SignerKey.Secp256r1(keyId)));
190
320
  ```
191
321
 
192
- #### `getContractId(options, index?)`
193
- Reverse lookup a wallet address from a signer.
322
+ `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.
323
+
324
+ ## Tokens (`SACClient`)
325
+
326
+ Helper for [Stellar Asset Contracts](https://developers.stellar.org/docs/tokens/stellar-asset-contract) (SEP-41): balances, metadata, and transfers.
194
327
 
195
328
  ```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)
329
+ import { SACClient, buildTokenTransferHostFunction } from "passkey-kit";
330
+
331
+ const sac = new SACClient({ rpcUrl, networkPassphrase });
332
+ const token = sac.getSACClient("C…tokenId");
333
+
334
+ const tx = await token.transfer({ from: kit.contractId!, to, amount: 1_000_000n });
335
+ await kit.sign(tx);
336
+ await server.send(tx);
201
337
  ```
202
338
 
203
- #### `send(txn)`
204
- Submit a transaction via OpenZeppelin Relayer.
339
+ `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.
340
+
341
+ ## Storage adapters
342
+
343
+ 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.
344
+
345
+ | Adapter | Backing store | Use |
346
+ |---|---|---|
347
+ | `IndexedDBStorage` | IndexedDB | Browser (recommended). |
348
+ | `LocalStorageAdapter` | `localStorage` | Browser (simple/synchronous). |
349
+ | `MemoryStorage` | in-memory | Tests / SSR. |
205
350
 
206
351
  ```ts
207
- const result = await server.send(txn) // AssembledTransaction | Tx | string
352
+ import { IndexedDBStorage } from "passkey-kit/storage";
353
+ const kit = new PasskeyKit({ rpcUrl, networkPassphrase, walletWasmHash, storage: new IndexedDBStorage() });
208
354
  ```
209
355
 
210
- ---
356
+ All adapters implement `StorageAdapter` (`save` / `get` / `getByContract` / `getAll` / `delete` / `update` / `clear`) over `StoredPasskey` records.
211
357
 
212
- ## SACClient
358
+ ## Errors
213
359
 
214
- Helper for interacting with Stellar Asset Contracts.
360
+ 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
361
 
216
- ```ts
217
- const sac = new SACClient({
218
- networkPassphrase: string,
219
- rpcUrl: string
220
- })
362
+ **One deliberate exception:** submission methods (`server.send`, `getTransaction`) **do not throw** for expected on-chain/relayer failures. They return a discriminated `TransactionResult`:
221
363
 
222
- const tokenClient = sac.getSACClient('C...') // SAC contract ID
364
+ ```ts
365
+ const result = await server.send(tx);
366
+ if (result.success) {
367
+ // TransactionSuccess: { success: true, hash, ledger?, transactionId? }
368
+ console.log(result.hash);
369
+ } else {
370
+ // TransactionFailure: { success: false, error: PasskeyKitError, hash? }
371
+ if (result.error instanceof ContractError && result.error.contractErrorName === "SignerExpired") {
372
+ // handle an on-chain contract failure by its decoded name
373
+ }
374
+ }
223
375
  ```
224
376
 
225
- ---
377
+ 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).
378
+
379
+ ### Contract error decoding
380
+
381
+ 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`):
382
+
383
+ | Code | Name | Meaning |
384
+ |---|---|---|
385
+ | 100 | `SignerNotFound` | The requested signer does not exist. |
386
+ | 101 | `SignerAlreadyExists` | `add_signer` on an existing key. |
387
+ | 102 | `SignerExpired` | Expiration timestamp is in the past. |
388
+ | 110 | `MissingContext` | No signer in the map may authorize a requested context. |
389
+ | 111 | `SignatureKeyValueMismatch` | A signature's variant doesn't match its stored signer. |
390
+ | 120 | `ClientDataJsonTooLarge` | `clientDataJSON` exceeds the 1024-byte parse buffer. |
391
+ | 121 | `ClientDataJsonParseError` | `clientDataJSON` is not parseable. |
392
+ | 122 | `ClientDataJsonChallengeIncorrect` | WebAuthn challenge ≠ signature payload (binding). |
393
+ | 123 | `InvalidWebAuthnType` | `type` is not `"webauthn.get"`. |
394
+ | 124 | `InvalidAuthenticatorData` | `authenticatorData` shorter than 37 bytes. |
395
+ | 125 | `UserPresenceRequired` | Authenticator did not set the User Present (UP) flag. |
226
396
 
227
397
  ## Types
228
398
 
229
- ### SignerKey
399
+ ### `SignerKey`
400
+
401
+ Identifies a signer. The `value` is the string you work with.
230
402
 
231
403
  ```ts
232
- SignerKey.Policy(contractAddress) // Policy signer
233
- SignerKey.Ed25519(publicKey) // Ed25519 signer
234
- SignerKey.Secp256r1(keyId) // Passkey signer
404
+ SignerKey.Secp256r1(keyId) // base64url passkey credential id
405
+ SignerKey.Ed25519(publicKey) // G… public key
406
+ SignerKey.Policy(address) // C… policy contract
235
407
  ```
236
408
 
237
- ### SignerLimits
409
+ ### `SignerLimits`
238
410
 
239
411
  ```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
- ])
412
+ type SignerLimits = Map<string, SignerKey[] | undefined> | undefined;
246
413
  ```
247
414
 
248
- ### SignerStore
415
+ - `undefined` (whole map) — **fully unlimited**: may authorize anything, including deploys and this wallet's own admin functions.
416
+ - `Map` present but a contract → `undefined` — may authorize any call to that contract, no co-signers.
417
+ - `Map` present, contract → `[keys]` — may authorize calls to that contract **only if every listed key also approves** (required co-signers).
249
418
 
250
419
  ```ts
251
- enum SignerStore {
252
- Persistent = 'Persistent', // Permanent storage
253
- Temporary = 'Temporary' // Expires, cheaper
254
- }
420
+ // This signer may only call C…token, and only alongside a passkey co-signer.
421
+ const limits = new Map([["C…token", [SignerKey.Secp256r1(keyId)]]]);
255
422
  ```
256
423
 
257
- ### Signer
424
+ > [!IMPORTANT]
425
+ > **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.
426
+
427
+ ### `SignerStore`
258
428
 
259
429
  ```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
- }
430
+ enum SignerStore { Persistent = "Persistent", Temporary = "Temporary" }
269
431
  ```
270
432
 
271
- ---
433
+ `Temporary` entries are cheaper but **can be evicted** when their ledger TTL lapses — see [Caveats](#caveats--footguns).
272
434
 
273
- ## Deploy the Mercury Indexer
435
+ ### Expiration
274
436
 
275
- To track signers and reverse lookup wallet addresses, deploy the Zephyr program:
437
+ 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
438
 
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
- ```
439
+ ## Caveats & footguns
440
+
441
+ > [!WARNING]
442
+ > These are inherent to the wallet model. The SDK does not guard against them — handle them in your app.
285
443
 
286
- ---
444
+ - **Don't remove your last usable signer → the wallet bricks.** The contract does not enforce a minimum signer count. If you remove (or let expire) every signer that can authorize admin functions, the wallet becomes permanently uncontrollable. Always keep at least one live, unlimited signer, and add the replacement *before* removing the old one.
445
+ - **A sole `Temporary` signer can be evicted → the wallet bricks.** Temporary entries are reclaimed when their TTL lapses. Never let a wallet's only admin signer live in `Temporary` storage; keep at least one `Persistent` signer.
446
+ - **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).
447
+ - **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.
448
+ - **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.
449
+ - **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`.
287
450
 
288
- ## TypeScript Configuration
451
+ ## Contract interface
289
452
 
290
- This library exports TypeScript only to avoid bundling `@stellar/stellar-sdk` twice. Configure your bundler to transpile it.
453
+ 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.
291
454
 
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
- }
455
+ **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`).
303
456
 
304
- export default nextConfig
305
- ```
457
+ **Signer kinds:** `Policy(Address)` · `Ed25519(BytesN<32>)` · `Secp256r1(Bytes keyId)`, each with a `SignerExpiration`, `SignerLimits`, and `SignerStorage`.
306
458
 
307
- ---
459
+ **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.
308
460
 
309
- ## Contributing
461
+ **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()`).
310
462
 
311
- ```bash
312
- # Install dependencies
313
- pnpm i
463
+ **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.
464
+
465
+ See [`contracts/smart-wallet-interface/src/`](./contracts/smart-wallet-interface/src) for the canonical trait and types.
466
+
467
+ ### Deterministic derivation
314
468
 
315
- # Build
316
- pnpm run build
469
+ 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
470
 
318
- # Run demo
319
- cd ./demo && pnpm i && pnpm run start
471
+ ```text
472
+ contractId = sha256(XDR(HashIdPreimage::EnvelopeTypeContractId {
473
+ networkId: sha256(networkPassphrase),
474
+ contractIdPreimage: ContractIdPreimageFromAddress {
475
+ address: G-address of the canonical deployer keypair,
476
+ salt: sha256(keyId),
477
+ },
478
+ }))
320
479
  ```
321
480
 
322
- **Directory structure:**
323
- - `./src` - TypeScript SDK source
324
- - `./demo` - Demo application
325
- - `./contracts` - Rust Soroban smart contracts
326
- - `./zephyr` - Mercury Zephyr indexer program
481
+ - 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.
482
+ - The WASM hash is deliberately **not** in the preimage, so an `upgrade` never moves a wallet's address.
327
483
 
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`.
484
+ 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--footguns)).
330
485
 
331
- > [!IMPORTANT]
332
- > The bindings in `./packages` have been heavily modified. When rebuilding, prefer updating only the `src/index.ts` files in each package.
486
+ ## Repository layout & development
487
+
488
+ | Path | Contents |
489
+ |---|---|
490
+ | `src/` | The `passkey-kit` SDK (client, server, signers, indexer, storage). |
491
+ | `packages/passkey-kit-sdk` | Generated smart-wallet contract bindings (do not hand-edit — see [releasing](./docs/releasing.md)). |
492
+ | `packages/sac-sdk` | Generated SEP-41 SAC bindings. |
493
+ | `contracts/` | Rust Soroban contracts: `smart-wallet`, `smart-wallet-interface`, `sample-policy`, `example-contract`. |
494
+ | `relayer-proxy/` | Cloudflare Worker for keyless, fee-sponsored submission. |
495
+ | `demo/` | Svelte 5 demo exercising the full client API. |
496
+
497
+ ```bash
498
+ pnpm install
499
+ pnpm build # regenerate bindings, compile to dist/, verify Node-ESM import
500
+ pnpm test # vitest (co-located src/*.test.ts)
501
+ pnpm verify:bindings # assert the committed bindings match the canonical WASM
502
+ ```
333
503
 
334
- ---
504
+ - **[CHANGELOG.md](./CHANGELOG.md)** — the v1 overhaul, by component.
505
+ - **[docs/migration-v1.md](./docs/migration-v1.md)** — upgrading from 0.12.x, with Before/After and a gap analysis.
506
+ - **[docs/releasing.md](./docs/releasing.md)** — the dependency-ordered publish flow.
335
507
 
336
508
  ## Resources
337
509
 
338
- - [Super Peach](https://github.com/kalepail/superpeach) - Real-world implementation example
339
- - [Discord #passkeys](https://discord.gg/stellardev) - Questions and showcase
510
+ - [Super Peach](https://github.com/kalepail/superpeach) a real-world implementation example.
511
+ - [Stellar Developers Discord `#passkeys`](https://discord.gg/stellardev) questions and showcase.