@waaskey/sdk 0.1.0 → 0.2.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.
package/README.md CHANGED
@@ -4,24 +4,33 @@ Official TypeScript SDK for [Waaskey](https://waaskey.com) — embedded, non-cus
4
4
  **MPC wallets** for your app. Create wallets and sign transactions where the private
5
5
  key is never assembled in one place (2-of-3 threshold ECDSA, no seed phrase).
6
6
 
7
- > **Early access (v0.0.x).** The public API is taking shape and may change before
7
+ > **Early access (v0.2.x).** The public API is taking shape and may change before
8
8
  > `1.0.0`. Pin an exact version.
9
9
 
10
10
  ## Install
11
11
 
12
12
  ```bash
13
- pnpm add @waaskey/sdk
13
+ pnpm add @waaskey/sdk @waaskey/client-wasm
14
14
  ```
15
15
 
16
+ `@waaskey/client-wasm` is the WASM MPC engine `WasmMpcCore` loads at runtime — it is
17
+ an **exact-pinned optional peer** (the SDK refuses a mismatched version), so install
18
+ it alongside the SDK for any app that creates wallets or signs. Passkey / step-up
19
+ flows additionally need the optional peer `@simplewebauthn/browser`.
20
+
16
21
  ## Quickstart
17
22
 
18
23
  ```ts
19
- import { Waaskey, WasmMpcCore, EncryptedShareStore } from '@waaskey/sdk';
24
+ import { Waaskey, WasmMpcCore, EncryptedShareStore, loadClientWasm } from '@waaskey/sdk';
25
+
26
+ // A secret YOU derive per user (session token, passkey PRF output, device secret) —
27
+ // it keys the AES-256-GCM sealing of the local key share; never hardcode it.
28
+ const sessionSecret = await deriveUserSessionSecret();
20
29
 
21
30
  const waaskey = new Waaskey({
22
31
  apiKey: process.env.WAASKEY_PUBLISHABLE_KEY!,
23
32
  // Non-custodial: the device runs its half of the ceremony and seals its key share.
24
- mpc: new WasmMpcCore(loadClientWasm), // device-party crypto core (WASM)
33
+ mpc: new WasmMpcCore(loadClientWasm), // dynamic-imports @waaskey/client-wasm
25
34
  shareStore: EncryptedShareStore.browser(sessionSecret), // sealed in IndexedDB
26
35
  });
27
36
 
@@ -33,6 +42,56 @@ const wallet = await waaskey.wallets.create({ chain: 'ethereum' });
33
42
  const signature = await wallet.sign(digestHex);
34
43
  ```
35
44
 
45
+ **Reusing an existing wallet:** persist `wallet.id` (`wlt_…`) next to your user record
46
+ and load it later with `waaskey.wallets.get(id)` — the sealed device share is already
47
+ in the `shareStore` under that id, so `sign`/`send` work as soon as the sealing secret
48
+ is available again. Forgot the ids? `waaskey.wallets.list()` pages through the
49
+ tenant's wallets. On a brand-new device the share is restored via _Multi-factor
50
+ recovery_ (below), not re-created.
51
+
52
+ ### Node.js (no browser)
53
+
54
+ The same flow works server-side on Node ≥ 22 (global `fetch`, `WebSocket` and WebCrypto
55
+ are built in). Two things differ from the browser: the wasm engine is loaded from disk
56
+ (Node's `fetch` can't read file paths), and there is no IndexedDB — pass
57
+ `MemoryKeyValueStore` (or your own `KeyValueStore`, e.g. DB-backed). Generate the
58
+ Paillier primes **before** the ceremony with a `PrimePool` — inline generation takes
59
+ minutes of single-threaded CPU and can outlive the server party's ceremony timeout:
60
+
61
+ ```ts
62
+ import { readFile } from 'node:fs/promises';
63
+ import { createRequire } from 'node:module';
64
+ import { Waaskey, WasmMpcCore, EncryptedShareStore, MemoryKeyValueStore, PrimePool, generateRecoveryCode } from '@waaskey/sdk';
65
+
66
+ const require = createRequire(import.meta.url);
67
+
68
+ // Node wasm loader: read the bytes from the installed package and initialize with them.
69
+ async function loadClientWasmNode() {
70
+ const mod = await import('@waaskey/client-wasm');
71
+ const bytes = await readFile(require.resolve('@waaskey/client-wasm/client_wasm_bg.wasm'));
72
+ await mod.default(await WebAssembly.compile(bytes));
73
+ return mod;
74
+ }
75
+
76
+ const mpc = new WasmMpcCore(loadClientWasmNode);
77
+ const waaskey = new Waaskey({
78
+ apiKey: process.env.WAASKEY_API_KEY!,
79
+ mpc,
80
+ shareStore: new EncryptedShareStore(new MemoryKeyValueStore(), process.env.SHARE_SECRET!),
81
+ primePool: new PrimePool(mpc),
82
+ });
83
+
84
+ // Off the hot path (boot/idle): pre-generate the primes so create() takes seconds.
85
+ await waaskey.wallets.prewarm('ethereum');
86
+
87
+ const recoveryCode = generateRecoveryCode(); // show it to the user once
88
+ const wallet = await waaskey.wallets.create({ chain: 'ethereum' }, { backup: { recoveryCode, totpSecret, email } });
89
+ const signature = await wallet.sign(digestHex);
90
+ ```
91
+
92
+ > A wallet created this way is non-custodial with the _server you run this on_ acting
93
+ > as the "device" — its share lives in your `shareStore`. Guard that store accordingly.
94
+
36
95
  ### Send a transaction — WaaS signs, **you** broadcast
37
96
 
38
97
  WaaS is a **signing service, not a broadcaster** (non-custodial: it never owns the
@@ -55,8 +114,9 @@ integrator's concern.
55
114
 
56
115
  ### Custody policy — choose the threshold topology (advanced)
57
116
 
58
- `wallets.create({ chain })` defaults to the **embedded 2-of-3** topology (the platform
59
- alone can meet the threshold — the honest default posture). To change the `(t, n)`
117
+ `wallets.create({ chain })` defaults to the **non-custodial 2-of-3** topology
118
+ `[device, server, user_backup]` — the platform holds 1 share < t=2, so it can never
119
+ sign alone (`custodyType: 'shared'`, `isNonCustodial: true`). To change the `(t, n)`
60
120
  threshold or the custody guarantee, pass a custody policy; the SDK validates it
61
121
  client-side and the backend enforces the attested invariant `platformShares < t ⟺
62
122
  non-custodial`:
@@ -65,14 +125,14 @@ non-custodial`:
65
125
  const wallet = await waaskey.wallets.create({
66
126
  chain: 'ethereum',
67
127
  threshold: 2, // t
68
- parties: ['device', 'backup', 'server'], // n = 3 party roles
128
+ parties: ['device', 'user_backup', 'server'], // n = 3 well-known party roles
69
129
  custodyKinds: ['user_device', 'user_backup', 'platform_signer'], // parallel to parties
70
130
  custodyType: 'shared', // requested posture — the server refuses (400) on a mismatch
71
131
  });
72
132
 
73
133
  // The returned wallet surfaces the attestation so you can display the guarantee:
74
134
  wallet.threshold; // 2
75
- wallet.parties; // ['device','backup','server']
135
+ wallet.parties; // ['device','user_backup','server']
76
136
  wallet.custodyKinds; // ['user_device','user_backup','platform_signer']
77
137
  wallet.platformShareCount; // 1
78
138
  wallet.custodyType; // 'shared'
@@ -96,15 +156,16 @@ const { items } = await wallet.signatures({ page: 1, limit: 20 });
96
156
 
97
157
  ## API
98
158
 
99
- | Method | Description |
100
- | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
101
- | `new Waaskey({ apiKey, mpc, shareStore, baseUrl?, fetch? })` | Create a client. `mpc` + `shareStore` are required to create wallets. |
102
- | `waaskey.wallets.create({ chain, label?, threshold?, parties?, custodyKinds?, custodyType? }, options?)` | Create (keygen + sealed share). Optional custody policy (default embedded 2-of-3). Returns a `Wallet`. |
103
- | `waaskey.wallets.get(id, options?)` | Load an existing wallet. |
104
- | `wallet.sign(digestHex, options?)` | Sign a 32-byte digest with the wallet's MPC key. |
105
- | `wallet.send(params, options?)` | Build + MPC-sign a tx. Returns the **signed raw tx** (you broadcast). |
106
- | `wallet.signatures(query?, options?)` | The wallet's signing activity (paginated). |
107
- | `waaskey.broadcast(signedTx, { rpcUrl })` | **Optional** best-effort submit of a signed tx from your own node. |
159
+ | Method | Description |
160
+ | -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
161
+ | `new Waaskey({ apiKey, mpc, shareStore, baseUrl?, fetch? })` | Create a client. `mpc` + `shareStore` are required to create wallets. |
162
+ | `waaskey.wallets.create({ chain, label?, threshold?, parties?, custodyKinds?, custodyType? }, options?)` | Create (keygen + sealed share). Optional custody policy (default: non-custodial 2-of-3 `[device, server, user_backup]`). Returns a `Wallet`. |
163
+ | `waaskey.wallets.list(query?, options?)` | List the tenant's wallets, newest first (paginated `WalletData` rows). |
164
+ | `waaskey.wallets.get(id, options?)` | Load an existing wallet. |
165
+ | `wallet.sign(digestHex, options?)` | Sign a 32-byte digest with the wallet's MPC key. |
166
+ | `wallet.send(params, options?)` | Build + MPC-sign a tx. Returns the **signed raw tx** (you broadcast). |
167
+ | `wallet.signatures(query?, options?)` | The wallet's signing activity (paginated). |
168
+ | `waaskey.broadcast(signedTx, { rpcUrl })` | **Optional** best-effort submit of a signed tx from your own node. |
108
169
 
109
170
  `create` / `sign` accept `{ signal }` for cancellation; `create` also takes
110
171
  `{ waitForActive?, activationTimeoutMs?, pollIntervalMs? }`.
@@ -124,6 +185,32 @@ IndexedDB (`MemoryKeyValueStore` for tests/SSR). Wipe everything on logout:
124
185
  await shareStore.clear();
125
186
  ```
126
187
 
188
+ #### Passkey-derived secret (recommended)
189
+
190
+ The strongest source for the sealing secret is a **passkey with the WebAuthn PRF
191
+ extension** (`PasskeyPrfSecretProvider`): the 32-byte secret only materializes after
192
+ the user touches their authenticator (or passes biometrics) and never sits on disk —
193
+ malware that steals the IndexedDB ciphertext cannot decrypt it in the background.
194
+ Needs the optional peer `@simplewebauthn/browser`.
195
+
196
+ ```ts
197
+ import { PasskeyPrfSecretProvider, isPrfSupported } from '@waaskey/sdk';
198
+
199
+ const prf = new PasskeyPrfSecretProvider();
200
+
201
+ // Onboarding — register a PRF-capable passkey (one authenticator touch).
202
+ // Persist credentialId + salt anywhere (not secret); keep `secret` in memory only.
203
+ const { credentialId, salt, secret } = await prf.enroll();
204
+
205
+ // Every later session — unlock with the same passkey (another touch).
206
+ const { secret: sessionSecret } = await prf.unlock(credentialId, { salt });
207
+
208
+ const shareStore = EncryptedShareStore.browser(sessionSecret);
209
+ ```
210
+
211
+ Feature-detect with `isPasskeySupported()` / `isPrfSupported()` and fall back to a
212
+ session-derived secret where PRF is unavailable.
213
+
127
214
  ### Multi-factor recovery
128
215
 
129
216
  Back up the device share so a user can recover after losing their device. The share
@@ -174,6 +261,8 @@ const waaskey = new Waaskey({
174
261
 
175
262
  const eth = await waaskey.balances.getBalance('ethereum', wallet.address!);
176
263
  // → { raw: 1000000000000000000n, decimals: 18, symbol: 'ETH', formatted: '1' }
264
+ // (`symbol` is optional in the type — present for built-in EVM chains, may be
265
+ // undefined for a custom provider that doesn't supply one)
177
266
 
178
267
  const usdc = await waaskey.balances.getTokenBalance('ethereum', usdcAddress, wallet.address!, { symbol: 'USDC' });
179
268
  ```
package/dist/index.cjs CHANGED
@@ -1286,6 +1286,17 @@ var Wallets = class {
1286
1286
  if (curve === "ed25519") return;
1287
1287
  await this.deps.primePool?.ensure(curve);
1288
1288
  }
1289
+ /**
1290
+ * List the tenant's wallets, newest first (paginated). Returns plain {@link WalletData}
1291
+ * rows — pass an `id` to {@link get} to obtain a signing-capable {@link Wallet}.
1292
+ */
1293
+ async list(query = {}, options = {}) {
1294
+ const qs = new URLSearchParams();
1295
+ if (query.page !== void 0) qs.set("page", String(query.page));
1296
+ if (query.limit !== void 0) qs.set("limit", String(query.limit));
1297
+ const suffix = qs.toString() ? `?${qs}` : "";
1298
+ return this.http.request("GET", `/v1/wallets${suffix}`, void 0, options.signal);
1299
+ }
1289
1300
  /** Load an existing wallet by id. */
1290
1301
  async get(id, options = {}) {
1291
1302
  const data = await this.http.request("GET", `/v1/wallets/${id}`, void 0, options.signal);
@@ -1868,7 +1879,7 @@ var Waaskey = class {
1868
1879
  this.auth = new Auth(http);
1869
1880
  this.members = new Members(http);
1870
1881
  http.useMemberAccessToken(() => this.members.accessToken);
1871
- this.wallets = new Wallets(http, { mpc: options.mpc, shareStore: options.shareStore, analytics });
1882
+ this.wallets = new Wallets(http, { mpc: options.mpc, shareStore: options.shareStore, primePool: options.primePool, analytics });
1872
1883
  this.recovery = new Recovery(http, { shareStore: options.shareStore, analytics });
1873
1884
  this.reshare = new Reshare(http, { mpc: options.mpc, shareStore: options.shareStore, analytics });
1874
1885
  this.balances = new Balances(options.chains, options.fetch);
@@ -2206,7 +2217,7 @@ function decodePublicKey(sharedPublicKeyJson) {
2206
2217
 
2207
2218
  // src/mpc/load-wasm.ts
2208
2219
  var CLIENT_WASM_PACKAGE = "@waaskey/client-wasm";
2209
- var CLIENT_WASM_VERSION = "0.2.0";
2220
+ var CLIENT_WASM_VERSION = "0.2.1";
2210
2221
  async function verifyWasmIntegrity(bytes, expectedSha384) {
2211
2222
  if (!expectedSha384 || !expectedSha384.startsWith("sha384-")) {
2212
2223
  throw new Error("Waaskey: an expected SHA-384 integrity hash (sha384-<base64>) is required to load the wasm MPC core.");