@waaskey/sdk 0.0.1 → 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/dist/index.d.cts CHANGED
@@ -1,12 +1,867 @@
1
+ import { PublicKeyCredentialCreationOptionsJSON, PublicKeyCredentialRequestOptionsJSON, AuthenticationResponseJSON } from '@simplewebauthn/browser';
2
+
3
+ /** Thin typed HTTP client over fetch — the single place requests are issued. */
4
+ declare class HttpClient {
5
+ private readonly apiKey;
6
+ private readonly baseUrl;
7
+ private readonly fetchImpl;
8
+ private memberAccessToken?;
9
+ constructor(apiKey: string, baseUrl: string, fetchImpl?: typeof fetch);
10
+ /**
11
+ * Wire a provider of the held org-member access token (from {@link Members}). When it returns a
12
+ * token, {@link requestAsMember} authenticates member calls with `Authorization: Bearer <token>`
13
+ * rather than the ambient dashboard cookie. Injected by the client (not imported) so `Members` and
14
+ * `HttpClient` don't form an import cycle.
15
+ */
16
+ useMemberAccessToken(provider: () => string | undefined): void;
17
+ request<T>(method: string, path: string, body?: unknown, signal?: AbortSignal, bearer?: string): Promise<T>;
18
+ /**
19
+ * `@Public` request: NO `Authorization` header and NO cookie. Used by the org-member Firebase
20
+ * login exchange (`POST /v1/auth/firebase`), which authenticates from the Firebase ID token in the
21
+ * body alone and returns the member bearer tokens in its response.
22
+ */
23
+ requestPublic<T>(method: string, path: string, body?: unknown, signal?: AbortSignal): Promise<T>;
24
+ /**
25
+ * Member-session request (#349). Authenticates as the org member in one of two ways, transparent
26
+ * to the caller ({@link Wallets.joinCeremony} / {@link Wallets.joinSignCeremony}):
27
+ *
28
+ * - **Held bearer session** (a {@link memberAccessToken} provider returns a token — e.g. after
29
+ * {@link Members.loginWithFirebase}): sends `Authorization: Bearer <accessToken>`. The backend's
30
+ * `ApiKeyAuthGuard` recognises a non-API-key bearer and falls through to the member-session
31
+ * resolver, so a headless (non-browser) consumer authenticates without a cookie.
32
+ * - **No held session** (the browser dashboard path): sends NO `Authorization` header and
33
+ * `credentials: 'include'`, relying on the caller's own same-site httpOnly member cookie. The
34
+ * guard only falls back to the cookie resolver when the header is absent entirely, so it is
35
+ * omitted here.
36
+ *
37
+ * Never used by the tenant-apiKey ({@link request}) or embedded-end-user paths.
38
+ */
39
+ requestAsMember<T>(method: string, path: string, body?: unknown, signal?: AbortSignal): Promise<T>;
40
+ private send;
41
+ }
42
+
43
+ /** Lifecycle events the SDK emits for the tenant dashboard. */
44
+ type AnalyticsEventType = 'wallet.created' | 'wallet.signed' | 'wallet.sent' | 'wallet.recovered' | 'wallet.reshared';
45
+ /**
46
+ * A privacy-respecting SDK analytics event. Carries NO PII, secrets, key shares, addresses, or
47
+ * signing digests — only the action, a timestamp, and tenant-owned non-sensitive identifiers.
48
+ */
49
+ interface AnalyticsEvent {
50
+ type: AnalyticsEventType;
51
+ /** ISO-8601 timestamp. */
52
+ timestamp: string;
53
+ /** Wallet id — an opaque Waaskey identifier owned by the tenant (not PII). */
54
+ walletId?: string;
55
+ /** Chain slug, e.g. "ethereum". */
56
+ chain?: string;
57
+ /** Signing curve, e.g. "secp256k1". */
58
+ curve?: string;
59
+ }
60
+ /**
61
+ * Receives SDK analytics events. The default is an HTTP sink to the Waaskey API; pass your own to
62
+ * forward to a custom pipeline, or `false` in {@link WaaskeyOptions.analytics} to opt out entirely.
63
+ */
64
+ interface AnalyticsSink {
65
+ track(event: AnalyticsEvent): void;
66
+ }
67
+
68
+ /**
69
+ * Emits lifecycle events to a {@link AnalyticsSink}; a no-op when analytics is disabled (no sink).
70
+ * Fire-and-forget and exception-safe — analytics must NEVER break or slow a wallet operation.
71
+ */
72
+ declare class Analytics {
73
+ private readonly sink?;
74
+ constructor(sink?: AnalyticsSink | undefined);
75
+ track(type: AnalyticsEventType, props?: Omit<AnalyticsEvent, 'type' | 'timestamp'>): void;
76
+ }
77
+
78
+ /** Default sink: POST events to the Waaskey API, fire-and-forget — delivery failures are swallowed. */
79
+ declare class HttpAnalyticsSink implements AnalyticsSink {
80
+ private readonly http;
81
+ constructor(http: HttpClient);
82
+ track(event: AnalyticsEvent): void;
83
+ }
84
+
85
+ /**
86
+ * Client-side balance reads.
87
+ *
88
+ * WaaS is non-custodial and the backend does not index chain state — balances are
89
+ * read **directly from a chain provider** (dApp-style), never from the backend. The
90
+ * SDK depends on the {@link ChainProvider} port; the default web implementation is a
91
+ * JSON-RPC provider for EVM chains, and a consumer can plug a custom provider for
92
+ * any chain/transport.
93
+ */
94
+ /** Reads chain state for one chain. Native is always supported; tokens/decimals are EVM-style. */
95
+ interface ChainProvider {
96
+ /** Native asset balance of `address`, in base units (wei / sat / lamports). */
97
+ getNativeBalance(address: string): Promise<bigint>;
98
+ /** Token balance of `owner` for `token`, in the token's base units. */
99
+ getTokenBalance(token: string, owner: string): Promise<bigint>;
100
+ /** Token decimals, if the provider can resolve them (EVM `decimals()`). */
101
+ getTokenDecimals?(token: string): Promise<number>;
102
+ }
103
+ /** Per-chain provider configuration. */
104
+ interface ChainConfig {
105
+ /** JSON-RPC URL for an EVM chain. Ignored if `provider` is set. */
106
+ rpcUrl?: string;
107
+ /** A custom provider (for non-EVM chains or a bespoke transport). Overrides `rpcUrl`. */
108
+ provider?: ChainProvider;
109
+ }
110
+ /** A resolved balance — exact base units plus a human-readable rendering. */
111
+ interface Balance {
112
+ /** Exact balance in base units (no float). */
113
+ raw: bigint;
114
+ /** Decimals of the asset. */
115
+ decimals: number;
116
+ /** `raw` rendered as a decimal string (e.g. `"1.5"`). */
117
+ formatted: string;
118
+ /** Asset symbol, when known. */
119
+ symbol?: string;
120
+ }
121
+ /** Options for a token balance read. */
122
+ interface TokenBalanceOptions {
123
+ /** Token decimals (skips an on-chain `decimals()` lookup). */
124
+ decimals?: number;
125
+ /** Token symbol, for the returned {@link Balance}. */
126
+ symbol?: string;
127
+ }
128
+
129
+ /**
130
+ * Reads balances directly from a chain provider (never the backend). EVM chains work
131
+ * out of the box via a default public RPC; override per chain with `chains` (an RPC
132
+ * URL or a custom {@link ChainProvider}), and supply a provider for non-EVM chains.
133
+ */
134
+ declare class Balances {
135
+ private readonly config;
136
+ private readonly fetchImpl?;
137
+ private readonly providers;
138
+ constructor(config?: Partial<Record<Chain, ChainConfig>>, fetchImpl?: typeof fetch | undefined);
139
+ /** Native (gas-token) balance of an address on a chain. */
140
+ getBalance(chain: Chain, address: string): Promise<Balance>;
141
+ /** Token balance of `owner` for `token` on a chain (ERC-20 on EVM). */
142
+ getTokenBalance(chain: Chain, token: string, owner: string, options?: TokenBalanceOptions): Promise<Balance>;
143
+ /** Resolve (and cache) the provider for a chain: custom → configured RPC → default EVM RPC. */
144
+ private provider;
145
+ }
146
+
147
+ /**
148
+ * {@link ChainProvider} backed by an EVM JSON-RPC endpoint (`eth_getBalance`,
149
+ * `eth_call`). Reads are dApp-style and need no backend. A custom `fetch` can be
150
+ * injected (non-browser runtimes / tests).
151
+ */
152
+ declare class EvmRpcProvider implements ChainProvider {
153
+ private readonly rpcUrl;
154
+ private readonly fetchImpl;
155
+ constructor(rpcUrl: string, fetchImpl?: typeof fetch);
156
+ getNativeBalance(address: string): Promise<bigint>;
157
+ getTokenBalance(token: string, owner: string): Promise<bigint>;
158
+ getTokenDecimals(token: string): Promise<number>;
159
+ private call;
160
+ private toBigInt;
161
+ }
162
+
163
+ /** Render a base-unit integer as a decimal string (e.g. `formatUnits(1500000n, 6)` → `"1.5"`). No float. */
164
+ declare function formatUnits(raw: bigint, decimals: number): string;
165
+
166
+ /**
167
+ * Device-party MPC contract.
168
+ *
169
+ * Waaskey wallets are 2-of-3: the user's **device** holds one key share and runs
170
+ * its half of the cggmp24 ceremony locally (in WASM), talking to the Waaskey
171
+ * `signer` (the server party) over the relay. The private key never exists whole
172
+ * anywhere. This is the device side of that ceremony.
173
+ *
174
+ * The wire shapes mirror the waas-core `client-wasm` exports (`keygen` / `sign`),
175
+ * which mirror the server `party-runner`. The backend tells the SDK the per-party
176
+ * routing (relay url, session id, indices, roles) in the create-wallet response.
177
+ */
178
+ /** Curves the device party can run a ceremony on. */
179
+ type MpcCurve = 'secp256k1';
180
+ /** Relay routing + party identity shared by keygen and sign. */
181
+ interface CeremonyParams {
182
+ curve: MpcCurve;
183
+ /** Relay websocket URL both parties connect to. */
184
+ relayUrl: string;
185
+ /** Ceremony id shared by all parties; derives the execution ids. */
186
+ sessionId: string;
187
+ /** This party's relay routing id. Defaults to `"device"`. */
188
+ role?: string;
189
+ /** The peer (server) party's relay routing id. Defaults to `"server"`. */
190
+ peerRole?: string;
191
+ /** This party's keygen index. */
192
+ partyIndex: number;
193
+ /** The peer (server) party's keygen index. */
194
+ peerPartyIndex: number;
195
+ /**
196
+ * Short-lived relay token (JWT) the device presents to the relay to join this session as
197
+ * `role`. Returned by the backend on the ceremony / sign-session response; present only when
198
+ * relay authentication is enabled. Absent ⇒ the relay accepts an unauthenticated join.
199
+ */
200
+ relayToken?: string;
201
+ }
202
+ /** Parameters for the device half of a keygen ceremony. */
203
+ interface DeviceKeygenParams extends CeremonyParams {
204
+ /** Total parties `n`. */
205
+ parties: number;
206
+ /** Signing threshold `t` (`2 <= t <= n`). */
207
+ threshold: number;
208
+ /**
209
+ * Pre-generated Paillier safe-primes (JSON) for this device, produced ahead of time off the
210
+ * hot path (see {@link MpcCore.pregeneratePrimes} / `PrimePool`). When omitted the core
211
+ * generates them inline — correct, but slow in single-threaded browser WASM. The primes are
212
+ * this device's private aux material and are NEVER sent to the server.
213
+ */
214
+ pregeneratedPrimes?: string;
215
+ }
216
+ /** Result of the device half of keygen — the device's share never leaves the device. */
217
+ interface DeviceKeygenResult {
218
+ /** The device's KeyShare (JSON). Seal + store on the device; never send to the server. */
219
+ keyShare: string;
220
+ /** The ceremony's aux info (JSON). */
221
+ auxInfo: string;
222
+ /** The wallet's shared public key (hex), derived from the share — safe to publish. */
223
+ sharedPublicKey: string;
224
+ }
225
+ /** Parameters for the device half of a sign ceremony. */
226
+ interface DeviceSignParams extends CeremonyParams {
227
+ /**
228
+ * The device's KeyShare as its JSON **string** — the bare crypto material from a prior keygen
229
+ * ({@link DeviceKeygenResult.keyShare}), NOT the on-device storage blob. It rides the sign wire as
230
+ * a parsed JSON OBJECT (the wasm deserializes it with `serde_json::from_value::<KeyShare>`).
231
+ */
232
+ share: string;
233
+ /** Keygen indices signing together (any `t` of `n`). */
234
+ participants: number[];
235
+ /** This party's 0-based position within `participants`. */
236
+ signerPosition: number;
237
+ /** 32-byte hex digest to sign (`0x` prefix optional). */
238
+ digest: string;
239
+ }
240
+ /** Result of the device half of sign. */
241
+ interface DeviceSignResult {
242
+ /** The cggmp24 signature (JSON). */
243
+ signature: string;
244
+ }
245
+ /**
246
+ * Parameters for the device's reshare **assemble** stage ({@link MpcCore.runReshareAssemble}).
247
+ * A structural mirror of the backend `DeviceReshareMaterial` plus the wallet's curve — pure
248
+ * and local, so no relay routing is needed. The fields are opaque JSON produced by the backend
249
+ * reshare; the device only routes them into the core.
250
+ */
251
+ interface DeviceReshareAssembleParams {
252
+ curve: MpcCurve;
253
+ /** This device's 0-based slot within {@link newPreimages}. */
254
+ newPosition: number;
255
+ /** New share preimages `I'` (32-byte big-endian hex scalars), new-holder order. */
256
+ newPreimages: string[];
257
+ /** The new signing threshold `t'`. */
258
+ newThreshold: number;
259
+ /** The unchanged wallet public info (`WalletPublicInfo` JSON, opaque). */
260
+ wallet: unknown;
261
+ /** One Feldman-commitments object per dealer (the broadcast set), opaque JSON. */
262
+ commitments: unknown[];
263
+ /** This device's private sub-share from each dealer, one per dealer, opaque JSON. */
264
+ subShares: unknown[];
265
+ }
266
+ /**
267
+ * Result of the device reshare **assemble** — the bare NEW-epoch core. It cannot sign yet
268
+ * (aux material is generated in {@link MpcCore.runCompleteReshare}), and it is secret: seal it,
269
+ * never log it.
270
+ */
271
+ interface DeviceReshareAssembleResult {
272
+ /** The device's NEW-epoch bare core (`IncompleteKeyShare` JSON). Secret. */
273
+ core: string;
274
+ /** The assembled shared public key (hex) — must equal the wallet's (unchanged across a reshare). */
275
+ sharedPublicKey: string;
276
+ }
277
+ /**
278
+ * Parameters for the device's **complete-reshare** stage ({@link MpcCore.runCompleteReshare}) —
279
+ * the interactive aux-info ceremony over the NEW committee that turns the bare core into a
280
+ * signable share. Relay routing mirrors keygen/sign ({@link CeremonyParams}); the backend uses
281
+ * `<sessionId>/reshare-aux` (kind `reshare-aux`) and drives the server + recovery parties.
282
+ */
283
+ interface DeviceCompleteReshareParams extends CeremonyParams {
284
+ /** The device's bare NEW-epoch core (JSON) from {@link MpcCore.runReshareAssemble}. */
285
+ core: string;
286
+ /** Total parties `n'` in the NEW committee. */
287
+ parties: number;
288
+ /**
289
+ * This device's OWN pre-generated Paillier safe-primes (JSON) for the aux ceremony, off the hot
290
+ * path (see {@link MpcCore.pregeneratePrimes} / `PrimePool`). When omitted the core generates
291
+ * them inline. The primes are the device's private material and are NEVER server-provided — a
292
+ * server-supplied prime pool would collapse the threshold to custodial.
293
+ */
294
+ pregeneratedPrimes?: string;
295
+ }
296
+ /** Result of the device **complete-reshare** — the COMPLETE, signable share. Seal it, never log it. */
297
+ interface DeviceCompleteReshareResult {
298
+ /** The device's complete KeyShare (JSON). Seal + store on the device; never send to the server. */
299
+ keyShare: string;
300
+ /** The wallet's shared public key (hex), preserved across the reshare — verify it is unchanged. */
301
+ sharedPublicKey: string;
302
+ }
303
+ /** Relay connection info shared by an n-party (member-bound) keygen/sign ceremony (#349). */
304
+ interface MemberCeremonyParams {
305
+ curve: MpcCurve;
306
+ /** Relay websocket URL every party connects to. */
307
+ relayUrl: string;
308
+ /** Ceremony id shared by every party; derives the execution ids. */
309
+ sessionId: string;
310
+ /**
311
+ * Short-lived relay token (JWT) this party presents to join the session as its own role in
312
+ * {@link MemberKeygenParams.roles} / {@link MemberSignParams.roles}. Present only when relay
313
+ * authentication is enabled. Absent ⇒ the relay accepts an unauthenticated join.
314
+ */
315
+ relayToken?: string;
316
+ }
317
+ /** Parameters for this party's half of an n-party keygen ceremony (member-bound wallets, #349). */
318
+ interface MemberKeygenParams extends MemberCeremonyParams {
319
+ /** Every party's relay role, in protocol index order — the FULL n-party roster (`roles[i]` is party `i`'s role). */
320
+ roles: string[];
321
+ /** This party's own 0-based index into {@link roles} (its keygen index). */
322
+ partyIndex: number;
323
+ /** Signing threshold `t` (`2 <= t <= roles.length`). */
324
+ threshold: number;
325
+ /**
326
+ * Pre-generated Paillier safe-primes (JSON) for this party, produced ahead of time off the hot
327
+ * path (see {@link MpcCore.pregeneratePrimes}). When omitted the core generates them inline.
328
+ */
329
+ pregeneratedPrimes?: string;
330
+ }
331
+ /** Parameters for this party's half of an n-party sign ceremony (member-bound wallets, #349). */
332
+ interface MemberSignParams extends MemberCeremonyParams {
333
+ /** The FIXED t-of-n quorum's relay roles, in signing order (this ceremony's `PartyRouting`). */
334
+ roles: string[];
335
+ /**
336
+ * This party's KeyShare as its JSON **string** — the bare crypto material from a prior
337
+ * {@link MpcCore.runMemberKeygen} ({@link DeviceKeygenResult.keyShare}), NOT the on-device storage
338
+ * blob. It rides the sign wire as a parsed JSON OBJECT (`serde_json::from_value::<KeyShare>`).
339
+ */
340
+ share: string;
341
+ /** Keygen indices of the parties signing together, parallel to {@link roles}. */
342
+ participants: number[];
343
+ /** This party's 0-based position within {@link roles} / {@link participants}. */
344
+ signerPosition: number;
345
+ /** 32-byte hex digest to sign (`0x` prefix optional). */
346
+ digest: string;
347
+ }
348
+ /** Relay connection info shared by an ed25519 (FROST) keygen/sign ceremony (#110). */
349
+ interface EddsaCeremonyParams {
350
+ /** Relay websocket URL every party connects to. */
351
+ relayUrl: string;
352
+ /** Ceremony id shared by every party; derives the FROST execution ids. */
353
+ sessionId: string;
354
+ /**
355
+ * Short-lived relay token (JWT) this party presents to join the session as its own role in
356
+ * {@link roles}. Present only when relay authentication is enabled; absent ⇒ unauthenticated join.
357
+ */
358
+ relayToken?: string;
359
+ /**
360
+ * Every party's relay role, in protocol-index order (`roles[i]` is party `i`'s role) — for keygen
361
+ * the FULL n-party roster; for sign the FIXED t-of-n quorum in {@link EddsaSignParams.participants}
362
+ * order. MUST match the platform/party-runner order (role ↔ FROST identifier).
363
+ */
364
+ roles: string[];
365
+ /** This party's own 0-based index into {@link roles} (its slot in the ceremony). */
366
+ partyIndex: number;
367
+ }
368
+ /** Parameters for this device's half of an ed25519 (FROST) DKG keygen ceremony (#110). */
369
+ interface EddsaKeygenParams extends EddsaCeremonyParams {
370
+ /** Signing threshold `t` (`2 <= t <= roles.length`, validated by the FROST DKG). */
371
+ threshold: number;
372
+ /**
373
+ * The FROST DKG round-2 encryption roster (#114): one X25519 encryption PUBLIC key (32-byte hex) per
374
+ * party, in `roles`/protocol-index order (`encPubkeys[i]` ↔ FROST identifier `i + 1`). Every party
375
+ * seals its round-2 packages to these keys so no secret share crosses the relay in the clear — the
376
+ * device needs the whole ordered roster (chiefly the server's key). Maps to the wasm `enc_pubkeys`.
377
+ */
378
+ encPubkeys: string[];
379
+ /**
380
+ * This device's OWN X25519 encryption SECRET key (32-byte hex) — opens the round-2 packages sealed to
381
+ * it (the counterpart of this device's entry in {@link encPubkeys}). SECRET: never logged, never sent
382
+ * to the server. Maps to the wasm `enc_secret`.
383
+ */
384
+ encSecret: string;
385
+ }
386
+ /**
387
+ * Result of the device half of an ed25519 keygen — the device's FROST share. The `keyPackage` is
388
+ * **secret** (the device's signing share): seal + store it, never send it to the server. The
389
+ * `publicKeyPackage` is the wallet's group verifying key package (safe to publish). Both are opaque
390
+ * FROST JSON objects (the exact shapes `signEddsa` feeds back), not the cggmp24 KeyShare blob.
391
+ */
392
+ interface EddsaKeygenResult {
393
+ /** The device's FROST `key_package` (JSON object). Secret — seal + store on the device. */
394
+ keyPackage: unknown;
395
+ /** The wallet's shared `public_key_package` (JSON object). Group key — safe to publish. */
396
+ publicKeyPackage: unknown;
397
+ }
398
+ /** Parameters for this device's half of an ed25519 (FROST) two-round sign ceremony (#110). */
399
+ interface EddsaSignParams extends EddsaCeremonyParams {
400
+ /** This device's FROST `key_package` — the secret share from {@link EddsaKeygenResult.keyPackage}, as a JSON object (or its JSON string). */
401
+ keyPackage: unknown;
402
+ /** The wallet's shared `public_key_package` from {@link EddsaKeygenResult.publicKeyPackage}, as a JSON object (or its JSON string). */
403
+ publicKeyPackage: unknown;
404
+ /** The 1-based FROST identifiers of the signing quorum, parallel to {@link EddsaCeremonyParams.roles}. */
405
+ participants: number[];
406
+ /** Hex-encoded raw message bytes to sign (`0x` prefix optional) — the chain adapter's serialized tx message. */
407
+ message: string;
408
+ }
409
+ /** Result of the device half of an ed25519 sign — the RFC 8032 signature. */
410
+ interface EddsaSignResult {
411
+ /** The 64-byte ed25519 signature, hex. */
412
+ signature: string;
413
+ }
414
+ /**
415
+ * The device-party crypto core. Implemented by {@link WasmMpcCore} (web) and, in
416
+ * future, a native core on mobile — the SDK depends on this port, not the engine.
417
+ */
418
+ interface MpcCore {
419
+ runKeygen(params: DeviceKeygenParams): Promise<DeviceKeygenResult>;
420
+ runSign(params: DeviceSignParams): Promise<DeviceSignResult>;
421
+ /**
422
+ * Pre-generate this device's Paillier safe-primes for `curve`, off the keygen hot path
423
+ * (e.g. in a Web Worker during onboarding/idle). Returns opaque serialized primes (JSON) to
424
+ * cache and later pass as {@link DeviceKeygenParams.pregeneratedPrimes}. Optional: a native
425
+ * mobile core generates primes fast inline and need not implement it.
426
+ */
427
+ pregeneratePrimes?(curve: MpcCurve): Promise<string>;
428
+ /**
429
+ * Assemble this device's NEW-epoch bare core from a device-retaining reshare's material (#318).
430
+ * Pure and local — no relay. Optional: only a core built with the reshare capability implements
431
+ * it (the web wasm needs a `--features reshare` build).
432
+ */
433
+ runReshareAssemble?(params: DeviceReshareAssembleParams): Promise<DeviceReshareAssembleResult>;
434
+ /**
435
+ * Complete a reshared bare core into a signable share by running the aux-info ceremony over the
436
+ * NEW committee (#318 / #95). Relay-driven, mirroring keygen's aux phase. Optional: only a core
437
+ * built with the reshare capability implements it (the web wasm needs a `--features reshare` build).
438
+ */
439
+ runCompleteReshare?(params: DeviceCompleteReshareParams): Promise<DeviceCompleteReshareResult>;
440
+ /**
441
+ * Run this party's half of an n-party (member-bound wallet) keygen ceremony (#349) — every
442
+ * member device + the platform join the SAME relay session, addressed by the full
443
+ * {@link MemberKeygenParams.roles} roster rather than a single peer. Optional: only a core built
444
+ * with n-party (member-ceremony) support implements it.
445
+ */
446
+ runMemberKeygen?(params: MemberKeygenParams): Promise<DeviceKeygenResult>;
447
+ /**
448
+ * Run this party's half of an n-party (member-bound wallet) sign ceremony (#349) over the FIXED
449
+ * t-of-n quorum in {@link MemberSignParams.roles}. Optional: only a core built with n-party
450
+ * (member-ceremony) support implements it.
451
+ */
452
+ runMemberSign?(params: MemberSignParams): Promise<DeviceSignResult>;
453
+ /**
454
+ * Run this device's half of an ed25519 (FROST) DKG keygen ceremony (#110) — the device holds one
455
+ * FROST share in a real 2-of-3 wallet, co-generating the group key with the server (+ recovery)
456
+ * parties on the same relay session. Optional: only a core with the ed25519 (FROST) exports
457
+ * (`keygenEddsa`) implements it.
458
+ */
459
+ runEddsaKeygen?(params: EddsaKeygenParams): Promise<EddsaKeygenResult>;
460
+ /**
461
+ * Run this device's half of an ed25519 (FROST) two-round sign ceremony (#110) over the fixed
462
+ * quorum in {@link EddsaSignParams.roles} — co-signing the raw message with the server party.
463
+ * Optional: only a core with the ed25519 (FROST) exports (`signEddsa`) implements it.
464
+ */
465
+ runEddsaSign?(params: EddsaSignParams): Promise<EddsaSignResult>;
466
+ }
467
+
468
+ /** The subset of the client-wasm module the SDK drives. Each export takes one JSON string. */
469
+ interface ClientWasmModule {
470
+ /** Device half of keygen; resolves with `{ aux_info_json, keyshare_json, shared_public_key_json }`. */
471
+ keygen(paramsJson: string): Promise<unknown>;
472
+ /** Device half of sign; resolves with `{ signature_json }`. */
473
+ sign(paramsJson: string): Promise<unknown>;
474
+ /** Pre-generate Paillier safe-primes off the hot path; resolves with `{ primes_json }`. Optional. */
475
+ pregeneratePrimes?(paramsJson: string): Promise<unknown>;
476
+ /**
477
+ * Device reshare **assemble** — compute the NEW-epoch bare core locally; returns
478
+ * `{ core_json, shared_public_key_json }`. Present only in a `--features reshare` wasm build.
479
+ * Synchronous in the core (no relay); the SDK awaits it uniformly.
480
+ */
481
+ reshareAssemble?(paramsJson: string): unknown | Promise<unknown>;
482
+ /**
483
+ * Device **complete-reshare** — run the aux ceremony over the new committee and return the
484
+ * signable share as `{ keyshare_json, shared_public_key_json }`. Present only in a
485
+ * `--features reshare` wasm build.
486
+ */
487
+ completeReshare?(paramsJson: string): Promise<unknown>;
488
+ /**
489
+ * This party's half of an n-party (member-bound wallet) keygen ceremony (#349); resolves with
490
+ * `{ aux_info_json, keyshare_json, shared_public_key_json }`. Present only in a client-wasm
491
+ * build with n-party (member-ceremony) support.
492
+ */
493
+ keygenMember?(paramsJson: string): Promise<unknown>;
494
+ /**
495
+ * This party's half of an n-party (member-bound wallet) sign ceremony over a fixed t-of-n
496
+ * quorum (#349); resolves with `{ signature_json }`. Present only in a client-wasm build with
497
+ * n-party (member-ceremony) support.
498
+ */
499
+ signMember?(paramsJson: string): Promise<unknown>;
500
+ /**
501
+ * This device's half of an ed25519 (FROST) DKG keygen ceremony (#110); resolves with
502
+ * `{ key_package, public_key_package }` (JSON objects). Present only in a client-wasm build with
503
+ * the ed25519 (FROST) exports.
504
+ */
505
+ keygenEddsa?(paramsJson: string): Promise<unknown>;
506
+ /**
507
+ * This device's half of an ed25519 (FROST) two-round sign ceremony (#110); resolves with
508
+ * `{ signature }` (64-byte hex). Present only in a client-wasm build with the ed25519 (FROST) exports.
509
+ */
510
+ signEddsa?(paramsJson: string): Promise<unknown>;
511
+ }
512
+ /** Lazily loads + initializes the client-wasm module (e.g. dynamic `import()` of the wasm-pack pkg). */
513
+ type ClientWasmLoader = () => Promise<ClientWasmModule>;
514
+ /**
515
+ * {@link MpcCore} backed by the waas-core `client-wasm` module (`keygen` / `sign`).
516
+ *
517
+ * The wasm engine is injected via a loader so the SDK stays runtime-agnostic and
518
+ * the core is unit-testable without a real ceremony. The device key share returned
519
+ * by keygen must be sealed and stored on the device — it is never sent to the server.
520
+ */
521
+ declare class WasmMpcCore implements MpcCore {
522
+ private readonly load;
523
+ private modulePromise?;
524
+ constructor(load: ClientWasmLoader);
525
+ private init;
526
+ runKeygen(params: DeviceKeygenParams): Promise<DeviceKeygenResult>;
527
+ runSign(params: DeviceSignParams): Promise<DeviceSignResult>;
528
+ pregeneratePrimes(curve: MpcCurve): Promise<string>;
529
+ runReshareAssemble(params: DeviceReshareAssembleParams): Promise<DeviceReshareAssembleResult>;
530
+ runCompleteReshare(params: DeviceCompleteReshareParams): Promise<DeviceCompleteReshareResult>;
531
+ runMemberKeygen(params: MemberKeygenParams): Promise<DeviceKeygenResult>;
532
+ runMemberSign(params: MemberSignParams): Promise<DeviceSignResult>;
533
+ runEddsaKeygen(params: EddsaKeygenParams): Promise<EddsaKeygenResult>;
534
+ runEddsaSign(params: EddsaSignParams): Promise<EddsaSignResult>;
535
+ /** Build the snake_case params JSON the wasm exports expect from the common routing + extras. */
536
+ private encode;
537
+ /**
538
+ * Build the snake_case params JSON for an n-party (member-bound, #349) member ceremony — the
539
+ * roster-based routing shape (`roles` + `party_index`), not the 2-party `role`/`peer_role` shape
540
+ * {@link encode} builds. The `roles` roster and `party_index` are passed through in `extra` by
541
+ * the caller and land on the wire under those exact keys — what `keygenMember` / `signMember`
542
+ * deserialize.
543
+ */
544
+ private encodeMember;
545
+ /**
546
+ * Build the snake_case params JSON for an ed25519 (FROST, #110) ceremony — the roster-based routing
547
+ * shape (`roles` + `party_index`) the `keygenEddsa` / `signEddsa` exports deserialize. Unlike
548
+ * {@link encodeMember} there is NO `curve` (ed25519-only) and no Paillier primes; the caller passes
549
+ * the ceremony-specific extras (keygen: `threshold`; sign: the FROST packages + participants + message).
550
+ */
551
+ private encodeEddsa;
552
+ }
553
+
554
+ /**
555
+ * The **exact** `@waaskey/client-wasm` version this SDK build is pinned to (issue #40).
556
+ *
557
+ * Pin an exact version — never a floating `^`/`~` range — so a compromised or
558
+ * dependency-confused higher release can't be silently pulled in. Keep this in
559
+ * lockstep with the `@waaskey/client-wasm` entry in `package.json` and with
560
+ * {@link CLIENT_WASM_SHA384}.
561
+ *
562
+ * **`0.2.x` adds the ed25519 (FROST) exports** (`keygenEddsa` / `signEddsa`, waas-core #110) the SDK's
563
+ * ed25519 wallet path drives. It is **published on npm** (`client-wasm-v0.2.1`) and pinned as the SDK's
564
+ * exact, optional peer dependency in `package.json` — so an app that installs `@waaskey/client-wasm`
565
+ * alongside the SDK runs the real ed25519 create/send path end to end.
566
+ */
567
+ declare const CLIENT_WASM_VERSION = "0.2.1";
568
+ /**
569
+ * Verify the integrity of raw wasm bytes against an expected **SHA-384** hash, in
570
+ * Subresource-Integrity (`sha384-<base64>`) form (issue #40).
571
+ *
572
+ * The wasm MPC core handles the plaintext device share, so it must be
573
+ * cryptographically verified **before** `WebAssembly.compile` / `instantiate` — a
574
+ * registry compromise or malicious re-host otherwise silently exfiltrates share-1.
575
+ * Throws (fails closed) on any mismatch.
576
+ */
577
+ declare function verifyWasmIntegrity(bytes: BufferSource, expectedSha384: string): Promise<void>;
578
+ /** Options for {@link createVerifiedClientWasmLoader}. */
579
+ interface VerifiedWasmLoaderOptions {
580
+ /**
581
+ * URL (or path the runtime's `fetch` accepts) of the `@waaskey/client-wasm` `.wasm`
582
+ * binary — vendored/self-hosted so its bytes can be fetched and hashed before use.
583
+ */
584
+ wasmUrl: string | URL;
585
+ /** Expected SHA-384 of the wasm binary, SRI form (`sha384-<base64>`). Pin per {@link CLIENT_WASM_VERSION}. */
586
+ expectedSha384: string;
587
+ /** Custom fetch (non-browser runtimes / tests). Defaults to global `fetch`. */
588
+ fetch?: typeof fetch;
589
+ }
590
+ /**
591
+ * Build an integrity-checked {@link ClientWasmLoader} (issue #40): it fetches the
592
+ * pinned `.wasm` bytes, verifies their SHA-384 against `expectedSha384`, and only
593
+ * **then** compiles + initializes the module — so a tampered binary is rejected
594
+ * before any wasm code runs.
595
+ *
596
+ * Requires a non-auto-initializing (wasm-pack `--target web`) `@waaskey/client-wasm`
597
+ * build, so the verified bytes drive initialization:
598
+ *
599
+ * ```ts
600
+ * const mpc = new WasmMpcCore(
601
+ * createVerifiedClientWasmLoader({ wasmUrl: '/waaskey_client_wasm_bg.wasm', expectedSha384: CLIENT_WASM_SHA384 }),
602
+ * );
603
+ * ```
604
+ */
605
+ declare function createVerifiedClientWasmLoader(options: VerifiedWasmLoaderOptions): ClientWasmLoader;
606
+ /**
607
+ * Convenience loader for the Waaskey client-wasm engine — dynamically imports `@waaskey/client-wasm`
608
+ * (the wasm-pack/bundler build, auto-initialized on import) and returns it as a {@link ClientWasmModule}:
609
+ *
610
+ * ```ts
611
+ * import { Waaskey, WasmMpcCore, loadClientWasm } from '@waaskey/sdk';
612
+ * const waaskey = new Waaskey({ apiKey, mpc: new WasmMpcCore(loadClientWasm), shareStore });
613
+ * ```
614
+ *
615
+ * ⚠️ **No integrity verification (issue #40).** A bundler-target build instantiates the wasm on
616
+ * import, so its bytes cannot be checked first. For production — where a registry compromise or
617
+ * dependency-confusion attack on `@waaskey/client-wasm` could exfiltrate the plaintext device share
618
+ * — use {@link createVerifiedClientWasmLoader} with a vendored `.wasm` and the pinned SHA-384 instead.
619
+ *
620
+ * `@waaskey/client-wasm` is an OPTIONAL peer dependency pinned to {@link CLIENT_WASM_VERSION}: install
621
+ * it alongside the SDK in apps that create wallets. The indirect import specifier keeps it out of
622
+ * static module resolution — so a missing install fails here with an actionable message, not at build.
623
+ */
624
+ declare const loadClientWasm: ClientWasmLoader;
625
+
626
+ /**
627
+ * Client-side pool of pre-generated Paillier safe-primes (#2).
628
+ *
629
+ * Safe-prime generation is the slow part of a device keygen — minutes in single-threaded
630
+ * browser WASM. Primes don't depend on the key, so we generate them **ahead of the hot path**
631
+ * (a Web Worker during onboarding/idle) and cache a small pool; at keygen time we hand one over
632
+ * instantly. The primes are this device's PRIVATE aux material — they are cached locally and
633
+ * NEVER sent to the server (serving them from the API would let the server recover the device's
634
+ * share). Use an encrypted store in production.
635
+ */
636
+ /** Where the pool persists cached primes. Swap in an encrypted IndexedDB store for production. */
637
+ interface PrimePoolStore {
638
+ /** Remove and return one cached prime for `curve`, or undefined if the pool is empty. */
639
+ take(curve: MpcCurve): Promise<string | undefined>;
640
+ /** Add a generated prime to the pool. */
641
+ add(curve: MpcCurve, primes: string): Promise<void>;
642
+ /** Current number of cached primes for `curve`. */
643
+ size(curve: MpcCurve): Promise<number>;
644
+ }
645
+ /** Default in-memory store (lost on reload). Production should persist to an encrypted store. */
646
+ declare class MemoryPrimeStore implements PrimePoolStore {
647
+ private readonly pools;
648
+ take(curve: MpcCurve): Promise<string | undefined>;
649
+ add(curve: MpcCurve, primes: string): Promise<void>;
650
+ size(curve: MpcCurve): Promise<number>;
651
+ }
652
+ /** The minimal core capability the pool needs — just prime generation. */
653
+ interface PrimeGenerator {
654
+ pregeneratePrimes(curve: MpcCurve): Promise<string>;
655
+ }
656
+ interface PrimePoolOptions {
657
+ /** How many spare primes to keep cached per curve. Default 2. */
658
+ targetSize?: number;
659
+ /** Persistence for the pool. Defaults to an in-memory store. */
660
+ store?: PrimePoolStore;
661
+ }
662
+ declare class PrimePool {
663
+ private readonly core;
664
+ private readonly store;
665
+ private readonly targetSize;
666
+ /** Per-curve in-flight refill, so concurrent calls don't over-generate. */
667
+ private readonly refilling;
668
+ constructor(core: PrimeGenerator, options?: PrimePoolOptions);
669
+ /**
670
+ * Top up the pool to the target size — call this OFF the hot path (onboarding/idle, ideally a
671
+ * Web Worker). Deduped per curve, so calling it repeatedly is safe and cheap.
672
+ */
673
+ ensure(curve: MpcCurve): Promise<void>;
674
+ private refill;
675
+ /**
676
+ * Claim a prime for a keygen. Returns a cached one instantly when the pool is warm; otherwise
677
+ * generates one inline (the slow fallback) so keygen never fails on an empty pool. Either way it
678
+ * kicks off a background refill so the next wallet is instant.
679
+ */
680
+ take(curve: MpcCurve): Promise<string>;
681
+ }
682
+
683
+ /**
684
+ * Device key-share storage contract.
685
+ *
686
+ * In a Waaskey 2-of-3 wallet the user's **device** holds one key share (produced by
687
+ * the device-party keygen — see {@link MpcCore}). That share is the user's half of
688
+ * the key and must (a) never be persisted in plaintext, (b) survive a page reload,
689
+ * and (c) be wiped on explicit logout. The SDK depends on the {@link ShareStore}
690
+ * port; the web implementation seals shares with WebCrypto and persists the
691
+ * ciphertext in IndexedDB, and a native Keychain/Keystore core can implement the
692
+ * same port on mobile.
693
+ */
694
+ /** Stores the device's (already-serialized) key share per wallet, encrypted at rest. */
695
+ interface ShareStore {
696
+ /** Seal and persist the share for `walletId` (overwrites any existing one). */
697
+ put(walletId: string, share: string): Promise<void>;
698
+ /** Load and decrypt the share for `walletId`, or `null` if none is stored. */
699
+ get(walletId: string): Promise<string | null>;
700
+ /** Whether a share is stored for `walletId` (does not decrypt it). */
701
+ has(walletId: string): Promise<boolean>;
702
+ /** Remove the stored share for `walletId` (no-op if absent). */
703
+ remove(walletId: string): Promise<void>;
704
+ /** Wipe every stored share — call on explicit logout. */
705
+ clear(): Promise<void>;
706
+ }
707
+ /**
708
+ * Low-level opaque string key/value persistence backing an encrypting
709
+ * {@link ShareStore}. The web adapter is IndexedDB-backed; an in-memory adapter is
710
+ * used for tests / non-persistent (SSR) contexts. It only ever sees ciphertext.
711
+ */
712
+ interface KeyValueStore {
713
+ get(key: string): Promise<string | null>;
714
+ set(key: string, value: string): Promise<void>;
715
+ delete(key: string): Promise<void>;
716
+ /** All stored keys (used to enumerate stored shares). */
717
+ keys(): Promise<string[]>;
718
+ /** Remove every entry. */
719
+ clear(): Promise<void>;
720
+ }
721
+
722
+ /** Options for the {@link EncryptedShareStore} constructor / {@link EncryptedShareStore.browser}. */
723
+ interface EncryptedShareStoreOptions {
724
+ dbName?: string;
725
+ storeName?: string;
726
+ /** Bypass the {@link MIN_SECRET_LENGTH} entropy floor — only when the secret's entropy is guaranteed elsewhere. */
727
+ allowWeakSecret?: boolean;
728
+ }
729
+ /**
730
+ * {@link ShareStore} that seals every share with AES-256-GCM before handing it to a
731
+ * {@link KeyValueStore}, so the backing storage only ever holds ciphertext.
732
+ *
733
+ * The AES key is derived (PBKDF2) from a **secret the app supplies** — from the
734
+ * user's authenticated session, a passkey/PRF, or a device secret — never embedded
735
+ * in the bundle. A random salt is generated once and persisted alongside the data;
736
+ * `clear()` wipes both, so a subsequent login derives a fresh key.
737
+ *
738
+ * @example
739
+ * ```ts
740
+ * const store = EncryptedShareStore.browser(sessionSecret);
741
+ * await store.put(wallet.id, keygen.keyShare); // sealed in IndexedDB
742
+ * const share = await store.get(wallet.id); // decrypted, or null
743
+ * await store.clear(); // on logout
744
+ * ```
745
+ */
746
+ declare class EncryptedShareStore implements ShareStore {
747
+ private readonly kv;
748
+ private readonly secret;
749
+ private keyPromise?;
750
+ constructor(kv: KeyValueStore, secret: string, options?: {
751
+ allowWeakSecret?: boolean;
752
+ });
753
+ /** Default web store: AES-GCM sealing over IndexedDB. */
754
+ static browser(secret: string, options?: EncryptedShareStoreOptions): EncryptedShareStore;
755
+ put(walletId: string, share: string): Promise<void>;
756
+ get(walletId: string): Promise<string | null>;
757
+ has(walletId: string): Promise<boolean>;
758
+ remove(walletId: string): Promise<void>;
759
+ clear(): Promise<void>;
760
+ /** Lazily load (or create) the persisted salt and derive the AES key once. */
761
+ private key;
762
+ private loadKey;
763
+ }
764
+
765
+ /**
766
+ * In-memory {@link KeyValueStore} — non-persistent. Used in tests and as a fallback
767
+ * where IndexedDB is unavailable (e.g. SSR). Shares stored here do NOT survive a
768
+ * reload; for persistent device storage use {@link IndexedDbKeyValueStore}.
769
+ */
770
+ declare class MemoryKeyValueStore implements KeyValueStore {
771
+ private readonly map;
772
+ get(key: string): Promise<string | null>;
773
+ set(key: string, value: string): Promise<void>;
774
+ delete(key: string): Promise<void>;
775
+ keys(): Promise<string[]>;
776
+ clear(): Promise<void>;
777
+ }
778
+
779
+ /**
780
+ * IndexedDB-backed {@link KeyValueStore} — the default persistent backend on web.
781
+ * A single object store holds opaque ciphertext records keyed by string, so device
782
+ * shares survive a reload. Stores only sealed data (see EncryptedShareStore).
783
+ */
784
+ declare class IndexedDbKeyValueStore implements KeyValueStore {
785
+ private readonly dbName;
786
+ private readonly storeName;
787
+ private dbPromise?;
788
+ constructor(dbName?: string, storeName?: string);
789
+ private db;
790
+ private run;
791
+ get(key: string): Promise<string | null>;
792
+ set(key: string, value: string): Promise<void>;
793
+ delete(key: string): Promise<void>;
794
+ keys(): Promise<string[]>;
795
+ clear(): Promise<void>;
796
+ }
797
+
798
+ /**
799
+ * Structured error model for the SDK.
800
+ *
801
+ * Every failure surfaces as a {@link WaaskeyError} carrying a typed {@link WaaskeyErrorCode}
802
+ * — never a bare string — so callers can branch on `error.code` instead of matching
803
+ * message text. Transport/HTTP failures map the API's error envelope
804
+ * (`{ statusCode, message, error }`) to a code; orchestration failures (device keygen,
805
+ * missing share, cancellation) use the SDK-side codes.
806
+ */
807
+ type WaaskeyErrorCode = 'unauthorized' | 'forbidden' | 'not_found' | 'validation' | 'conflict' | 'rate_limited' | 'server_error' | 'network' | 'unsupported_chain' | 'provider_error' | 'device_core_required' | 'keygen_failed' | 'sign_failed' | 'reshare_failed' | 'reshare_pubkey_mismatch' | 'share_not_found' | 'recovery_failed' | 'invalid_recovery_code' | 'backup_failed' | 'wallet_activation_timeout' | 'sign_ceremony_timeout' | 'unauthenticated' | 'firebase_not_enabled' | 'mfa_required' | 'no_member' | 'unsupported' | 'aborted' | 'unknown';
808
+ interface WaaskeyErrorOptions {
809
+ /** HTTP status, when the error originates from an API response. */
810
+ status?: number;
811
+ /** Raw error payload from the API (or other context), for debugging. */
812
+ details?: unknown;
813
+ /** The underlying error that triggered this one. */
814
+ cause?: unknown;
815
+ }
816
+ /** The single error type thrown by the SDK. */
817
+ declare class WaaskeyError extends Error {
818
+ readonly code: WaaskeyErrorCode;
819
+ readonly status?: number;
820
+ readonly details?: unknown;
821
+ constructor(message: string, code: WaaskeyErrorCode, options?: WaaskeyErrorOptions);
822
+ }
823
+
1
824
  /**
2
825
  * Public contract for the Waaskey SDK.
3
826
  *
4
- * These types describe the wire shapes the SDK exchanges with the Waaskey API.
5
- * They will be replaced by types generated from the backend OpenAPI spec once the
6
- * API contract is stable; until then this file is the single source of truth.
827
+ * These types mirror the wire shapes the Waaskey API exchanges (the backend
828
+ * `@waas/types` request/response contracts). They will be replaced by types
829
+ * generated from the backend OpenAPI spec once it is published; until then this
830
+ * file is the single source of truth and must track the API exactly — a drift
831
+ * here is a contract bug.
7
832
  */
833
+
8
834
  /** Chains a wallet can be created on. */
9
835
  type Chain = 'ethereum' | 'polygon' | 'arbitrum' | 'base' | 'optimism' | 'bitcoin' | 'solana';
836
+ /**
837
+ * Threshold-signing curve a wallet uses (backend `WalletCurve`). `ed25519` is threshold EdDSA
838
+ * (FROST, RFC 8032/9591) — the ed25519 chains (Solana, NEAR, Aptos, Sui); the others are cggmp24
839
+ * ECDSA. An `ed25519` wallet's device share and sign flow are the FROST path, distinct from the
840
+ * cggmp24 KeyShare blob (see the `mpc` Eddsa* types).
841
+ */
842
+ type WalletCurve = 'secp256k1' | 'ed25519';
843
+ /**
844
+ * Lifecycle state of a wallet — tracks keygen progress (backend `WalletStatus`).
845
+ * `pending_keygen` is a member-bound wallet (#342/#344) whose keygen roster is still open —
846
+ * waiting for every {@link WalletShareholder} to {@link Wallets.joinCeremony} before the
847
+ * multi-device keygen ceremony runs. Not fundable (no `publicKey`/`address`) until `active`.
848
+ */
849
+ type WalletStatus = 'pending' | 'pending_keygen' | 'active' | 'failed';
850
+ /**
851
+ * Custody role of a single MPC key share, independent of its party-role label
852
+ * (backend `CustodyKind`, #293). Drives the wallet's custody attestation:
853
+ * `platform_signer` / `platform_recovery` are shares WaaS itself holds; the other
854
+ * kinds are shares only the user or a third party holds.
855
+ */
856
+ type CustodyKind = 'user_device' | 'user_backup' | 'platform_signer' | 'platform_recovery' | 'external_party';
857
+ /**
858
+ * The custody posture a wallet's topology attests to (backend `CustodyType`, #293),
859
+ * derived from its {@link CustodyKind} shares vs the threshold `t`:
860
+ * `embedded` when the platform alone holds `>= t` shares (custodial-capable — the
861
+ * default 2-of-3), `self_custody` when the platform holds none, `shared` otherwise
862
+ * (neither party alone meets `t` — a true multi-party quorum).
863
+ */
864
+ type CustodyType = 'embedded' | 'shared' | 'self_custody';
10
865
  /** Options accepted by `new Waaskey(...)`. */
11
866
  interface WaaskeyOptions {
12
867
  /** Publishable API key issued from the Waaskey dashboard. */
@@ -15,81 +870,1825 @@ interface WaaskeyOptions {
15
870
  baseUrl?: string;
16
871
  /** Custom fetch implementation (e.g. for non-browser runtimes). Defaults to global `fetch`. */
17
872
  fetch?: typeof fetch;
873
+ /**
874
+ * Device-party MPC core (e.g. `WasmMpcCore`). Required to create a wallet — the
875
+ * device runs its half of the keygen ceremony with this.
876
+ */
877
+ mpc?: MpcCore;
878
+ /**
879
+ * Where the device key share is sealed and persisted (e.g.
880
+ * `EncryptedShareStore.browser(secret)`). Required to create a wallet.
881
+ */
882
+ shareStore?: ShareStore;
883
+ /**
884
+ * Optional pool of pre-generated Paillier primes (`new PrimePool(mpc)`). When set,
885
+ * `wallets.prewarm(chain)` fills it off the hot path and `wallets.create` claims from
886
+ * it instead of generating inline — turning minutes of keygen into seconds.
887
+ */
888
+ primePool?: PrimePool;
889
+ /**
890
+ * Per-chain provider config for client-side balance reads (RPC URL or a custom
891
+ * provider). EVM chains have built-in public-RPC defaults; override them here.
892
+ */
893
+ chains?: Partial<Record<Chain, ChainConfig>>;
894
+ /**
895
+ * Analytics for the tenant dashboard (wallet created/signed/recovered — no PII or secrets).
896
+ * Defaults to an HTTP sink to the Waaskey API; pass a custom {@link AnalyticsSink}, or `false`
897
+ * to opt out entirely.
898
+ */
899
+ analytics?: AnalyticsSink | false;
18
900
  }
19
901
  /** Parameters for creating a wallet. */
20
902
  interface CreateWalletParams {
21
- /** Your end-user's id in your own system; binds the wallet to that user. */
22
- userId?: string;
23
- /** Chain the wallet is created on. */
903
+ /** Chain the wallet is created on (determines the signing curve). */
24
904
  chain: Chain;
905
+ /** Human label for the wallet. Defaults to the chain name. */
906
+ label?: string;
907
+ /**
908
+ * Signing threshold `t` for this wallet's `t`-of-`n` MPC key. Optional — omit the whole
909
+ * custody policy for the deployment default (embedded 2-of-3). When a topology is supplied
910
+ * it must satisfy `2 <= threshold <= parties.length` (validated client-side before the request).
911
+ */
912
+ threshold?: number;
913
+ /**
914
+ * Ordered party roles of the keygen topology, length `n` (e.g. `['device','server','recovery']`).
915
+ * Optional. When supplied it must be at least 2 distinct, non-empty role strings.
916
+ */
917
+ parties?: string[];
918
+ /**
919
+ * Per-party custody kind, parallel to {@link parties} (same length, same order) (#293). Optional —
920
+ * when omitted the backend derives the kind from each party's role name. When supplied it must
921
+ * have exactly one entry per party.
922
+ */
923
+ custodyKinds?: CustodyKind[];
924
+ /**
925
+ * Requested custody posture (#293) — the backend enforces it against the resolved
926
+ * `(threshold, custodyKinds)` and refuses the create (400) when the topology does not attest to it
927
+ * (e.g. asking for `self_custody` but the shares are platform-heavy enough to be `embedded`).
928
+ */
929
+ custodyType?: CustodyType;
930
+ }
931
+ /** Options for `wallets.create(...)`. */
932
+ interface CreateWalletOptions {
933
+ /** Cancel the create (request + ceremony + activation wait). */
934
+ signal?: AbortSignal;
935
+ /** Wait until the wallet is ACTIVE (keygen finished) before resolving. Default `true`. */
936
+ waitForActive?: boolean;
937
+ /** Max time to wait for activation, ms. Default 60000. */
938
+ activationTimeoutMs?: number;
939
+ /** Poll interval while waiting for activation, ms. Default 1000. */
940
+ pollIntervalMs?: number;
941
+ /**
942
+ * Recovery-backup material for a **non-custodial** `[device, server, user_backup]` create (#351/#78):
943
+ * REQUIRED when the returned ceremony carries a `user_backup` party ({@link WalletCeremony.additionalParties}).
944
+ * In that topology the device runs a SECOND client party (`user_backup`) whose share replaces the
945
+ * platform's recovery share — so it must be sealed with the user's recovery code and registered as an
946
+ * opaque ciphertext server-side at keygen time (the same mechanism as `recovery.register`), or the
947
+ * wallet would be unrecoverable on device loss. Absent/ignored for a single-client-party create
948
+ * (`[device, server]`, custodial `[device, server, recovery]`, ed25519).
949
+ */
950
+ backup?: WalletBackupParams;
951
+ }
952
+ /**
953
+ * Recovery-backup factors sealing + enrolling the client-held `user_backup` share on a non-custodial
954
+ * create ({@link CreateWalletOptions.backup}, #351/#78) — the `recovery.register` material minus the
955
+ * `share` (the SDK supplies the freshly-generated user_backup share itself). The `recoveryCode` is
956
+ * **caller-provided** (unlike `recovery.register`, which may generate one): `create` returns the
957
+ * {@link Wallet}, so it cannot hand a generated code back — the caller mints one up front (e.g. with
958
+ * `generateRecoveryCode()`), shows it to the user, and passes it here. It is a client-side sealing
959
+ * secret and never leaves the device (only its SHA-256 is enrolled), so a lost code is an
960
+ * unrecoverable backup.
961
+ */
962
+ interface WalletBackupParams {
963
+ /** High-entropy recovery code that seals the user_backup share. Show it to the user once — it is the only key to the backup and never leaves the client. */
964
+ recoveryCode: string;
965
+ /** Base32 TOTP secret to enrol the authenticator factor. */
966
+ totpSecret: string;
967
+ /** Email address to enrol the email-OTP factor. */
968
+ email: string;
969
+ /** Extra factor enrolments beyond the standard three. */
970
+ extraFactors?: FactorEnrollment[];
971
+ }
972
+ /** Options for `wallets.joinCeremony(...)` (#349). */
973
+ interface JoinCeremonyOptions {
974
+ /** Cancel the join (roster ack + the relay keygen ceremony). */
975
+ signal?: AbortSignal;
976
+ }
977
+ /** Options for `wallets.joinSignCeremony(...)` (#349). */
978
+ interface JoinSignCeremonyOptions {
979
+ /** Cancel the join (the approve vote + waiting for the quorum + the relay sign ceremony). */
980
+ signal?: AbortSignal;
981
+ /** Max time to wait for the t-of-n quorum to fix and select this member, ms. Default 60000. */
982
+ readyTimeoutMs?: number;
983
+ /** Poll interval while waiting for the quorum, ms. Default 1000. */
984
+ pollIntervalMs?: number;
985
+ }
986
+ /**
987
+ * A WebAuthn assertion attached for passkey step-up (issue #21/#39) — the structural
988
+ * shape of `@simplewebauthn/browser`'s `AuthenticationResponseJSON`. Declared here (not
989
+ * imported) so the field is typed without a hard dependency on the optional passkey peer.
990
+ */
991
+ interface PasskeyAssertionJSON {
992
+ id: string;
993
+ rawId: string;
994
+ type: string;
995
+ response: {
996
+ clientDataJSON: string;
997
+ authenticatorData: string;
998
+ signature: string;
999
+ userHandle?: string;
1000
+ };
1001
+ authenticatorAttachment?: unknown;
1002
+ clientExtensionResults?: unknown;
25
1003
  }
26
- /** A wallet as returned by the API. */
1004
+ /** Options for signing. */
1005
+ interface SignOptions {
1006
+ /** Cancel the signing request. */
1007
+ signal?: AbortSignal;
1008
+ /**
1009
+ * When `true`, run a WebAuthn assertion before submitting (passkey step-up,
1010
+ * Pattern B / issue #21/#39). The SDK fetches a server-issued one-time challenge,
1011
+ * prompts the user's authenticator over it, and attaches the resulting
1012
+ * `passkeyAssertion` (+ its `passkeyChallengeId`) to the POST body.
1013
+ *
1014
+ * Requires WebAuthn / `@simplewebauthn/browser` to be available. Throws
1015
+ * `WaaskeyError('unsupported')` when passkeys are unavailable, and
1016
+ * `WaaskeyError('aborted')` when the user cancels the prompt.
1017
+ *
1018
+ * Alternatively pass a pre-built assertion directly via `passkeyAssertion`.
1019
+ */
1020
+ requirePasskey?: boolean;
1021
+ /**
1022
+ * A pre-built WebAuthn `AuthenticationResponseJSON` to attach as
1023
+ * `passkeyAssertion` on the sign request. Takes precedence over
1024
+ * `requirePasskey` when supplied.
1025
+ */
1026
+ passkeyAssertion?: PasskeyAssertionJSON;
1027
+ /**
1028
+ * Id of the server-issued step-up challenge the pre-built `passkeyAssertion` was
1029
+ * produced against (from {@link StepUpChallengeResponse}). The server verifies the
1030
+ * assertion against this one-time challenge and burns it. Required only when you
1031
+ * supply `passkeyAssertion` yourself; with `requirePasskey` the SDK fetches and
1032
+ * echoes it for you.
1033
+ */
1034
+ passkeyChallengeId?: string;
1035
+ /**
1036
+ * Credential id to restrict the passkey assertion to (used with `requirePasskey`).
1037
+ * When omitted the browser presents all resident credentials for the RP.
1038
+ */
1039
+ passkeyCredentialId?: string;
1040
+ }
1041
+ /** Options for sending a transaction. */
1042
+ interface SendOptions {
1043
+ /** Cancel the send request. */
1044
+ signal?: AbortSignal;
1045
+ /**
1046
+ * When `true`, run a WebAuthn assertion before submitting (passkey step-up,
1047
+ * Pattern B / issue #21/#39), over a server-issued one-time challenge.
1048
+ */
1049
+ requirePasskey?: boolean;
1050
+ /**
1051
+ * A pre-built WebAuthn `AuthenticationResponseJSON` to attach as
1052
+ * `passkeyAssertion` on the send request. Takes precedence over
1053
+ * `requirePasskey` when supplied.
1054
+ */
1055
+ passkeyAssertion?: PasskeyAssertionJSON;
1056
+ /**
1057
+ * Id of the server-issued step-up challenge the pre-built `passkeyAssertion` was
1058
+ * produced against (from {@link StepUpChallengeResponse}). Required only when you
1059
+ * supply `passkeyAssertion` yourself; with `requirePasskey` the SDK handles it.
1060
+ */
1061
+ passkeyChallengeId?: string;
1062
+ /** Credential id to restrict the passkey assertion to (used with `requirePasskey`). */
1063
+ passkeyCredentialId?: string;
1064
+ }
1065
+ /** The step-up operation a passkey challenge is minted for (issue #39). */
1066
+ type StepUpOperation = 'sign' | 'send';
1067
+ /**
1068
+ * A server-issued **one-time** WebAuthn step-up challenge (issue #39). The SDK
1069
+ * fetches this before running a passkey assertion so the challenge is a fresh
1070
+ * server nonce — not derived from the request payload — which the server verifies
1071
+ * and **burns** on use, making a captured assertion non-replayable.
1072
+ */
1073
+ interface StepUpChallengeResponse {
1074
+ /** Opaque id the client echoes back (`passkeyChallengeId`) so the server can verify + burn the challenge. */
1075
+ challengeId: string;
1076
+ /** base64url one-time challenge nonce the authenticator signs over. */
1077
+ challenge: string;
1078
+ }
1079
+ /** Per-party relay coordination returned on create so the device can join the keygen ceremony (backend `IWalletCeremony`). */
1080
+ interface WalletCeremony {
1081
+ relayUrl: string;
1082
+ sessionId: string;
1083
+ curve: WalletCurve;
1084
+ role: string;
1085
+ peerRole: string;
1086
+ partyIndex: number;
1087
+ peerPartyIndex: number;
1088
+ parties: number;
1089
+ threshold: number;
1090
+ /** Short-lived relay token (JWT) the device presents to join this keygen session; present only when relay auth is enabled. */
1091
+ relayToken?: string;
1092
+ /**
1093
+ * The FROST DKG round-2 encryption roster — **ed25519 keygen only** (#114, backend `IWalletCeremony`).
1094
+ * One X25519 encryption PUBLIC key (32-byte hex) per party, in `parties`/protocol-index order, so
1095
+ * `encPubkeys[i]` is the key of FROST participant `i + 1` (e.g. `[deviceEncPubkey, serverEncPubkey]`
1096
+ * for `[device, server]`). The device seals its round-2 packages to these keys (chiefly the server's).
1097
+ * The device's own entry is the `deviceEncPubkey` it supplied on create, echoed back so it builds the
1098
+ * exact same ordered roster the signer uses. Absent for secp keygen and on a refresh fetch.
1099
+ */
1100
+ encPubkeys?: string[];
1101
+ /**
1102
+ * Additional CLIENT-run party descriptors the SAME caller must ALSO drive for this keygen ceremony,
1103
+ * beyond the primary `device` party this object describes (backend `IWalletCeremony`, #396/#351/#78).
1104
+ * Present for the non-custodial default `[device, server, user_backup]` (secp256k1): ONE entry — the
1105
+ * client-held `user_backup` party (`role: 'user_backup'`, its own `partyIndex`, `peerRole: 'server'`,
1106
+ * and its OWN role-scoped {@link relayToken}) — because the platform signer drives ONLY the `server`
1107
+ * share, so the one device must run BOTH its `device` party (this object) AND the `user_backup` party.
1108
+ * Each entry is a full, self-contained {@link WalletCeremony} joining the SAME relay `sessionId`.
1109
+ *
1110
+ * ADDITIVE: absent for single-client-party rosters (`[device, server]`, custodial
1111
+ * `[device, server, recovery]`) and for ed25519 — a client that reads only `ceremony` and ignores
1112
+ * this field keeps running exactly one party. Each entry's own `additionalParties` is always absent
1113
+ * (the list is flat, never recursive).
1114
+ */
1115
+ additionalParties?: WalletCeremony[];
1116
+ }
1117
+ /** A wallet as returned by the API (backend `WalletResponse`). Dates are ISO strings on the wire. */
27
1118
  interface WalletData {
28
- /** Waaskey wallet id, e.g. `wlt_...`. */
29
1119
  id: string;
30
- /** On-chain address. */
31
- address: string;
32
- /** Chain the wallet belongs to. */
33
- chain: Chain;
1120
+ tenantId: string;
1121
+ label: string;
1122
+ curve: WalletCurve;
1123
+ status: WalletStatus;
1124
+ /** Compressed public key (hex). Set once keygen completes. */
1125
+ publicKey?: string;
1126
+ /** On-chain address derived from the public key. Set once keygen completes. */
1127
+ address?: string;
1128
+ /** When the key shares were last proactively rotated. */
1129
+ keyRefreshedAt?: string;
1130
+ /** Effective signing threshold `t` of the wallet's `t`-of-`n` key (the persisted value, or the deployment default). */
1131
+ threshold: number;
1132
+ /** Effective ordered party roles of the wallet's keygen topology, length `n`. */
1133
+ parties: string[];
1134
+ /** Effective per-party custody kind, parallel to {@link parties} (same length, same order). */
1135
+ custodyKinds: CustodyKind[];
1136
+ /** Count of {@link custodyKinds} entries the platform itself holds (`platform_signer` + `platform_recovery`). */
1137
+ platformShareCount: number;
1138
+ /**
1139
+ * The wallet's custody attestation (#293): `embedded` when `platformShareCount >= threshold`
1140
+ * (platform alone is custodial-capable — the default 2-of-3), `self_custody` when
1141
+ * `platformShareCount === 0`, else `shared`. See {@link isNonCustodial}.
1142
+ */
1143
+ custodyType: CustodyType;
1144
+ createdAt: string;
1145
+ /**
1146
+ * Keygen ceremony params — present on create and on `GET` while the wallet is `pending`
1147
+ * (backend `WalletResponse.ceremony`). **Absent** for a member-bound wallet (#342): it is
1148
+ * provisioned `pending_keygen` with no single ceremony to join here — each
1149
+ * {@link WalletShareholder} instead runs {@link Wallets.joinCeremony} for its own party.
1150
+ */
1151
+ ceremony?: WalletCeremony;
34
1152
  }
35
- /** Error thrown for any non-2xx API response. */
36
- declare class WaaskeyError extends Error {
37
- readonly status: number;
38
- readonly code?: string | undefined;
39
- constructor(message: string, status: number, code?: string | undefined);
1153
+ /**
1154
+ * Response to creating a wallet (backend `CreateWalletResponse`) — structurally identical to
1155
+ * {@link WalletData} (`ceremony` lives there now, since `GET` returns it too while pending).
1156
+ */
1157
+ type CreateWalletResponse = WalletData;
1158
+ /** Parameters for `wallets.createWallet(...)` — the member-bound create (#342). */
1159
+ interface CreateMemberWalletParams {
1160
+ /** Human label for the wallet. */
1161
+ label: string;
1162
+ /** Elliptic curve for threshold signing. Defaults to secp256k1 (the backend default) when omitted. */
1163
+ curve?: WalletCurve;
1164
+ /**
1165
+ * The `N` org membership ids that each hold one share of this wallet's key. The backend derives
1166
+ * the full topology from them — `n = N+1` parties (the N members + exactly one platform share),
1167
+ * `parties = ['member:<membershipId>' × N, 'platform']`. Every id must be a membership of the
1168
+ * caller's tenant with the `canHoldShare` capability, and distinct.
1169
+ */
1170
+ shareholderMembershipIds: string[];
1171
+ /** Signing threshold `t` (`2 <= t <= N+1`). Defaults to 2 when omitted. */
1172
+ threshold?: number;
1173
+ }
1174
+ /**
1175
+ * A single org-member share-holder of a member-bound wallet's `t`-of-`n` key (#342, backend
1176
+ * `IWalletShareholder`). The N member share-holders plus exactly one (non-row) platform share make
1177
+ * up the wallet's N+1 topology.
1178
+ */
1179
+ interface WalletShareholder {
1180
+ id: string;
1181
+ tenantId: string;
1182
+ walletId: string;
1183
+ /** The membership that holds this share — must have `canHoldShare`. */
1184
+ membershipId: string;
1185
+ /** 0-based index of this party in the wallet's topology (the protocol party index). */
1186
+ partyIndex: number;
1187
+ /** Deterministic protocol role string for this party — `member:<membershipId>`. */
1188
+ role: string;
1189
+ /** Custody kind of this share (a member share-holder is always `user_device`). */
1190
+ custodyKind: CustodyKind;
1191
+ /** The member's device this share is bound to, once enrolled. Absent before it binds. */
1192
+ deviceId?: string;
1193
+ /** When this member's device joined/ack'd the keygen roster ({@link Wallets.joinCeremony}). Absent until it acks. */
1194
+ joinedAt?: string;
1195
+ createdAt: string;
1196
+ }
1197
+ /**
1198
+ * The CALLING member's own party of a member-bound wallet's multi-device keygen ceremony (#344,
1199
+ * backend `IMemberCeremony`), returned by {@link Wallets.joinCeremony}'s internal `ceremony/mine`
1200
+ * fetch. Carries only the caller's own party params — never another party's. Unlike
1201
+ * {@link WalletCeremony} (the 2-party device/server keygen), an n-party member ceremony has no
1202
+ * single peer: every other member + the platform party join the SAME relay session, so the caller
1203
+ * only needs its own `partyIndex`, the total party count `parties`, and a relay token bound to its
1204
+ * `{sessionId, role, sub=membershipId}`.
1205
+ */
1206
+ interface MemberCeremony {
1207
+ /** Relay websocket URL the member device connects to. */
1208
+ relayUrl: string;
1209
+ /** Ceremony id shared by every party on the relay — the wallet id (registered verbatim). */
1210
+ sessionId: string;
1211
+ curve: WalletCurve;
1212
+ /** The caller's own relay routing role — `member:<membershipId>` (registered byte-for-byte). */
1213
+ role: string;
1214
+ /** The caller's own 0-based protocol party index in the wallet's topology. */
1215
+ partyIndex: number;
1216
+ /** Total parties `n` (= N members + the single platform party). */
1217
+ parties: number;
1218
+ /** Signing threshold `t`. */
1219
+ threshold: number;
1220
+ /**
1221
+ * Short-lived relay token (JWT) the device presents to join this session as {@link role}. Bound
1222
+ * to `{sessionId, role, sub=membershipId}`; present only when relay authentication is enabled.
1223
+ */
1224
+ relayToken?: string;
1225
+ }
1226
+ /**
1227
+ * Result of a member device acking the keygen roster (#344, backend `MemberCeremonyJoinResponse`) —
1228
+ * reports roster progress so the caller can render "3 of 4 devices ready" and learn when the
1229
+ * multi-device keygen has started ({@link rosterComplete} flips true on the last member's join).
1230
+ */
1231
+ interface MemberCeremonyJoinResponse {
1232
+ walletId: string;
1233
+ /** The wallet status after the ack — `pending_keygen` while filling / running, `active` once complete. */
1234
+ status: WalletStatus;
1235
+ /** How many of the wallet's member share-holders have joined/ack'd so far. */
1236
+ joined: number;
1237
+ /** The total member share-holders `N` that must join before keygen runs. */
1238
+ total: number;
1239
+ /** True once every member has joined — the multi-device keygen ceremony has been triggered. */
1240
+ rosterComplete: boolean;
1241
+ }
1242
+ /** Response to signing (backend `SignMessageResponse`). */
1243
+ interface SignMessageResponse {
1244
+ walletId: string;
1245
+ /** Hex-encoded signature produced by the MPC protocol. */
1246
+ signature: string;
1247
+ }
1248
+ /**
1249
+ * Lifecycle of a send/sweep signing activity (backend `TxStatus`). WaaS is
1250
+ * **sign-only** — a send is built (`pending`), MPC-signed (`signed`, terminal:
1251
+ * the signed raw tx is returned for the client to broadcast), or its ceremony
1252
+ * fails (`failed`). WaaS never broadcasts, so there is no on-chain/broadcast state.
1253
+ */
1254
+ type TxStatus = 'pending' | 'signed' | 'failed';
1255
+ /** Parameters to send a transaction (backend `SendTxRequest`). */
1256
+ interface SendParams {
1257
+ /** Target chain id, e.g. `evm:1`, `evm:11155111`. */
1258
+ chainId: string;
1259
+ /** Destination address. */
1260
+ to: string;
1261
+ /** Transfer amount in the chain's base unit (wei), as a numeric string. */
1262
+ value?: string;
1263
+ /** Arbitrary call data (hex) for contract interactions. */
1264
+ data?: string;
1265
+ }
1266
+ /**
1267
+ * Result of a send (backend `SendTxResponse`). WaaS is **sign-only**: it builds
1268
+ * and MPC-signs the transaction and returns the signed raw tx — it does **not**
1269
+ * broadcast it. Submit `signedTx` from your own node/provider (see
1270
+ * {@link Waaskey.broadcast} or your own submitter).
1271
+ */
1272
+ interface SendResult {
1273
+ walletId: string;
1274
+ chainId: string;
1275
+ /**
1276
+ * The signed raw transaction — the canonical broadcast payload the **client**
1277
+ * submits to its own node (an EVM raw RLP tx hex, a Bitcoin tx hex, …). WaaS
1278
+ * never broadcasts this.
1279
+ */
1280
+ signedTx: string;
1281
+ /**
1282
+ * The transaction id, computed offline as a deterministic hash of the signed
1283
+ * tx — for reference/tracking only. It is **not** fetched from a node and is
1284
+ * not proof of broadcast or confirmation.
1285
+ */
1286
+ txHash: string;
1287
+ /** The 32-byte digest the MPC signer signed. */
1288
+ digest: string;
1289
+ }
1290
+ /**
1291
+ * The relay coordination + raw message the device co-signs for an ed25519 send (backend
1292
+ * `EddsaSendSessionResponse`, #110). Returned by the START phase (`POST …/send-session`). Unlike the
1293
+ * secp {@link SignSessionResponse} (a 32-byte digest + 2-party peer roles), this carries the FROST
1294
+ * signing quorum roster (`roles` in `participants` order) and the WHOLE `message` bytes (ed25519 signs
1295
+ * the message, not a digest).
1296
+ */
1297
+ interface EddsaSendSession {
1298
+ /** The pending signing-activity row id — echoed back to the ASSEMBLE phase to finalize the signed tx. */
1299
+ txId: string;
1300
+ /** Relay websocket URL the device connects to. */
1301
+ relayUrl: string;
1302
+ /** Relay session id shared by the device + server parties. */
1303
+ sessionId: string;
1304
+ /** The FROST signing quorum's relay roles, in `participants` order (`roles[i]` ↔ `participants[i]`), e.g. `['device','server']`. */
1305
+ roles: string[];
1306
+ /** The 1-based FROST identifiers of the quorum, parallel to {@link roles} (e.g. `[1, 2]`). */
1307
+ participants: number[];
1308
+ /** This device's own 0-based slot into {@link roles} (its position in the quorum). */
1309
+ signerPosition: number;
1310
+ /** The raw message bytes to sign, hex (`0x` prefix optional) — the chain adapter's serialized tx message. */
1311
+ message: string;
1312
+ /** Short-lived relay token (JWT) the device presents to join this session; present only when relay auth is enabled. */
1313
+ relayToken?: string;
1314
+ }
1315
+ /** Body of the ASSEMBLE phase (`POST …/send-session/:txId/assemble`) — the aggregated ed25519 signature the backend embeds into the wire tx (backend `AssembleEddsaTxRequest`). */
1316
+ interface EddsaAssembleRequest {
1317
+ /** The 64-byte RFC 8032 ed25519 signature (hex) the device + server co-produced. */
1318
+ signature: string;
1319
+ }
1320
+ /** How a signature was produced (backend `SignatureKind`). */
1321
+ type SignatureKind = 'message' | 'personal_sign' | 'typed_data' | 'session' | 'transaction';
1322
+ /**
1323
+ * A record in the wallet's unified **signing activity** (backend `SignatureResponse`,
1324
+ * #289). A raw sign is `kind` + `digest` + `signature`; a send/sweep (`kind =
1325
+ * 'transaction'`) additionally carries the tx fields, including the `signedTx` the
1326
+ * client broadcasts. Dates are ISO strings on the wire.
1327
+ */
1328
+ interface Signature {
1329
+ id: string;
1330
+ tenantId: string;
1331
+ walletId: string;
1332
+ kind: SignatureKind;
1333
+ /** 32-byte hex digest that was signed. */
1334
+ digest?: string;
1335
+ /** Hex-encoded signature from the MPC protocol. */
1336
+ signature?: string;
1337
+ /** Chain id, e.g. `evm:1` (send/sweep only). */
1338
+ chainId?: string;
1339
+ /** Destination address (send/sweep only). */
1340
+ to?: string;
1341
+ /** Amount in the chain's base unit, numeric string (send/sweep value transfer). */
1342
+ value?: string;
1343
+ /** The signed raw transaction returned to the client to broadcast (send/sweep, once signed). */
1344
+ signedTx?: string;
1345
+ /** Transaction id computed offline from the signed tx (send/sweep, once signed). */
1346
+ txHash?: string;
1347
+ /** Send/sweep lifecycle status (absent for a plain sign). */
1348
+ status?: TxStatus;
1349
+ createdAt: string;
1350
+ }
1351
+ /** A wallet operation gated behind approval before its MPC ceremony runs (backend `WalletActionType`). */
1352
+ type WalletActionType = 'sign' | 'send';
1353
+ /**
1354
+ * Lifecycle of an asynchronous, approval-gated signing request (backend `SigningRequestStatus`).
1355
+ * `requested` is the single device-approval flow (#229, back-compat); `pending_approval` is the
1356
+ * M-of-N approver-quorum flow (#309) — distinct so a caller can tell the two apart. `approved`
1357
+ * means the quorum settled and the MPC ceremony started; it flips to `signed` once the ceremony
1358
+ * completes, or `declined`/`expired`/`failed` otherwise.
1359
+ */
1360
+ type SigningRequestStatus = 'requested' | 'pending_approval' | 'approved' | 'signed' | 'declined' | 'expired' | 'failed';
1361
+ /**
1362
+ * Relay coordination to run an approved request's MPC ceremony (backend `SignSessionResponse`).
1363
+ * Returned only from `approveSignRequest` of a `sign` action, once the approval requirement is
1364
+ * met — the device runs its half of the ceremony with these params.
1365
+ */
1366
+ interface SignSessionResponse {
1367
+ relayUrl: string;
1368
+ sessionId: string;
1369
+ curve: WalletCurve;
1370
+ /** The device party's relay routing id (e.g. `"device"`). */
1371
+ role: string;
1372
+ /** The server party's relay routing id (e.g. `"server"`). */
1373
+ peerRole: string;
1374
+ /** This device's signer slot — its keygen index among the participants. */
1375
+ signerPosition: number;
1376
+ /** Keygen indices of the signing quorum, in protocol order (e.g. `[0, 1]`). */
1377
+ participants: number[];
1378
+ /** Short-lived relay token (JWT) the device presents to join this session; present only when relay auth is enabled. */
1379
+ relayToken?: string;
1380
+ /** Additive BIP32 child tweak (32-byte hex) applied when signing under a derived deposit address. */
1381
+ tweak?: string;
1382
+ /** The 32-byte hex digest to sign, present only when the server built the transaction server-side. */
1383
+ digest?: string;
1384
+ }
1385
+ /**
1386
+ * An asynchronous wallet action (sign/send) awaiting approval — either the device owner
1387
+ * (single-approval, #229) or an M-of-N approver quorum (#309) — (backend `SigningRequestResponse`).
1388
+ */
1389
+ interface SigningRequestResponse {
1390
+ id: string;
1391
+ walletId: string;
1392
+ /** The operation being approved. */
1393
+ action: WalletActionType;
1394
+ status: SigningRequestStatus;
1395
+ /** 32-byte hex digest being signed (the message for `sign`; the tx digest for `send`, once built). */
1396
+ digest?: string;
1397
+ /** Present once a `sign` action is approved and signed. */
1398
+ signature?: string;
1399
+ /** Transaction id computed offline from the signed tx. Present once a `send` action is signed. */
1400
+ txHash?: string;
1401
+ createdAt: string;
1402
+ /** ISO time after which an un-approved request expires. */
1403
+ expiresAt: string;
1404
+ /**
1405
+ * Returned only from `approveSignRequest` of a `sign` action: the relay coordination params
1406
+ * the approving device runs its half of the MPC ceremony with — immediately for a non-quorum
1407
+ * request, or once an approver quorum settles APPROVED. Absent on `get` and on `send` approvals.
1408
+ */
1409
+ session?: SignSessionResponse;
1410
+ /**
1411
+ * Under an M-of-N approver quorum (#309): the number of further approvals still needed before
1412
+ * the MPC ceremony runs. `0` once the quorum is reached (the ceremony has started and a
1413
+ * `session` is returned). Absent for a non-quorum (single device-approval) request.
1414
+ */
1415
+ approvalsRemaining?: number;
1416
+ }
1417
+ /**
1418
+ * The CALLING member's own party of a member-bound sign-request's multi-device SIGN ceremony
1419
+ * (#347, backend `IMemberSignCeremony`), returned by {@link Wallets.joinSignCeremony}'s internal
1420
+ * `sign-requests/:reqId/ceremony/mine` fetch. The signing analogue of {@link MemberCeremony}
1421
+ * (keygen): only the caller's own params, never another party's. Because a sign is a t-of-n
1422
+ * SELECTION (only `t` of the N+1 parties sign), the participant set is not known until `t-1`
1423
+ * members have approved and the quorum is FIXED — so the quorum fields appear only once
1424
+ * {@link ready}: `false` while the roster is still collecting approvals, or when the caller was
1425
+ * not selected into the fixed quorum.
1426
+ */
1427
+ interface MemberSignCeremony {
1428
+ /** Relay websocket URL the member device connects to. */
1429
+ relayUrl: string;
1430
+ /** The sign ceremony's relay session id (unique per sign-request) — registered verbatim. */
1431
+ sessionId: string;
1432
+ curve: WalletCurve;
1433
+ /** The caller's own relay routing role — `member:<membershipId>` (registered byte-for-byte). */
1434
+ role: string;
1435
+ /**
1436
+ * `true` once the quorum is fixed AND the caller is one of its `t` signing parties — only then
1437
+ * are the quorum fields below present and the device can run its half.
1438
+ */
1439
+ ready: boolean;
1440
+ /** The fixed quorum's relay roles in signing order (this ceremony's `PartyRouting`). Present only when {@link ready}. */
1441
+ quorumRoles?: string[];
1442
+ /** The caller's 0-based signer position within {@link quorumRoles}/{@link participants}. Present only when {@link ready}. */
1443
+ signerPosition?: number;
1444
+ /** Keygen indices of the fixed quorum, in signing order. Present only when {@link ready}. */
1445
+ participants?: number[];
1446
+ /** The 32-byte hex digest to sign. Present only when {@link ready}. */
1447
+ digest?: string;
1448
+ /**
1449
+ * Short-lived relay token (JWT) bound to `{sessionId, role, sub=membershipId}`. Present only
1450
+ * when relay authentication is enabled.
1451
+ */
1452
+ relayToken?: string;
1453
+ }
1454
+ /** Options for the optional client-side broadcast helper ({@link Waaskey.broadcast}). */
1455
+ interface BroadcastOptions {
1456
+ /** JSON-RPC endpoint of **your** node/provider to submit the signed tx to. */
1457
+ rpcUrl: string;
1458
+ /**
1459
+ * Chain id (e.g. `evm:1`) — reserved for future per-chain routing. The helper
1460
+ * assumes an EVM raw tx (`eth_sendRawTransaction`) today.
1461
+ */
1462
+ chainId?: string;
1463
+ /** Custom fetch implementation (non-browser runtimes / tests). Defaults to global `fetch`. */
1464
+ fetch?: typeof fetch;
1465
+ /** Cancel the broadcast request. */
1466
+ signal?: AbortSignal;
1467
+ }
1468
+ /** Result of a client-side broadcast — the hash the node returned for the submitted tx. */
1469
+ interface BroadcastResult {
1470
+ /** Transaction hash the node returned when it accepted the raw tx. */
1471
+ txHash: string;
1472
+ }
1473
+ /** A page of results (backend `Pagination<T>`). */
1474
+ interface Page<T> {
1475
+ items: T[];
1476
+ /** Total matching records across all pages. */
1477
+ total: number;
1478
+ /** 1-based page number returned. */
1479
+ page: number;
1480
+ /** Page size applied. */
1481
+ limit: number;
1482
+ }
1483
+ /** Query for a paginated list. */
1484
+ interface PageQuery {
1485
+ page?: number;
1486
+ limit?: number;
1487
+ }
1488
+ /**
1489
+ * Query for a wallet's signing requests — {@link PageQuery} plus an optional status filter
1490
+ * (e.g. `pending_approval` to build an approver queue, #329). Backed by the API's
1491
+ * `status` query param on `GET /v1/wallets/{id}/sign-requests` (#332), which paginates the
1492
+ * filtered set correctly (unlike a client-side filter over an unfiltered page).
1493
+ */
1494
+ interface SignRequestsQuery extends PageQuery {
1495
+ status?: SigningRequestStatus;
1496
+ }
1497
+ /** Parameters for an on-ramp widget URL (backend `GetWidgetUrlRequest`). */
1498
+ interface OnrampWidgetParams {
1499
+ /** Wallet address the purchased crypto is delivered to. */
1500
+ walletAddress: string;
1501
+ /** Crypto to buy, e.g. `ETH`. */
1502
+ cryptoCurrency: string;
1503
+ /** Chain id, e.g. `evm:1`. */
1504
+ chainId: string;
1505
+ /** Fiat the user pays with (defaults to USD server-side). */
1506
+ fiatCurrency?: string;
1507
+ /** Pre-fill the fiat amount. */
1508
+ fiatAmount?: number;
1509
+ }
1510
+ /** A provider on-ramp widget URL (backend `OnrampWidgetUrl`). */
1511
+ interface OnrampWidgetUrl {
1512
+ /** Open this URL to launch the on-ramp widget. */
1513
+ url: string;
1514
+ /** Provider that generated the URL, e.g. `transak`. */
1515
+ provider: string;
1516
+ /** ISO timestamp when the signed URL expires, if applicable. */
1517
+ expiresAt?: string;
1518
+ }
1519
+ /** The recovery factors a wallet enrols (backend `RecoveryFactor`). */
1520
+ type RecoveryFactor = 'recovery_code' | 'totp' | 'email_otp';
1521
+ /**
1522
+ * Enrolment of one factor at register time (backend `FactorEnrollment`).
1523
+ *
1524
+ * Contract A: the recovery code is a client-side **sealing secret** and must never
1525
+ * reach the server, so the `recovery_code` factor enrols `credentialHash` (the
1526
+ * lowercase-hex SHA-256 of the code), never the code itself. `totp` / `email_otp`
1527
+ * enrol their non-sealing `credential` (base32 secret / email) as before.
1528
+ */
1529
+ interface FactorEnrollment {
1530
+ type: RecoveryFactor;
1531
+ /** totp → base32 secret; email_otp → email address. Omitted for `recovery_code`. */
1532
+ credential?: string;
1533
+ /** recovery_code → lowercase-hex SHA-256 of the recovery code (the raw code never leaves the client). */
1534
+ credentialHash?: string;
1535
+ }
1536
+ /**
1537
+ * One factor's proof at recovery time (backend `FactorVerification`).
1538
+ *
1539
+ * Contract A: `recovery_code` proves possession with `credentialHash` (the same
1540
+ * SHA-256 the server stored), never the plaintext code — the server compares the
1541
+ * hash in constant time and can never derive the code to unseal the ciphertext.
1542
+ */
1543
+ interface FactorVerification {
1544
+ type: RecoveryFactor;
1545
+ /** totp → current 6-digit OTP; email_otp → the emailed OTP. Omitted for `recovery_code`. */
1546
+ token?: string;
1547
+ /** recovery_code → lowercase-hex SHA-256 of the code (matches the enrolled hash). */
1548
+ credentialHash?: string;
1549
+ }
1550
+ /** A registered recovery record's metadata (backend `RecoveryShareResponse`). */
1551
+ interface RecoveryShareInfo {
1552
+ id: string;
1553
+ walletId: string;
1554
+ factors: RecoveryFactor[];
1555
+ createdAt: string;
1556
+ }
1557
+ /** Response to initiating a recovery session (backend `RecoveryChallengeResponse`). */
1558
+ interface RecoveryChallengeResponse {
1559
+ challengeId: string;
1560
+ requiredFactors: RecoveryFactor[];
1561
+ }
1562
+ /** Encrypted share returned after verifying factors (backend `RecoveryRetrieveResponse`). */
1563
+ interface RecoveryRetrieveResponse {
1564
+ id: string;
1565
+ ciphertext: string;
1566
+ }
1567
+ /** Result of device-loss recovery — factors verified + shares rotated (backend `RecoverWalletResponse`). */
1568
+ interface RecoverWalletResponse {
1569
+ recovered: boolean;
1570
+ id: string;
1571
+ ciphertext: string;
1572
+ refreshedAt: string;
1573
+ }
1574
+ /** Parameters for `recovery.register(...)`. */
1575
+ interface RegisterRecoveryParams {
1576
+ /** The device key share to back up (e.g. `await shareStore.get(walletId)`). */
1577
+ share: string;
1578
+ /** High-entropy recovery code used to encrypt the backup. Generated and returned if omitted. */
1579
+ recoveryCode?: string;
1580
+ /** Base32 TOTP secret to enrol the authenticator factor. */
1581
+ totpSecret: string;
1582
+ /** Email address to enrol the email-OTP factor. */
1583
+ email: string;
1584
+ /** Extra factor enrolments beyond the standard three. */
1585
+ extraFactors?: FactorEnrollment[];
1586
+ }
1587
+ /** Parameters for `recovery.recover(...)` / `recovery.retrieveShare(...)`. */
1588
+ interface RecoverParams {
1589
+ /** challengeId from `recovery.challenge(...)`. */
1590
+ challengeId: string;
1591
+ /** One verification per enrolled factor. */
1592
+ verifications: FactorVerification[];
1593
+ /** The recovery code — decrypts the retrieved share client-side. */
1594
+ recoveryCode: string;
1595
+ }
1596
+ /**
1597
+ * Parameters for `wallets.recoverSign(...)` — the device-loss RECOVERY CO-SIGN of a non-custodial
1598
+ * `[device, server, user_backup]` secp256k1 wallet (#351/#78). Extends the recovery gate ({@link RecoverParams})
1599
+ * that releases the sealed `user_backup` ciphertext with the digest to co-sign. The `recoveryCode` opens
1600
+ * the backup CLIENT-SIDE (Contract A: it never reaches the server); the restored `user_backup` share then
1601
+ * co-signs 2-party with the platform's `server` party. Passkey step-up (when the wallet requires it) rides
1602
+ * the call's options, exactly like a normal sign.
1603
+ */
1604
+ interface RecoverSignParams extends RecoverParams {
1605
+ /** 32-byte hex digest to co-sign (a leading `0x` is accepted and stripped). */
1606
+ digest: string;
1607
+ /** Target chain (e.g. `"evm:1"`) — forwarded to the recover-sign session for plan/chain gating, parity with `sign-session`. */
1608
+ chainId?: string;
1609
+ }
1610
+ /**
1611
+ * Assemble material the backend hands a retained USER_DEVICE new holder so it can finish its OWN
1612
+ * reshared share client-side — the platform never learns the device's share (backend
1613
+ * `DeviceReshareMaterial`, #83/#318). Present on {@link ReshareWalletResponse.deviceMaterial} only
1614
+ * when a reshare keeps a device holder. The values are opaque JSON the device routes into the core.
1615
+ */
1616
+ interface DeviceReshareMaterial {
1617
+ curve: WalletCurve;
1618
+ /** The device holder's 0-based position within {@link newPreimages}. */
1619
+ newPosition: number;
1620
+ /** New share preimages (32-byte hex), one per new holder. */
1621
+ newPreimages: string[];
1622
+ /** The new signing threshold `t'`. */
1623
+ newThreshold: number;
1624
+ /** The unchanged `WalletPublicInfo` JSON. */
1625
+ wallet: unknown;
1626
+ /** One Feldman-commitments object per dealer (the broadcast set). */
1627
+ commitments: unknown[];
1628
+ /** The device holder's private sub-share from each dealer. */
1629
+ subShares: unknown[];
1630
+ }
1631
+ /**
1632
+ * Relay coordination for the device to join the post-reshare **aux-completion** ceremony (#318 /
1633
+ * #95) — the interactive `aux_info_gen` over the NEW committee that the backend signer drives for
1634
+ * the server + recovery parties. Mirrors the keygen {@link WalletCeremony} / sign-session relay
1635
+ * fields; the backend uses `<sessionId>/reshare-aux` (kind `reshare-aux`), with `sessionId` the
1636
+ * wallet id. `curve` is taken from {@link DeviceReshareMaterial}, so it is not repeated here.
1637
+ */
1638
+ interface ReshareCompletionCeremony {
1639
+ /** Relay websocket URL the device connects to. */
1640
+ relayUrl: string;
1641
+ /** Ceremony id shared by every committee party (the wallet id). */
1642
+ sessionId: string;
1643
+ /** This device's relay routing id in the NEW committee, e.g. `device`. */
1644
+ role: string;
1645
+ /** The peer (server) party's relay routing id, e.g. `server`. */
1646
+ peerRole: string;
1647
+ /** This device's 0-based party index in the NEW committee. */
1648
+ partyIndex: number;
1649
+ /** The peer (server) party's party index in the NEW committee. */
1650
+ peerPartyIndex: number;
1651
+ /** Total parties `n'` in the NEW committee. */
1652
+ parties: number;
1653
+ /** Short-lived relay token (JWT) the device presents to join the reshare-aux session; present only when relay auth is enabled. */
1654
+ relayToken?: string;
1655
+ }
1656
+ /** Parameters for `reshare.complete(...)` — everything the device needs to finish its NEW-epoch share. */
1657
+ interface ReshareCompletionParams {
1658
+ /** Assemble material from the reshare response ({@link ReshareWalletResponse.deviceMaterial}). */
1659
+ material: DeviceReshareMaterial;
1660
+ /** Relay coordination for the `<sessionId>/reshare-aux` ceremony the device joins. */
1661
+ ceremony: ReshareCompletionCeremony;
1662
+ /** The wallet's NEW key epoch ({@link ReshareWalletResponse.keyEpoch}) — the completed share is stored under it. */
1663
+ keyEpoch: number;
1664
+ /** This device's OWN pre-generated Paillier safe-primes for the aux ceremony (never server-provided). Absent ⇒ generated inline. */
1665
+ pregeneratedPrimes?: string;
1666
+ }
1667
+ /** Result of `reshare.complete(...)` — the wallet is now signable on this device under the new epoch. */
1668
+ interface ReshareCompletionResult {
1669
+ walletId: string;
1670
+ /** The key epoch the completed share was stored under (the wallet's new epoch). */
1671
+ keyEpoch: number;
1672
+ /** The wallet's shared public key (hex) — verified unchanged across the reshare. */
1673
+ sharedPublicKey: string;
1674
+ }
1675
+ /**
1676
+ * Result of a wallet reshare (backend `ReshareWalletResponse`, #295): the wallet's NEW topology +
1677
+ * custody attestation and the bumped `keyEpoch` (the public key/address are UNCHANGED). When the
1678
+ * new committee retains a USER_DEVICE holder, {@link deviceMaterial} carries the material that
1679
+ * device completes client-side (see `reshare.complete`).
1680
+ */
1681
+ interface ReshareWalletResponse {
1682
+ walletId: string;
1683
+ /** The new signing threshold `t'`. */
1684
+ threshold: number;
1685
+ /** The new ordered party roles, length `n'`. */
1686
+ parties: string[];
1687
+ /** The new per-party custody kinds. */
1688
+ custodyKinds: CustodyKind[];
1689
+ /** Count of {@link custodyKinds} entries the platform itself holds. */
1690
+ platformShareCount: number;
1691
+ /** The wallet's new custody attestation. */
1692
+ custodyType: CustodyType;
1693
+ /** The wallet's new key epoch (bumped). */
1694
+ keyEpoch: number;
1695
+ /** True: the new committee cannot sign until its aux material is (re)generated (device completion clears it). */
1696
+ auxPending: boolean;
1697
+ /** ISO timestamp the reshare completed at. */
1698
+ resharedAt: string;
1699
+ /** Assemble material for a retained USER_DEVICE new holder. Present only when the new committee keeps a device holder. */
1700
+ deviceMaterial?: DeviceReshareMaterial;
1701
+ }
1702
+ /** An embedded-wallet end-user (authenticates through the app, distinct from a dashboard member). */
1703
+ interface EndUser {
1704
+ id: string;
1705
+ tenantId: string;
1706
+ /** Email identifier (email-OTP / social). Absent when signed up by phone or passkey. */
1707
+ email?: string;
1708
+ /** Phone identifier (phone-OTP, E.164). Absent unless a verified phone is linked. */
1709
+ phone?: string;
1710
+ createdAt: string;
1711
+ }
1712
+ /** Result of starting an OTP login (email or phone). */
1713
+ interface EmailStartResult {
1714
+ sent: boolean;
1715
+ /** TEST-ONLY: the code, present only when the backend runs with `EMBEDDED_OTP_DEBUG` (non-prod). */
1716
+ debugCode?: string;
1717
+ }
1718
+ /** An authenticated end-user session. */
1719
+ interface EmbeddedSession {
1720
+ /** End-user session token (sent on subsequent end-user-scoped calls). */
1721
+ token: string;
1722
+ endUser: EndUser;
1723
+ }
1724
+ /** Body of a Firebase Auth login — the client-obtained Firebase ID token (backend `FirebaseAuthDto`). */
1725
+ interface FirebaseAuthRequest {
1726
+ /** Firebase ID token obtained on the client via the Firebase Auth SDK. */
1727
+ idToken: string;
1728
+ }
1729
+ /** A dashboard member's role within a tenant (backend `MemberRole`). */
1730
+ type MemberRole = 'owner' | 'admin' | 'member';
1731
+ /** Which environments a member may operate in (backend `MembershipScope`). */
1732
+ type MembershipScope = 'all' | 'production' | 'sandbox';
1733
+ /**
1734
+ * A dashboard org member — a human who logs into the tenant console (distinct from an embedded
1735
+ * {@link EndUser}, and from an API key). Mirrors the backend `IMember`; `createdAt` is an ISO string
1736
+ * on the wire.
1737
+ */
1738
+ interface Member {
1739
+ id: string;
1740
+ tenantId: string;
1741
+ email: string;
1742
+ role: MemberRole;
1743
+ /** Which environments this member may operate in (production / sandbox / all). */
1744
+ environmentScope: MembershipScope;
1745
+ /** Whether the member has completed TOTP 2FA enrolment. */
1746
+ totpEnabled: boolean;
1747
+ createdAt: string;
1748
+ }
1749
+ /**
1750
+ * A held org-member bearer session (mirrors the backend `FirebaseAuthResponse`). Returned by
1751
+ * {@link Members.loginWithFirebase} and held by the client so member-scoped calls authenticate with
1752
+ * `Authorization: Bearer <accessToken>` instead of the ambient dashboard cookie.
1753
+ */
1754
+ interface MemberSession {
1755
+ /** Member access token (JWT) — presented as `Authorization: Bearer …` on member calls. */
1756
+ accessToken: string;
1757
+ /**
1758
+ * Member refresh token (JWT). The SDK holds it but does NOT yet auto-refresh (out of scope) — once
1759
+ * the access token expires a member call surfaces a 401 error; a future revision exchanges it at
1760
+ * `/v1/auth/refresh`.
1761
+ */
1762
+ refreshToken: string;
1763
+ /** The authenticated member. */
1764
+ member: Member;
40
1765
  }
41
1766
 
42
- /** Thin typed HTTP client over fetch — the single place requests are issued. */
43
- declare class HttpClient {
44
- private readonly apiKey;
45
- private readonly baseUrl;
46
- private readonly fetchImpl;
47
- constructor(apiKey: string, baseUrl: string, fetchImpl?: typeof fetch);
48
- request<T>(method: string, path: string, body?: unknown): Promise<T>;
1767
+ /**
1768
+ * Embedded end-user authentication (#6) — Privy-style login that maps an end-user to a
1769
+ * non-custodial wallet, distinct from the dashboard member auth. The developer's API key
1770
+ * authorizes the flow; a successful login yields an end-user **session token** the SDK
1771
+ * holds and sends on end-user-scoped calls.
1772
+ *
1773
+ * Methods (email-OTP, phone-OTP, ) live behind the same `auth` surface and all establish
1774
+ * the same session; social / passkey are added the same way.
1775
+ */
1776
+ declare class Auth {
1777
+ private readonly http;
1778
+ private current?;
1779
+ constructor(http: HttpClient);
1780
+ /** Email one-time-code login. */
1781
+ readonly email: {
1782
+ /** Send a login code to `email`. */
1783
+ start: (email: string, signal?: AbortSignal) => Promise<EmailStartResult>;
1784
+ /** Verify the code; on success the end-user is provisioned and the session is established. */
1785
+ verify: (email: string, code: string, signal?: AbortSignal) => Promise<EmbeddedSession>;
1786
+ };
1787
+ /** Phone one-time-code (SMS) login. `phone` is E.164, e.g. `+14155550123`. */
1788
+ readonly phone: {
1789
+ /** Send a login code to `phone`. */
1790
+ start: (phone: string, signal?: AbortSignal) => Promise<EmailStartResult>;
1791
+ /** Verify the code; on success the end-user is provisioned and the session is established. */
1792
+ verify: (phone: string, code: string, signal?: AbortSignal) => Promise<EmbeddedSession>;
1793
+ };
1794
+ /** Social / federated-provider login. */
1795
+ readonly social: {
1796
+ /**
1797
+ * Exchange a Google ID token (obtained on the client via Google Identity Services) for a
1798
+ * session. Requires the tenant's Google client id to be configured server-side.
1799
+ */
1800
+ google: (idToken: string, signal?: AbortSignal) => Promise<EmbeddedSession>;
1801
+ /**
1802
+ * Exchange a Firebase ID token (obtained on the client via the Firebase Auth SDK) for a session
1803
+ * — the Firebase analogue of {@link social.google}. Requires the tenant to have opted into
1804
+ * embedded Firebase sign-in (`firebaseAuthEnabled`); when it hasn't, the API replies 412 and the
1805
+ * SDK surfaces a typed `firebase_not_enabled` {@link WaaskeyError}.
1806
+ */
1807
+ firebase: (idToken: string, signal?: AbortSignal) => Promise<EmbeddedSession>;
1808
+ };
1809
+ /**
1810
+ * Passkey (WebAuthn) login. `register` adds a passkey to the **currently logged-in** end-user;
1811
+ * `login` is usernameless (a passkey assertion resolves the user and establishes a session).
1812
+ *
1813
+ * The browser ceremony uses `@simplewebauthn/browser` (an optional peer dependency, loaded on
1814
+ * demand). Pass a `ceremony` to override it — e.g. on React Native with a native authenticator.
1815
+ */
1816
+ readonly passkey: {
1817
+ /** Add a passkey to the logged-in end-user. Requires an active session. */
1818
+ register: (ceremony?: PasskeyCeremony) => Promise<void>;
1819
+ /** Usernameless passkey login — establishes the session on success. */
1820
+ login: (ceremony?: PasskeyCeremony) => Promise<EmbeddedSession>;
1821
+ };
1822
+ /** The active session, or `undefined` when not logged in. */
1823
+ get session(): EmbeddedSession | undefined;
1824
+ /** The end-user session token, or `undefined` when not logged in. */
1825
+ get token(): string | undefined;
1826
+ /** Whether an end-user is currently logged in. */
1827
+ get isAuthenticated(): boolean;
1828
+ /** The logged-in end-user (re-fetched from the session token). Rejects if not logged in. */
1829
+ me(signal?: AbortSignal): Promise<EndUser>;
1830
+ /** Restore a session from a previously stored token (e.g. across reloads). */
1831
+ restore(session: EmbeddedSession): void;
1832
+ /** Clear the session (sign out). */
1833
+ logout(): void;
1834
+ /** POST a verify request, store the resulting session, and return it. */
1835
+ private establish;
1836
+ private requireToken;
1837
+ }
1838
+ /**
1839
+ * The browser WebAuthn ceremony. Defaults to `@simplewebauthn/browser`; override for non-DOM
1840
+ * runtimes (e.g. a React Native native-passkey module). `create` runs registration
1841
+ * (navigator.credentials.create), `get` runs authentication (navigator.credentials.get).
1842
+ */
1843
+ interface PasskeyCeremony {
1844
+ create(options: unknown): Promise<unknown>;
1845
+ get(options: unknown): Promise<unknown>;
49
1846
  }
50
1847
 
51
1848
  /**
52
- * A handle to a single wallet. Returned by `waaskey.wallets.create(...)`.
1849
+ * Org-member (dashboard "plane B") authentication — a bearer member login for a NON-browser
1850
+ * consumer (a Node app, or the browser extension via the SDK) that cannot carry the ambient
1851
+ * same-site dashboard session cookie the browser console relies on.
1852
+ *
1853
+ * {@link loginWithFirebase} exchanges a Firebase ID token at the `@Public` `POST /v1/auth/firebase`
1854
+ * (no API key, no cookie) for a member session `{ accessToken, refreshToken, member }`, which the
1855
+ * client HOLDS. Once held, member-scoped calls ({@link Wallets.joinCeremony} /
1856
+ * {@link Wallets.joinSignCeremony}, which go through {@link HttpClient.requestAsMember}) send
1857
+ * `Authorization: Bearer <accessToken>` rather than relying on the cookie — so a headless consumer
1858
+ * can drive the member-bound ceremonies.
53
1859
  *
54
- * Signing is an interactive threshold-MPC protocol; from the caller's point of
55
- * view it is a single awaited call that returns the signature.
1860
+ * **Refresh is out of scope for now:** the SDK holds `refreshToken` but does not yet auto-refresh.
1861
+ * Once the access token expires a member call surfaces a 401 (`unauthorized`) error; a future
1862
+ * revision will exchange the refresh token at `/v1/auth/refresh` from here.
56
1863
  */
57
- declare class Wallet implements WalletData {
1864
+ declare class Members {
58
1865
  private readonly http;
1866
+ private held?;
1867
+ constructor(http: HttpClient);
1868
+ /**
1869
+ * Exchange a Firebase ID token for an org-member session and hold it.
1870
+ *
1871
+ * The token must belong to an ALREADY-INVITED member — Firebase never self-provisions a member, so
1872
+ * an unknown email is rejected (`no_member`). A member with 2FA enrolled must present a Firebase
1873
+ * token that itself passed a second factor, or the login is refused (`mfa_required`).
1874
+ *
1875
+ * @throws {WaaskeyError} `no_member` — no org member exists for the token's Firebase identity (401).
1876
+ * @throws {WaaskeyError} `mfa_required` — the member needs a second factor the token didn't carry (401).
1877
+ */
1878
+ loginWithFirebase(idToken: string, signal?: AbortSignal): Promise<MemberSession>;
1879
+ /** The current member, or `undefined` when not logged in. */
1880
+ get member(): Member | undefined;
1881
+ /** The held member session (access + refresh tokens + member), or `undefined` when not logged in. */
1882
+ get session(): MemberSession | undefined;
1883
+ /** The held member access token — the value the client wires into {@link HttpClient.requestAsMember}. */
1884
+ get accessToken(): string | undefined;
1885
+ /** Whether an org member is currently logged in over a held bearer session. */
1886
+ get isAuthenticated(): boolean;
1887
+ /** Restore a previously stored member session (e.g. across process restarts). */
1888
+ restore(session: MemberSession): void;
1889
+ /** Clear the held member session (sign out); member calls fall back to the cookie path afterwards. */
1890
+ memberSignOut(): void;
1891
+ }
1892
+
1893
+ /**
1894
+ * The `onramp` resource — fund an embedded wallet with fiat (card/bank) via a provider
1895
+ * (e.g. Transak). The SDK returns a provider widget URL to open; the purchase settles on
1896
+ * chain to the wallet address and the on-chain balance reflects it once the provider delivers.
1897
+ */
1898
+ declare class Onramp {
1899
+ private readonly http;
1900
+ constructor(http: HttpClient);
1901
+ /** Get a provider widget URL to buy `cryptoCurrency` on `chainId` for `walletAddress`. */
1902
+ widgetUrl(params: OnrampWidgetParams, signal?: AbortSignal): Promise<OnrampWidgetUrl>;
1903
+ }
1904
+
1905
+ /** Dependencies the {@link Recovery} resource needs. */
1906
+ interface RecoveryDeps {
1907
+ shareStore?: ShareStore;
1908
+ /** Optional analytics emitter for the wallet.recovered event. */
1909
+ analytics?: Analytics;
1910
+ }
1911
+ /**
1912
+ * Multi-factor wallet recovery.
1913
+ *
1914
+ * The recovery share is encrypted **client-side** with the user's recovery code
1915
+ * (the server stores only the opaque ciphertext) and its release is gated behind
1916
+ * ≥3 factors (recovery code + TOTP + email OTP). Device-loss recovery verifies the
1917
+ * factors, has the server rotate the key shares (invalidating the lost device
1918
+ * share), then decrypts the backup and restores it on the new device.
1919
+ */
1920
+ declare class Recovery {
1921
+ private readonly http;
1922
+ private readonly deps;
1923
+ constructor(http: HttpClient, deps?: RecoveryDeps);
1924
+ /**
1925
+ * Back up a wallet's device share: encrypt it with the recovery code and enrol the
1926
+ * factors. Returns the (possibly generated) recovery code — show it to the user
1927
+ * once; it is the only key to the backup and is never recoverable from the server.
1928
+ */
1929
+ register(walletId: string, params: RegisterRecoveryParams, options?: {
1930
+ signal?: AbortSignal;
1931
+ }): Promise<{
1932
+ recoveryCode: string;
1933
+ share: RecoveryShareInfo;
1934
+ }>;
1935
+ /** The registered recovery factors for a wallet (no secrets). */
1936
+ getInfo(walletId: string, options?: {
1937
+ signal?: AbortSignal;
1938
+ }): Promise<RecoveryShareInfo>;
1939
+ /** Start a recovery session — returns the challengeId + the factors the user must verify. */
1940
+ challenge(walletId: string, options?: {
1941
+ signal?: AbortSignal;
1942
+ }): Promise<RecoveryChallengeResponse>;
1943
+ /**
1944
+ * Device-loss recovery: verify factors, have the server rotate the key shares,
1945
+ * then decrypt the backup with the recovery code and restore it to the share store
1946
+ * (when one is configured). Returns when the share is restored.
1947
+ */
1948
+ recover(walletId: string, params: RecoverParams, options?: {
1949
+ signal?: AbortSignal;
1950
+ }): Promise<{
1951
+ share: string;
1952
+ refreshedAt: string;
1953
+ }>;
1954
+ /**
1955
+ * Verify factors and decrypt the backed-up share **without** rotating keys — for a
1956
+ * read-only restore. Use {@link recover} for true device-loss (which re-keys).
1957
+ *
1958
+ * NOTE (issue #41, LOW): because this path does not rotate the (possibly lost) device
1959
+ * share, the server `/verify` endpoint must enforce that **all ≥3 factors** were
1960
+ * satisfied before releasing the ciphertext; prefer {@link recover} (always-rotate) for
1961
+ * device-loss so a leaked backup can't be replayed against a still-valid old share.
1962
+ */
1963
+ retrieveShare(walletId: string, params: RecoverParams, options?: {
1964
+ signal?: AbortSignal;
1965
+ }): Promise<string>;
1966
+ private decrypt;
1967
+ }
1968
+ /** A high-entropy (128-bit) recovery code, grouped for readability — e.g. `7F3A-9C21-...`. */
1969
+ declare function generateRecoveryCode(): string;
1970
+
1971
+ /** Dependencies the {@link Reshare} resource needs for the device-side completion ceremony. */
1972
+ interface ReshareDeps {
1973
+ mpc?: MpcCore;
1974
+ shareStore?: ShareStore;
1975
+ /** Optional analytics emitter for the wallet.reshared event. */
1976
+ analytics?: Analytics;
1977
+ }
1978
+ /**
1979
+ * Device-side completion of a **device-retaining** reshare (#318 phase 2b).
1980
+ *
1981
+ * A backend reshare that keeps a USER_DEVICE holder commits a NEW epoch whose shares are CORE-ONLY
1982
+ * (no aux) — the wallet is `reshareAuxPending` and cannot sign until the aux material is generated
1983
+ * over the new committee. The platform signer drives its server + recovery parties of that aux
1984
+ * ceremony; the DEVICE must join it or the ceremony can't complete. This resource is the device's
1985
+ * half: it assembles its NEW-epoch bare core locally from the reshare's {@link DeviceReshareMaterial},
1986
+ * runs the `<sessionId>/reshare-aux` aux ceremony over the relay alongside the platform parties, and
1987
+ * seals the resulting COMPLETE share on the device under the new epoch.
1988
+ *
1989
+ * The device is never left worse off: nothing is stored until the completed share's public key is
1990
+ * verified equal to the wallet's (unchanged across a reshare — the funds-safety invariant), and the
1991
+ * OLD-epoch share is kept intact, so an interrupted completion can simply be retried.
1992
+ */
1993
+ declare class Reshare {
1994
+ private readonly http;
1995
+ private readonly deps;
1996
+ constructor(http: HttpClient, deps?: ReshareDeps);
1997
+ /**
1998
+ * Complete this device's share for a device-retaining reshare and make the wallet signable on the
1999
+ * device under the new epoch.
2000
+ *
2001
+ * 1. Fetch the wallet to learn its (unchanged) public key — the invariant the completion is checked
2002
+ * against, taken from the server's truth rather than the caller.
2003
+ * 2. Assemble the NEW-epoch bare core locally from {@link ReshareCompletionParams.material} (no relay).
2004
+ * 3. Run the aux-completion ceremony over the relay ({@link ReshareCompletionParams.ceremony}),
2005
+ * alongside the platform server + recovery parties, to obtain the COMPLETE signable share.
2006
+ * 4. Verify the completed share's public key equals the wallet's (fail closed on any mismatch).
2007
+ * 5. Seal + persist the completed share under `(walletId, keyEpoch)`, keeping the old-epoch share.
2008
+ *
2009
+ * Requires `mpc` (built with the reshare capability) + `shareStore`. Idempotent/recoverable: safe to
2010
+ * retry, since nothing is overwritten until the new share is assembled, verified, and stored.
2011
+ */
2012
+ complete(walletId: string, params: ReshareCompletionParams, options?: {
2013
+ signal?: AbortSignal;
2014
+ }): Promise<ReshareCompletionResult>;
2015
+ }
2016
+ /**
2017
+ * Compose the epoch-keyed device-share storage key `(walletId, keyEpoch)`, mirroring the backend's
2018
+ * epoch-keyed share model (#295). Epoch 1 (a fresh keygen) keeps the bare `walletId` key for
2019
+ * back-compat with existing stored shares; later epochs (reshares) use a distinct suffixed key, so a
2020
+ * completed reshare never overwrites the old-epoch share until cutover is confirmed.
2021
+ */
2022
+ declare function epochShareKey(walletId: string, keyEpoch: number): string;
2023
+
2024
+ /** Device-party dependencies a {@link Wallet} needs to co-sign an ed25519 (FROST) transaction locally. */
2025
+ interface WalletDeviceDeps {
2026
+ mpc?: MpcCore;
2027
+ shareStore?: ShareStore;
2028
+ }
2029
+ /**
2030
+ * A handle to a single wallet. Returned by `waaskey.wallets.create(...)` /
2031
+ * `waaskey.wallets.get(...)`.
2032
+ */
2033
+ declare class Wallet {
2034
+ private readonly http;
2035
+ private readonly analytics?;
2036
+ /** Device core + share store, threaded from {@link Wallets}, so an ed25519 wallet can co-sign locally. */
2037
+ private readonly device;
2038
+ /** Waaskey wallet id, e.g. `wlt_…`. */
59
2039
  readonly id: string;
60
- readonly address: string;
61
- readonly chain: Chain;
62
- constructor(http: HttpClient, data: WalletData);
63
- /** Sign an arbitrary message with this wallet's key (2-of-3 threshold MPC). */
64
- signMessage(message: string): Promise<string>;
2040
+ /** The full wallet record as returned by the API. */
2041
+ readonly data: WalletData;
2042
+ constructor(http: HttpClient, data: WalletData, analytics?: Analytics | undefined,
2043
+ /** Device core + share store, threaded from {@link Wallets}, so an ed25519 wallet can co-sign locally. */
2044
+ device?: WalletDeviceDeps);
2045
+ /** On-chain address (set once keygen completes). */
2046
+ get address(): string | undefined;
2047
+ /** Lifecycle state (`pending` until keygen completes, then `active`). */
2048
+ get status(): WalletStatus;
2049
+ /** Signing curve of the wallet. */
2050
+ get curve(): WalletCurve;
2051
+ /** Signing threshold `t` of the wallet's `t`-of-`n` MPC key. */
2052
+ get threshold(): number;
2053
+ /** Ordered party roles of the wallet's keygen topology, length `n`. */
2054
+ get parties(): string[];
2055
+ /** Per-party custody kind, parallel to {@link parties}. */
2056
+ get custodyKinds(): CustodyKind[];
2057
+ /** How many of the wallet's shares the platform itself holds. */
2058
+ get platformShareCount(): number;
2059
+ /**
2060
+ * The wallet's custody attestation (`embedded` / `shared` / `self_custody`) — display
2061
+ * it to prove the custody guarantee to the end-user (see {@link isNonCustodial}).
2062
+ */
2063
+ get custodyType(): CustodyType;
2064
+ /**
2065
+ * Whether this wallet is **non-custodial** — the platform's shares alone do not meet
2066
+ * the threshold (`platformShareCount < threshold`, i.e. custody type `shared` or
2067
+ * `self_custody`), so WaaS cannot sign without the user/external party.
2068
+ */
2069
+ get isNonCustodial(): boolean;
2070
+ /**
2071
+ * Sign a 32-byte message digest with this wallet's key (2-of-3 threshold MPC).
2072
+ *
2073
+ * `digest` is a 32-byte hash as hex (the `0x` prefix is optional) — e.g. the
2074
+ * keccak-256 of an EVM transaction. Hashing a higher-level message/transaction
2075
+ * into a digest is the caller's (or a chain helper's) responsibility.
2076
+ *
2077
+ * **Passkey step-up (Pattern B / issue #21, #39):** pass `{ requirePasskey: true }` to run a
2078
+ * WebAuthn assertion before signing. The SDK fetches a **server-issued one-time challenge**,
2079
+ * prompts the user's authenticator over it, and attaches `passkeyAssertion` + its
2080
+ * `passkeyChallengeId` to the POST body; the backend verifies both the MPC signature and the
2081
+ * assertion, then burns the challenge (so it cannot be replayed). Alternatively supply a
2082
+ * pre-built assertion via `options.passkeyAssertion` (with its `options.passkeyChallengeId`).
2083
+ */
2084
+ sign(digest: string, options?: SignOptions): Promise<string>;
2085
+ /**
2086
+ * Send a transaction from this wallet. The platform builds the chain-specific transaction
2087
+ * and co-signs it with the 2-of-3 MPC quorum, returning the **signed raw transaction**.
2088
+ * `value` is in the chain's base unit (wei) as a numeric string.
2089
+ *
2090
+ * **WaaS is sign-only — it never broadcasts.** The result's {@link SendResult.signedTx} is
2091
+ * the signed raw tx the *client* submits to its own node/provider (see
2092
+ * {@link Waaskey.broadcast} or your own submitter); {@link SendResult.txHash} is a
2093
+ * deterministic offline id for reference, not proof of broadcast.
2094
+ *
2095
+ * **Passkey step-up (Pattern B / issue #21, #39):** pass `{ requirePasskey: true }` to run a
2096
+ * WebAuthn assertion before sending, over a server-issued one-time challenge (verified + burned
2097
+ * server-side). Alternatively supply a pre-built assertion via `options.passkeyAssertion`.
2098
+ */
2099
+ send(params: SendParams, options?: SendOptions): Promise<SendResult>;
2100
+ /**
2101
+ * Device-co-signed send for an ed25519 (FROST) wallet (#110) — the browser holds the device FROST
2102
+ * share and co-signs 2-party with the backend `server` party over the relay:
2103
+ *
2104
+ * 1. **START** (`POST …/send-session`): the backend builds the unsigned tx (chain adapter), starts
2105
+ * its server FROST party on the relay in the background, and returns the raw `message` bytes to
2106
+ * sign + the relay coordination ({@link EddsaSendSession}).
2107
+ * 2. **CO-SIGN**: the device runs `signEddsa` over the relay with its stored `{keyPackage,
2108
+ * publicKeyPackage}` share; the two parties aggregate the RFC 8032 signature (returned locally).
2109
+ * 3. **ASSEMBLE** (`POST …/send-session/:txId/assemble`): the backend embeds the aggregated
2110
+ * signature into the wire tx (chain adapter) and returns the signed raw tx + offline `txHash`.
2111
+ *
2112
+ * WaaS stays sign-only: the returned {@link SendResult.signedTx} is what the client broadcasts.
2113
+ */
2114
+ private sendEd25519;
2115
+ /**
2116
+ * Attach a passkey step-up assertion to `body` when the caller requests one (issue #39).
2117
+ *
2118
+ * The challenge is a **server-issued one-time nonce**, never derived from the request
2119
+ * payload: the SDK fetches it from the step-up challenge endpoint, runs the WebAuthn
2120
+ * assertion over it, and echoes its `challengeId` so the server can verify and burn it —
2121
+ * making a captured assertion non-replayable. A pre-built `passkeyAssertion` (with its
2122
+ * `passkeyChallengeId`) is attached as-is and takes precedence over `requirePasskey`.
2123
+ */
2124
+ private attachStepUp;
2125
+ /**
2126
+ * List this wallet's **signing activity**, newest first (paginated) — the unified audit
2127
+ * trail of what this key signed (raw signs and send/sweep signed txs, #289). A send/sweep
2128
+ * record carries the `signedTx` the client broadcasts.
2129
+ */
2130
+ signatures(query?: PageQuery, options?: SendOptions): Promise<Page<Signature>>;
65
2131
  }
66
2132
 
67
- /** The `wallets` resource: create and load wallets. */
2133
+ /** Dependencies a {@link Wallets} resource needs for the non-custodial ceremony. */
2134
+ interface WalletsDeps {
2135
+ mpc?: MpcCore;
2136
+ shareStore?: ShareStore;
2137
+ /** Optional pool of pre-generated primes; when present, keygen claims one instead of generating inline. */
2138
+ primePool?: PrimePool;
2139
+ /** Optional analytics emitter for wallet lifecycle events. */
2140
+ analytics?: Analytics;
2141
+ }
2142
+ /**
2143
+ * The `wallets` resource: create and load wallets, plus the approver actions
2144
+ * ({@link Wallets.approveSignRequest}/{@link Wallets.declineSignRequest}) and the approver
2145
+ * queue listing ({@link Wallets.listSignRequests}/{@link Wallets.listPendingApprovals}) on
2146
+ * their async signing requests (#229, #309, #329).
2147
+ */
68
2148
  declare class Wallets {
69
2149
  private readonly http;
70
- constructor(http: HttpClient);
71
- /** Create a new MPC wallet on the given chain. */
72
- create(params: CreateWalletParams): Promise<Wallet>;
2150
+ private readonly deps;
2151
+ constructor(http: HttpClient, deps?: WalletsDeps);
2152
+ /**
2153
+ * Create a new MPC wallet on the given chain. The keygen is a `t`-of-`n` ceremony:
2154
+ *
2155
+ * 1. ask the API to start a wallet + the server party (returns the ceremony params),
2156
+ * 2. run the **device** half of keygen locally (in WASM) against the relay,
2157
+ * 3. seal + persist the device share (it never leaves the device),
2158
+ * 4. wait until the wallet is ACTIVE (server party finished) and return it.
2159
+ *
2160
+ * By default (no custody policy) this is the embedded **2-of-3** topology. Supply an
2161
+ * explicit `{ threshold, parties, custodyKinds }` and/or a requested `custodyType` to
2162
+ * change the topology / custody posture — the SDK validates it client-side (see
2163
+ * {@link validateCustodyPolicy}) and the backend enforces the attested invariant. The
2164
+ * device runs whatever `t`-of-`n` the returned ceremony describes (its `parties` /
2165
+ * `threshold`), so the ceremony is not pinned to 2-of-3.
2166
+ *
2167
+ * **Non-custodial `[device, server, user_backup]` (#351/#78).** When the returned ceremony carries a
2168
+ * `user_backup` party ({@link WalletCeremony.additionalParties}), the platform signer drives ONLY the
2169
+ * `server` share, so this ONE device runs BOTH client parties in the SAME keygen ceremony — the
2170
+ * `device` party AND the client-held `user_backup` party — joining the same relay session with each
2171
+ * party's own relay token. It then persists the `device` share locally (as always) and, because there
2172
+ * is no platform recovery share, seals the `user_backup` share with the caller's recovery code and
2173
+ * registers the ciphertext server-side (reusing the recovery mechanism), so device-loss can never lock
2174
+ * funds. This path therefore REQUIRES `options.backup`. The sealed backup is persisted to a local
2175
+ * pending slot BEFORE the network call, so if registration fails the (non-re-derivable) share is not
2176
+ * lost — `create` throws `backup_failed` and {@link retryBackup} re-registers it (no re-keygen).
2177
+ *
2178
+ * Requires `mpc` + `shareStore` on the client. The device share is the user's half
2179
+ * of the key; without storing it the wallet would be unrecoverable.
2180
+ */
2181
+ create(params: CreateWalletParams, options?: CreateWalletOptions): Promise<Wallet>;
2182
+ /**
2183
+ * Create a **member-bound** wallet (#342-#349): an N+1 threshold topology where `N` specific ORG
2184
+ * MEMBERSHIPS (`shareholderMembershipIds`) each hold one share and the platform holds exactly
2185
+ * one (a derived `shared` custody topology — never `embedded`). Unlike {@link create} (the
2186
+ * embedded device+server(+recovery) flow, which runs the device's keygen inline and waits for
2187
+ * ACTIVE), this wallet is provisioned `pending_keygen` with **no ceremony to join here** — it
2188
+ * has no `mpc`/`shareStore` dependency and no custody-policy validation (that machinery is for
2189
+ * the raw `parties`/`custodyKinds` embedded topology, mutually exclusive with
2190
+ * `shareholderMembershipIds` server-side). Each invited member later runs {@link joinCeremony}
2191
+ * from their OWN device/session; the wallet only activates once every member has joined.
2192
+ */
2193
+ createWallet(params: CreateMemberWalletParams, options?: {
2194
+ signal?: AbortSignal;
2195
+ }): Promise<WalletData>;
2196
+ /**
2197
+ * Warm the prime pool for a chain's curve OFF the hot path — call during onboarding/idle (ideally
2198
+ * from a Web Worker) so the next {@link create} on that chain doesn't pay the safe-prime cost.
2199
+ * No-op when no prime pool is configured.
2200
+ */
2201
+ prewarm(chain: CreateWalletParams['chain']): Promise<void>;
2202
+ /**
2203
+ * List the tenant's wallets, newest first (paginated). Returns plain {@link WalletData}
2204
+ * rows — pass an `id` to {@link get} to obtain a signing-capable {@link Wallet}.
2205
+ */
2206
+ list(query?: PageQuery, options?: {
2207
+ signal?: AbortSignal;
2208
+ }): Promise<Page<WalletData>>;
73
2209
  /** Load an existing wallet by id. */
74
- get(id: string): Promise<Wallet>;
2210
+ get(id: string, options?: {
2211
+ signal?: AbortSignal;
2212
+ }): Promise<Wallet>;
2213
+ /** The device core + share store a {@link Wallet} needs to co-sign an ed25519 (FROST) tx locally. */
2214
+ private walletDeviceDeps;
2215
+ /**
2216
+ * Join a **member-bound** wallet's multi-device keygen ceremony (#344, #349) as an invited org
2217
+ * member — call this from the member's OWN device/session (never a tenant API key: every HTTP
2218
+ * call here is member-session-authenticated, see {@link HttpClient.requestAsMember}). Unlike
2219
+ * {@link create}, every member (N of them) + the platform join the SAME relay session, so:
2220
+ *
2221
+ * 1. fetch this member's own party ({@link MemberCeremony}) plus the wallet's other
2222
+ * share-holders (to derive the full n-party relay roster the ceremony needs),
2223
+ * 2. ack readiness (`POST .../ceremony/join`) — the backend starts its platform party's own
2224
+ * relay connection only once EVERY member has acked, so this must happen before the ceremony
2225
+ * can complete,
2226
+ * 3. run this device's half of the n-party keygen over the relay (blocks until every party,
2227
+ * including the platform, is present and the ceremony completes),
2228
+ * 4. seal + persist the device's ONE share, keyed by `(walletId, membershipId)` — never by
2229
+ * `walletId` alone, since several members' shares for the SAME wallet may live in one
2230
+ * `shareStore` (e.g. a shared device, or a test harness).
2231
+ *
2232
+ * Safe to retry: a failed ceremony reopens the roster server-side, and re-acking/re-running is
2233
+ * idempotent from the caller's perspective.
2234
+ */
2235
+ joinCeremony(walletId: string, options?: JoinCeremonyOptions): Promise<MemberCeremonyJoinResponse>;
2236
+ /**
2237
+ * Join a **member-bound** wallet's multi-device SIGN ceremony (#347, #349) as an invited org
2238
+ * member — the signing analogue of {@link joinCeremony}, called from the member's OWN
2239
+ * device/session. A sign is a `t`-of-`n` SELECTION (only `t` of the N+1 parties actually sign),
2240
+ * so the flow is:
2241
+ *
2242
+ * 1. cast this member's APPROVE vote (`POST .../sign-requests/:reqId/approve`) — a no-op if
2243
+ * already cast; once `t-1` members have approved, the backend fixes the signing quorum,
2244
+ * 2. poll this member's own sign-ceremony party (`GET .../sign-requests/:reqId/ceremony/mine`)
2245
+ * until the quorum is fixed AND this member was selected into it ({@link MemberSignCeremony.ready}),
2246
+ * 3. load this member's OWN stored share (keyed by `(walletId, membershipId)`, from
2247
+ * {@link joinCeremony}) and run this device's half of the fixed-quorum sign ceremony —
2248
+ * the interactive protocol's public output IS the completed signature.
2249
+ *
2250
+ * A member who is not selected into the fixed quorum, or whose approval never gets it there,
2251
+ * simply times out (`sign_ceremony_timeout`) rather than running anything.
2252
+ */
2253
+ joinSignCeremony(walletId: string, reqId: string, options?: JoinSignCeremonyOptions): Promise<DeviceSignResult>;
2254
+ /**
2255
+ * Approve a pending async signing request (#229) — either completing a single
2256
+ * device-approval request outright, or casting one APPROVE vote in an M-of-N approver
2257
+ * quorum (#309) configured on the wallet (via the tenant dashboard's `PUT .../quorum`). A
2258
+ * non-quorum request starts the MPC ceremony immediately; a quorum request only starts
2259
+ * it once enough approvers vote, and until then the response reports how many
2260
+ * approvals are still needed via {@link SigningRequestResponse.approvalsRemaining}.
2261
+ *
2262
+ * Takes `walletId` + `reqId` directly (rather than a {@link Wallet} instance) since an
2263
+ * approver typically learns of a pending request out-of-band — e.g. a
2264
+ * `sign_request.created` webhook or push notification — without first loading the wallet.
2265
+ */
2266
+ approveSignRequest(walletId: string, reqId: string, options?: {
2267
+ signal?: AbortSignal;
2268
+ }): Promise<SigningRequestResponse>;
2269
+ /**
2270
+ * Decline a pending async signing request (#229) — either rejecting a single
2271
+ * device-approval request outright, or casting a REJECT vote in an M-of-N approver
2272
+ * quorum (#309), which declines the request as soon as one approver rejects it.
2273
+ */
2274
+ declineSignRequest(walletId: string, reqId: string, options?: {
2275
+ signal?: AbortSignal;
2276
+ }): Promise<SigningRequestResponse>;
2277
+ /**
2278
+ * List a wallet's async signing requests, newest first (paginated) — optionally filtered to
2279
+ * a single lifecycle {@link SignRequestsQuery.status} (#332). The API paginates the
2280
+ * *filtered* set, so `total`/`page` always describe what was actually matched.
2281
+ */
2282
+ listSignRequests(walletId: string, query?: SignRequestsQuery, options?: {
2283
+ signal?: AbortSignal;
2284
+ }): Promise<Page<SigningRequestResponse>>;
2285
+ /**
2286
+ * List a wallet's **pending approver queue** — the ergonomic entry point over
2287
+ * {@link listSignRequests} pinned to `status: 'pending_approval'` (#309, #329): the
2288
+ * requests currently awaiting an approver's {@link approveSignRequest}/
2289
+ * {@link declineSignRequest} vote, either a single device-approval request or one still
2290
+ * short of its M-of-N quorum (see {@link SigningRequestResponse.approvalsRemaining}).
2291
+ */
2292
+ listPendingApprovals(walletId: string, query?: PageQuery, options?: {
2293
+ signal?: AbortSignal;
2294
+ }): Promise<Page<SigningRequestResponse>>;
2295
+ /**
2296
+ * Run the cggmp24 (secp256k1) device keygen and persist the resulting share(s) (#351/#78). Two shapes,
2297
+ * chosen by whether the ceremony carries client-held extra parties ({@link WalletCeremony.additionalParties}):
2298
+ *
2299
+ * - **Single client party** (today's path — `[device, server]` or custodial `[device, server, recovery]`,
2300
+ * where the platform drives every non-device party): run just the `device` party and seal its share
2301
+ * under the wallet id, exactly as before.
2302
+ * - **Non-custodial `[device, server, user_backup]`** (a `user_backup` extra party): run BOTH the
2303
+ * `device` and `user_backup` parties CONCURRENTLY in the SAME relay session (each with its own
2304
+ * role-scoped relay token + its OWN Paillier primes), then persist the `device` share locally and
2305
+ * seal + register the `user_backup` share as the non-custodial backup (requires `backup`).
2306
+ *
2307
+ * Any additional party the client cannot drive (a non-`user_backup` role) is refused up front — leaving
2308
+ * it unjoined would hang the whole ceremony, so failing loud beats silently deadlocking.
2309
+ */
2310
+ private runSecpKeygen;
2311
+ /**
2312
+ * Register a sealed user_backup {@link RecoveryRegisterPayload} server-side, with a small bounded retry
2313
+ * on TRANSIENT failures (network / 5xx / rate-limit) — a flaky moment must not cost the backup. A
2314
+ * non-transient failure (4xx) fails fast. On abort, the abort propagates unchanged. If it ultimately
2315
+ * cannot register, throws a distinct, actionable {@link WaaskeyError} `backup_failed` (NOT `keygen_failed`)
2316
+ * — the caller's cue that the wallet was created but its backup is only local, and pointing at
2317
+ * {@link retryBackup}. The pending slot is intentionally NOT touched here, so a failure leaves the
2318
+ * sealed share intact for the retry.
2319
+ */
2320
+ private registerBackup;
2321
+ /**
2322
+ * Re-register a non-custodial wallet's user_backup backup that a previous {@link create} sealed locally
2323
+ * but could not register (a `backup_failed` create) (#351/#78). Reads the LOCAL pending-backup slot's
2324
+ * sealed ciphertext and re-POSTs it — **no re-keygen** (the share is not re-derivable) — with the same
2325
+ * bounded transient retry, then clears the slot on success. Idempotent-ish: a no-op `share_not_found`
2326
+ * when nothing is pending (already registered, or never created here). Requires a share store.
2327
+ */
2328
+ retryBackup(walletId: string, options?: {
2329
+ signal?: AbortSignal;
2330
+ }): Promise<RecoveryShareInfo>;
2331
+ /**
2332
+ * Device-loss RECOVERY CO-SIGN for a non-custodial `[device, server, user_backup]` secp256k1 wallet
2333
+ * (#351/#78). When the device is lost, the user restores their client-held `user_backup` share from the
2334
+ * sealed server-side backup and co-signs `digest` with the platform's `server` party — the 2-party
2335
+ * `{server, user_backup}` quorum. This is NOT the platform-only custodial `{server, recovery}` recoverSign
2336
+ * (`recovery.recoverSign` / `POST …/recovery/recover-sign`): here the platform CANNOT sign alone; the
2337
+ * user's restored share is the co-signing factor.
2338
+ *
2339
+ * 1. **RESTORE** — retrieve the sealed `user_backup` ciphertext (the multi-factor recovery gate,
2340
+ * {@link RecoverParams}, releases it) and open it with the recovery code CLIENT-SIDE (Contract A: the
2341
+ * raw code never leaves the device), yielding the cggmp24 `user_backup` KeyShare. No registered backup
2342
+ * fails `share_not_found`; a wrong recovery code fails `invalid_recovery_code` — either BEFORE any
2343
+ * ceremony starts, so a bad restore never signs (and never hangs).
2344
+ * 2. **START** — `POST /v1/wallets/:id/recover-sign-session` with the digest (+ passkey step-up when the
2345
+ * wallet requires it, threaded exactly like a normal sign via `options`) → the `user_backup` party's
2346
+ * ceremony descriptor: its role, the `{server, user_backup}` keygen `participants`, this party's
2347
+ * `signerPosition`, and a `user_backup`-role relay token.
2348
+ * 3. **CO-SIGN** — run the `user_backup` MPC party ({@link MpcCore.runSign}) over the relay with the
2349
+ * restored share + that descriptor; the platform `server` party co-signs server-side. Returns the
2350
+ * resulting signature.
2351
+ *
2352
+ * The ONLY differences from a normal `{device, server}` device sign are the SHARE (the restored
2353
+ * `user_backup`, not the local device share) and the DESCRIPTOR (from the recover-sign session, not the
2354
+ * normal sign session) — the same {@link MpcCore.runSign} relay machinery drives both.
2355
+ *
2356
+ * Requires `mpc` on the client (the co-signing core). No share store is needed: the restored share is
2357
+ * held only in memory for the ceremony and never persisted (the device is lost/new). secp256k1 only —
2358
+ * re-provisioning a fresh device share (reshare back to a full 2-of-3) is a separate follow-up.
2359
+ */
2360
+ recoverSign(walletId: string, params: RecoverSignParams, options?: SignOptions): Promise<string>;
2361
+ /**
2362
+ * Retrieve the sealed `user_backup` ciphertext via the recovery gate ({@link fetchRecoveryCiphertext}),
2363
+ * mapping a "no recovery share registered" (404) to the actionable `share_not_found` — this wallet has no
2364
+ * client-held `user_backup` backup to co-sign with (its device-loss recovery is the custodial path instead).
2365
+ */
2366
+ private fetchUserBackupCiphertext;
2367
+ /**
2368
+ * Run one or more device keygen parties (each `mpc.runKeygen`), concurrently, mapping any failure to a
2369
+ * single `keygen_failed`. Used for both the single `device` party and the two-party non-custodial
2370
+ * `[device, user_backup]` ceremony — one place owns the error contract so both paths stay identical.
2371
+ */
2372
+ private runKeygenParties;
2373
+ /**
2374
+ * Run the device half of an ed25519 (FROST) keygen (#110) and seal the resulting `{keyPackage,
2375
+ * publicKeyPackage}` share — the EdDSA counterpart of the cggmp24 `runKeygen` branch in {@link create}.
2376
+ * The device co-generates the group key with the backend `server` party over the relay; the FROST DKG
2377
+ * is the 2-party {device, server} quorum the ceremony names (M6 scope), roster-addressed like the
2378
+ * member ceremony rather than the cggmp24 single-peer shape.
2379
+ */
2380
+ private runEddsaKeygen;
2381
+ /** Poll the wallet until keygen completes (ACTIVE), or throw on failure/timeout. */
2382
+ private waitUntilActive;
2383
+ /** Poll the member's own sign ceremony until the t-of-n quorum is fixed AND this member is selected, or throw on timeout. */
2384
+ private waitUntilReady;
75
2385
  }
2386
+ /**
2387
+ * Storage key for a member-bound wallet's per-member share (#349) — distinct from the embedded
2388
+ * flow's bare `walletId` key ({@link Wallet}'s single device share), since several members' shares
2389
+ * for the SAME wallet may live in one {@link ShareStore} (e.g. a shared device, or a test harness).
2390
+ */
2391
+ declare function memberShareKey(walletId: string, membershipId: string): string;
2392
+ /**
2393
+ * Storage key for a non-custodial wallet's PENDING user_backup backup (#351/#78) — the sealed
2394
+ * {@link RecoveryRegisterPayload} ciphertext {@link Wallets.create} persists before registering it
2395
+ * server-side, so a failed/interrupted registration never loses the (non-re-derivable) user_backup share.
2396
+ * Distinct from the device share's bare `walletId` key; cleared by {@link Wallets.retryBackup} on success.
2397
+ * The value is opaque without the recovery code, so a local copy is a safe retry buffer.
2398
+ */
2399
+ declare function userBackupPendingKey(walletId: string): string;
76
2400
 
77
2401
  /**
78
2402
  * The Waaskey client — entry point of the SDK.
79
2403
  *
80
2404
  * @example
81
2405
  * ```ts
82
- * import { Waaskey } from '@waaskey/sdk';
2406
+ * import { Waaskey, WasmMpcCore, EncryptedShareStore } from '@waaskey/sdk';
83
2407
  *
84
- * const waaskey = new Waaskey({ apiKey: process.env.WAASKEY_PUBLISHABLE_KEY! });
85
- * const wallet = await waaskey.wallets.create({ userId: user.id, chain: 'ethereum' });
86
- * const signature = await wallet.signMessage('Hello Waaskey');
2408
+ * const waaskey = new Waaskey({
2409
+ * apiKey: process.env.WAASKEY_PUBLISHABLE_KEY!,
2410
+ * mpc: new WasmMpcCore(loadClientWasm),
2411
+ * shareStore: EncryptedShareStore.browser(sessionSecret),
2412
+ * });
2413
+ * const wallet = await waaskey.wallets.create({ chain: 'ethereum' });
2414
+ * const signature = await wallet.sign(digestHex);
87
2415
  * ```
88
2416
  */
89
2417
  declare class Waaskey {
90
2418
  /** The `wallets` resource. */
91
2419
  readonly wallets: Wallets;
2420
+ /** The `recovery` resource — multi-factor, client-encrypted wallet recovery. */
2421
+ readonly recovery: Recovery;
2422
+ /** The `reshare` resource — device-side completion of a device-retaining reshare (#318). */
2423
+ readonly reshare: Reshare;
2424
+ /** The `balances` resource — client-side balance reads from a chain provider (no backend). */
2425
+ readonly balances: Balances;
2426
+ /** The `auth` resource — embedded end-user login (email-OTP, …) → non-custodial wallet. */
2427
+ readonly auth: Auth;
2428
+ /** The `members` resource — org-member (dashboard "plane B") bearer login for headless consumers. */
2429
+ readonly members: Members;
2430
+ /** The `onramp` resource — fund the wallet with fiat via a provider on-ramp. */
2431
+ readonly onramp: Onramp;
2432
+ /** Default fetch used by the optional {@link broadcast} helper (from `WaaskeyOptions.fetch`). */
2433
+ private readonly defaultFetch?;
92
2434
  constructor(options: WaaskeyOptions);
2435
+ /**
2436
+ * **Optional** client-side broadcast of a signed raw tx from {@link Wallet.send}.
2437
+ *
2438
+ * WaaS is a **signing service** — `wallet.send(...)` returns `signedTx` and WaaS never
2439
+ * submits it. This is a thin, best-effort convenience to broadcast from **your own**
2440
+ * node/provider (one JSON-RPC call, no status polling / no retries — tracking is your
2441
+ * concern). Most integrators broadcast with their own infra.
2442
+ *
2443
+ * @example
2444
+ * ```ts
2445
+ * const { signedTx } = await wallet.send({ chainId: 'evm:1', to, value });
2446
+ * const { txHash } = await waaskey.broadcast(signedTx, { rpcUrl: 'https://your-rpc' });
2447
+ * ```
2448
+ */
2449
+ broadcast(signedTx: string, options: BroadcastOptions): Promise<BroadcastResult>;
93
2450
  }
94
2451
 
95
- export { type Chain, type CreateWalletParams, Waaskey, WaaskeyError, type WaaskeyOptions, Wallet, type WalletData, Wallets };
2452
+ /**
2453
+ * Whether a wallet is **non-custodial** — the platform alone cannot produce a
2454
+ * signature because the shares it holds do not meet the threshold
2455
+ * (`platformShareCount < threshold`, i.e. custody type `shared` or `self_custody`).
2456
+ *
2457
+ * This mirrors the server-attested invariant `platformShares < t ⟺ non-custodial`,
2458
+ * so an app can display the custody guarantee straight off the returned wallet.
2459
+ */
2460
+ declare function isNonCustodial(wallet: Pick<WalletData, 'platformShareCount' | 'threshold'>): boolean;
2461
+ /**
2462
+ * Validate a create-wallet custody policy client-side, before the request — clear
2463
+ * errors instead of a round-trip to a 400. A no-op when no policy is supplied
2464
+ * (the default embedded 2-of-3 topology).
2465
+ *
2466
+ * The rules mirror the backend contract: `parties` (when supplied) are ≥2 distinct,
2467
+ * non-empty roles that define `n`; `threshold` is an integer in `[2, n]`; and
2468
+ * `custodyKinds` has exactly one known kind per party.
2469
+ */
2470
+ declare function validateCustodyPolicy(params: Pick<CreateWalletParams, 'threshold' | 'parties' | 'custodyKinds' | 'custodyType'>): void;
2471
+
2472
+ /**
2473
+ * **Optional** client-side broadcast helper.
2474
+ *
2475
+ * WaaS is a **signing service**, not a broadcaster: `wallet.send(...)` returns the
2476
+ * signed raw transaction (`signedTx`) and WaaS never submits it. Broadcasting,
2477
+ * nonce/mempool handling and status-tracking are the integrator's concern — that
2478
+ * is what non-custodial means. This helper is a thin convenience so you can submit
2479
+ * a signed tx from **your own** node/provider; it is deliberately best-effort:
2480
+ * one JSON-RPC call, **no status polling and no retries**. For production you will
2481
+ * usually broadcast (and track) with your own infra.
2482
+ *
2483
+ * Today it submits an EVM raw transaction via `eth_sendRawTransaction`. The node's
2484
+ * returned hash is passed through as {@link BroadcastResult.txHash}.
2485
+ */
2486
+ declare function broadcast(signedTx: string, options: BroadcastOptions): Promise<BroadcastResult>;
2487
+
2488
+ /**
2489
+ * PasskeyPrfSecretProvider — derives a stable AES-key secret from a passkey's
2490
+ * PRF extension output, so the device share in {@link EncryptedShareStore} is
2491
+ * sealed under a biometric-gated passkey rather than a password.
2492
+ *
2493
+ * ### How the secret stays stable
2494
+ * The PRF extension evaluates a pseudo-random function keyed by the credential
2495
+ * (stored in the authenticator) over a caller-supplied `salt`. `enroll()` mints a
2496
+ * **random per-user salt** (issue #41) and returns it alongside `credentialId`;
2497
+ * persist both (they are non-secret) and pass the salt back to `unlock()`. Because
2498
+ * the credential and the salt are the same, the PRF output — and therefore the
2499
+ * derived `secret` — is identical on every `unlock()`. (A random per-user salt,
2500
+ * rather than one global constant, prevents cross-context PRF correlation.)
2501
+ *
2502
+ * ### Wiring to EncryptedShareStore
2503
+ * ```ts
2504
+ * import { PasskeyPrfSecretProvider } from '@waaskey/sdk';
2505
+ * import { EncryptedShareStore } from '@waaskey/sdk';
2506
+ *
2507
+ * const prf = new PasskeyPrfSecretProvider();
2508
+ * const { credentialId, secret } = await prf.enroll({ rpName: 'My App', userName: user.email });
2509
+ * // persist credentialId (non-secret); keep secret in memory for this session
2510
+ * const store = EncryptedShareStore.browser(secret);
2511
+ *
2512
+ * // On a subsequent session:
2513
+ * const { secret } = await prf.unlock(credentialId);
2514
+ * const store = EncryptedShareStore.browser(secret);
2515
+ *
2516
+ * // @waaskey/react: usePrfStore() wraps the above + stores credentialId in localStorage.
2517
+ * // react-native: use a native PRF authenticator, supply a custom PrfCeremony.
2518
+ * ```
2519
+ */
2520
+
2521
+ /**
2522
+ * Override the underlying WebAuthn ceremony calls. Useful for React Native
2523
+ * (pass a native PRF authenticator module) or tests (mock the PRF output).
2524
+ */
2525
+ interface PrfCeremony {
2526
+ register(options: PublicKeyCredentialCreationOptionsJSON): Promise<{
2527
+ credentialId: string;
2528
+ prfResult: ArrayBuffer | null;
2529
+ }>;
2530
+ authenticate(credentialId: string, options: PublicKeyCredentialRequestOptionsJSON): Promise<{
2531
+ prfResult: ArrayBuffer | null;
2532
+ }>;
2533
+ }
2534
+ /** Options accepted by `PasskeyPrfSecretProvider.enroll(...)`. */
2535
+ interface PasskeyPrfEnrollOptions {
2536
+ /**
2537
+ * Human-readable name for the relying party (your app). Used in the
2538
+ * authenticator's registration ceremony UI.
2539
+ */
2540
+ rpName?: string;
2541
+ /** Display name shown in the authenticator UI for the user. */
2542
+ userName?: string;
2543
+ /**
2544
+ * Fully qualified domain name of the relying party. Defaults to the current
2545
+ * origin's hostname in the browser. Must match the value used for `unlock()`.
2546
+ */
2547
+ rpId?: string;
2548
+ /** Custom ceremony (for non-browser runtimes or tests). */
2549
+ ceremony?: PrfCeremony;
2550
+ }
2551
+ /** Result of a successful `enroll()` or `unlock()`. */
2552
+ interface PasskeyPrfResult {
2553
+ /** Credential id (Base64URL). Persist this — pass it to `unlock()` later. */
2554
+ credentialId: string;
2555
+ /**
2556
+ * Base64-encoded per-user PRF evaluation salt (issue #41). Non-secret — persist it
2557
+ * alongside `credentialId` and pass it back to `unlock({ salt })` so the same secret
2558
+ * is re-derived. Omitting it on `unlock` falls back to the legacy constant salt.
2559
+ */
2560
+ salt: string;
2561
+ /**
2562
+ * Base64-encoded 32-byte PRF output. Use as the `secret` argument to
2563
+ * `EncryptedShareStore.browser(secret)`. Keep in memory; never persist it.
2564
+ */
2565
+ secret: string;
2566
+ }
2567
+ /**
2568
+ * Returns `true` if the current runtime has WebAuthn available at all.
2569
+ * A `false` result means passkeys are completely unavailable; fall back to
2570
+ * password or device-secret share sealing.
2571
+ */
2572
+ declare function isPasskeySupported(): boolean;
2573
+ /**
2574
+ * Returns `true` if the current platform authenticator supports the PRF
2575
+ * extension (needed for passkey share sealing).
2576
+ *
2577
+ * This is a best-effort probe — not all browsers expose `credentials.get` in a
2578
+ * way that lets us query PRF support without a full ceremony. For definitive
2579
+ * detection, attempt `enroll()` and handle the `unsupported` error.
2580
+ *
2581
+ * In practice: Chrome 116+ / Edge 116+ on a platform authenticator support PRF;
2582
+ * Safari / iOS 17 do not yet.
2583
+ */
2584
+ declare function isPrfSupported(): Promise<boolean>;
2585
+ /**
2586
+ * Derives a stable AES-key `secret` from a passkey's PRF extension for use with
2587
+ * {@link EncryptedShareStore}. The secret is deterministic: same credential +
2588
+ * same fixed salt → same 32 bytes every time.
2589
+ */
2590
+ declare class PasskeyPrfSecretProvider {
2591
+ /**
2592
+ * Register a new passkey that supports PRF and derive the initial secret from it.
2593
+ *
2594
+ * @returns `{ credentialId, secret }` — persist `credentialId`; use `secret`
2595
+ * to construct `EncryptedShareStore.browser(secret)` for this session only.
2596
+ *
2597
+ * @throws `WaaskeyError('unsupported')` when PRF isn't available in this runtime.
2598
+ * @throws `WaaskeyError('aborted')` when the user cancels the authenticator dialog.
2599
+ */
2600
+ enroll(opts?: PasskeyPrfEnrollOptions): Promise<PasskeyPrfResult>;
2601
+ /**
2602
+ * Authenticate with an existing passkey and re-derive the same stable secret.
2603
+ *
2604
+ * @param credentialId — the id returned by `enroll()`.
2605
+ * @param opts.salt — the per-user salt `enroll()` returned (issue #41). Pass it to
2606
+ * re-derive the same secret; omit only for legacy credentials enrolled before
2607
+ * per-user salts (falls back to the constant salt).
2608
+ * @returns `{ credentialId, salt, secret }` — reconstruct `EncryptedShareStore.browser(secret)`.
2609
+ *
2610
+ * @throws `WaaskeyError('unsupported')` when PRF isn't available.
2611
+ * @throws `WaaskeyError('aborted')` when the user cancels.
2612
+ */
2613
+ unlock(credentialId: string, opts?: {
2614
+ rpId?: string;
2615
+ ceremony?: PrfCeremony;
2616
+ salt?: string;
2617
+ }): Promise<PasskeyPrfResult>;
2618
+ }
2619
+
2620
+ /**
2621
+ * Passkey step-up signing assertion (Pattern B).
2622
+ *
2623
+ * The backend accepts an optional `passkeyAssertion` (`AuthenticationResponseJSON`)
2624
+ * on sign/send/recover DTOs and verifies it server-side. This module runs the
2625
+ * WebAuthn assertion over a **server-issued one-time challenge** and returns the
2626
+ * typed `AuthenticationResponseJSON` ready to be included in the POST body.
2627
+ *
2628
+ * ### Challenge (issue #39 — replay-safe)
2629
+ * The challenge is a random nonce the **server** mints (via a step-up challenge
2630
+ * endpoint), NOT a value derived from the request payload. `Wallet.sign`/`send`
2631
+ * fetch it, run the assertion over it, and echo its `challengeId` on the request;
2632
+ * the server verifies the assertion against the stored challenge and **burns** it,
2633
+ * so a captured assertion cannot be replayed for a later identical payload.
2634
+ *
2635
+ * ### Wiring in Wallet
2636
+ * ```ts
2637
+ * // In wallet.sign() (handled by Wallet.resolveStepUp):
2638
+ * const { challengeId, challenge } = await http.request('POST', '/v1/wallets/${id}/stepup/challenge', { operation: 'sign' });
2639
+ * const assertion = await getSigningAssertion(challenge, { credentialId });
2640
+ * await http.request('POST', '/v1/wallets/${id}/sign', { message, passkeyAssertion: assertion, passkeyChallengeId: challengeId });
2641
+ *
2642
+ * // Caller opts in:
2643
+ * await wallet.sign(digest, { requirePasskey: true });
2644
+ * ```
2645
+ */
2646
+
2647
+ /**
2648
+ * Returns `true` if the current runtime has WebAuthn available — i.e. the SDK
2649
+ * can attempt a passkey assertion. Use this as a fast pre-flight before calling
2650
+ * `getSigningAssertion()` when you want to show/hide a "sign with passkey" button.
2651
+ */
2652
+ declare function isPasskeyAssertionSupported(): boolean;
2653
+ /**
2654
+ * Override the WebAuthn `credentials.get` call. Useful for React Native (native
2655
+ * passkey module) or unit tests (mock the assertion response).
2656
+ */
2657
+ interface SigningAssertionCeremony {
2658
+ get(options: PublicKeyCredentialRequestOptionsJSON): Promise<AuthenticationResponseJSON>;
2659
+ }
2660
+ /** Options for `getSigningAssertion()`. */
2661
+ interface SigningAssertionOptions {
2662
+ /**
2663
+ * Restrict the assertion to a specific credential. Pass the `credentialId`
2664
+ * that was enrolled (e.g. from `PasskeyPrfSecretProvider.enroll()`). When
2665
+ * omitted the browser presents all resident credentials for the RP.
2666
+ */
2667
+ credentialId?: string;
2668
+ /**
2669
+ * Fully qualified domain name of the relying party. Defaults to the current
2670
+ * origin's hostname in the browser.
2671
+ */
2672
+ rpId?: string;
2673
+ /**
2674
+ * Custom ceremony implementation (for React Native or tests).
2675
+ */
2676
+ ceremony?: SigningAssertionCeremony;
2677
+ }
2678
+ /**
2679
+ * Run a WebAuthn assertion over a **server-issued one-time `challenge`** and return
2680
+ * the typed `AuthenticationResponseJSON` to attach as `passkeyAssertion` in the
2681
+ * request body.
2682
+ *
2683
+ * @param challenge — the base64url one-time challenge nonce from the server's
2684
+ * step-up challenge endpoint ({@link StepUpChallengeResponse.challenge}). It is
2685
+ * passed to the authenticator verbatim; the server verifies the assertion against
2686
+ * the stored challenge and burns it, so the assertion cannot be replayed.
2687
+ * @param options — optional ceremony override and credential hint.
2688
+ *
2689
+ * @throws `WaaskeyError('unsupported')` when WebAuthn is not available in this runtime.
2690
+ * @throws `WaaskeyError('aborted')` when the user cancels the authenticator prompt.
2691
+ */
2692
+ declare function getSigningAssertion(challenge: string, options?: SigningAssertionOptions): Promise<AuthenticationResponseJSON>;
2693
+
2694
+ export { Analytics, type AnalyticsEvent, type AnalyticsEventType, type AnalyticsSink, Auth, type Balance, Balances, type BroadcastOptions, type BroadcastResult, CLIENT_WASM_VERSION, type CeremonyParams, type Chain, type ChainConfig, type ChainProvider, type ClientWasmLoader, type ClientWasmModule, type CreateMemberWalletParams, type CreateWalletOptions, type CreateWalletParams, type CreateWalletResponse, type CustodyKind, type CustodyType, type DeviceCompleteReshareParams, type DeviceCompleteReshareResult, type DeviceKeygenParams, type DeviceKeygenResult, type DeviceReshareAssembleParams, type DeviceReshareAssembleResult, type DeviceReshareMaterial, type DeviceSignParams, type DeviceSignResult, type EddsaAssembleRequest, type EddsaCeremonyParams, type EddsaKeygenParams, type EddsaKeygenResult, type EddsaSendSession, type EddsaSignParams, type EddsaSignResult, type EmailStartResult, type EmbeddedSession, EncryptedShareStore, type EndUser, EvmRpcProvider, type FactorEnrollment, type FactorVerification, type FirebaseAuthRequest, HttpAnalyticsSink, IndexedDbKeyValueStore, type JoinCeremonyOptions, type JoinSignCeremonyOptions, type KeyValueStore, type Member, type MemberCeremony, type MemberCeremonyJoinResponse, type MemberCeremonyParams, type MemberKeygenParams, type MemberRole, type MemberSession, type MemberSignCeremony, type MemberSignParams, Members, type MembershipScope, MemoryKeyValueStore, MemoryPrimeStore, type MpcCore, type MpcCurve, Onramp, type OnrampWidgetParams, type OnrampWidgetUrl, type Page, type PageQuery, type PasskeyAssertionJSON, type PasskeyCeremony, type PasskeyPrfEnrollOptions, type PasskeyPrfResult, PasskeyPrfSecretProvider, type PrfCeremony, PrimePool, type PrimePoolOptions, type PrimePoolStore, type RecoverParams, type RecoverSignParams, type RecoverWalletResponse, Recovery, type RecoveryChallengeResponse, type RecoveryFactor, type RecoveryRetrieveResponse, type RecoveryShareInfo, type RegisterRecoveryParams, Reshare, type ReshareCompletionCeremony, type ReshareCompletionParams, type ReshareCompletionResult, type ReshareWalletResponse, type SendOptions, type SendParams, type SendResult, type ShareStore, type SignMessageResponse, type SignOptions, type SignRequestsQuery, type SignSessionResponse, type Signature, type SignatureKind, type SigningAssertionCeremony, type SigningAssertionOptions, type SigningRequestResponse, type SigningRequestStatus, type StepUpChallengeResponse, type StepUpOperation, type TokenBalanceOptions, type TxStatus, type VerifiedWasmLoaderOptions, Waaskey, WaaskeyError, type WaaskeyErrorCode, type WaaskeyErrorOptions, type WaaskeyOptions, Wallet, type WalletActionType, type WalletBackupParams, type WalletCeremony, type WalletCurve, type WalletData, type WalletShareholder, type WalletStatus, Wallets, WasmMpcCore, broadcast, createVerifiedClientWasmLoader, epochShareKey, formatUnits, generateRecoveryCode, getSigningAssertion, isNonCustodial, isPasskeyAssertionSupported, isPasskeySupported, isPrfSupported, loadClientWasm, memberShareKey, userBackupPendingKey, validateCustodyPolicy, verifyWasmIntegrity };