@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 +106 -17
- package/dist/index.cjs +13 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -3
- package/dist/index.d.ts +16 -3
- package/dist/index.js +13 -2
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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.
|
|
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), //
|
|
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 **
|
|
59
|
-
|
|
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', '
|
|
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','
|
|
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
|
|
103
|
-
| `waaskey.wallets.
|
|
104
|
-
| `
|
|
105
|
-
| `wallet.
|
|
106
|
-
| `wallet.
|
|
107
|
-
| `
|
|
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.
|
|
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.");
|