@waaskey/sdk 0.4.1 → 0.6.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.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import { x25519 } from '@noble/curves/ed25519.js';
2
+ import { secp256k1 } from '@noble/curves/secp256k1.js';
3
+ import { keccak_256 } from '@noble/hashes/sha3.js';
2
4
 
3
5
  // src/analytics/analytics.ts
4
6
  var Analytics = class {
@@ -314,6 +316,99 @@ async function broadcast(signedTx, options) {
314
316
  return { txHash: json.result };
315
317
  }
316
318
 
319
+ // src/devices.ts
320
+ var DEVICE_ID_KEY = "waaskey:device-id";
321
+ var Devices = class {
322
+ constructor(http, store) {
323
+ this.http = http;
324
+ this.store = store;
325
+ }
326
+ http;
327
+ store;
328
+ /**
329
+ * This device's stable id, minting and persisting one on first use.
330
+ *
331
+ * A caller with no configured store must pass its own id to {@link register} — an id that is
332
+ * regenerated per call is worse than none: the registry would fill with orphan devices and none
333
+ * of them would match the share this browser actually holds.
334
+ *
335
+ * @throws {WaaskeyError} `device_store_required` — no store configured to persist the id.
336
+ */
337
+ async deviceId() {
338
+ if (!this.store) {
339
+ throw new WaaskeyError("A stable device id needs somewhere to live \u2014 pass `deviceStore` to `new Waaskey(...)`, or supply `deviceId` yourself.", "device_store_required");
340
+ }
341
+ const existing = await this.store.get(DEVICE_ID_KEY);
342
+ if (existing) return existing;
343
+ const minted = crypto.randomUUID();
344
+ await this.store.set(DEVICE_ID_KEY, minted);
345
+ return minted;
346
+ }
347
+ /**
348
+ * The persisted device id, or `undefined` when this client has never minted one.
349
+ *
350
+ * Read-only on purpose: it is how other resources DEFAULT to this device without minting an
351
+ * identity as a side effect of joining a ceremony. A silently minted id would be one this
352
+ * device never registered and holds no share for.
353
+ */
354
+ async stored() {
355
+ return await this.store?.get(DEVICE_ID_KEY) ?? void 0;
356
+ }
357
+ /**
358
+ * Register (or re-register) this device under the caller's member seat.
359
+ *
360
+ * Idempotent by `deviceId`: the backend upserts, so calling it on every start refreshes
361
+ * `lastSeenAt` and rotates the encryption public key without creating a second device. That
362
+ * matters — a duplicate device would appear in the picker as a share-holder candidate that holds
363
+ * nothing.
364
+ *
365
+ * `encryptionPublicKey` is this device's X25519 key; dealers seal reshare sub-shares and FROST
366
+ * round-2 packages to it, so a device that will hold a share should register one.
367
+ */
368
+ async register(registration, signal) {
369
+ const deviceId = registration.deviceId ?? await this.deviceId();
370
+ return this.http.requestAsMember(
371
+ "POST",
372
+ "/v1/devices",
373
+ { deviceId, label: registration.label, ...registration.encryptionPublicKey ? { encryptionPublicKey: registration.encryptionPublicKey } : {} },
374
+ signal
375
+ );
376
+ }
377
+ /** The caller's own registered devices — this one and their others. */
378
+ async list(signal) {
379
+ return this.http.requestAsMember("GET", "/v1/devices", void 0, signal);
380
+ }
381
+ /**
382
+ * The caller's devices, each marked whether it is THIS client (#57).
383
+ *
384
+ * A device-management UI cannot work this out on its own: the registry returns a member's
385
+ * devices with no notion of who is asking, and the only thing that distinguishes this browser is
386
+ * the id stored here. Getting it wrong is not cosmetic — "revoke" next to the wrong row revokes
387
+ * the device the user is standing on, and the backend will happily do it.
388
+ *
389
+ * Every device reads as "not this one" when this client has no stored id, which is the honest
390
+ * answer: it has no identity to match against.
391
+ */
392
+ async mine(signal) {
393
+ const [devices, current] = await Promise.all([this.list(signal), this.stored()]);
394
+ return devices.map((device) => ({ ...device, isThisDevice: current !== void 0 && device.deviceId === current }));
395
+ }
396
+ /**
397
+ * Revoke one of the caller's own devices by its registry id (not its `deviceId`).
398
+ *
399
+ * The backend tombstones any live shares it holds, and REFUSES (409) when that would strand a
400
+ * wallet mid-keygen or drop an active wallet below its signing threshold — losing the ability to
401
+ * sign is not something a device-management action gets to do quietly.
402
+ */
403
+ async revoke(id, signal) {
404
+ await this.http.requestAsMember("DELETE", `/v1/devices/${encodeURIComponent(id)}`, void 0, signal);
405
+ }
406
+ /** Forget this browser's device id, so the next {@link deviceId} mints a new one. */
407
+ async forget() {
408
+ await this.store?.delete(DEVICE_ID_KEY);
409
+ }
410
+ };
411
+
317
412
  // src/http.ts
318
413
  var HttpClient = class {
319
414
  apiKey;
@@ -458,6 +553,36 @@ var Members = class {
458
553
  memberSignOut() {
459
554
  this.held = void 0;
460
555
  }
556
+ /**
557
+ * The tenant's team members, optionally narrowed to the share-holder-eligible roster (#57) —
558
+ * what a wallet creator picks share-holders from.
559
+ *
560
+ * `canHoldShare` is a per-membership capability, not part of the login identity, so it is only
561
+ * on this view: the session's own `member` does not carry it (see {@link eligibility}).
562
+ */
563
+ async list(options = {}, signal) {
564
+ const query = options.canHoldShare === void 0 ? "" : `?canHoldShare=${options.canHoldShare}`;
565
+ return this.http.requestAsMember("GET", `/v1/members${query}`, void 0, signal);
566
+ }
567
+ /**
568
+ * Whether the logged-in member may hold a wallet key share, and who else may — one call, because
569
+ * a device-picker UI needs both at once.
570
+ *
571
+ * It reads the roster to answer a question about the caller because the member session does not
572
+ * carry `canHoldShare` (the backend's session view is the login identity, `IMember`). Worth
573
+ * knowing rather than hiding: a UI that offers "hold a share on this device" to an ineligible
574
+ * member produces a create that the backend refuses at binding time, after the user has chosen.
575
+ *
576
+ * @throws {WaaskeyError} `unauthorized` — no member session is held.
577
+ */
578
+ async eligibility(signal) {
579
+ const self = this.held?.member;
580
+ if (!self) {
581
+ throw new WaaskeyError("Share-holder eligibility is a property of the logged-in member \u2014 call `members.loginWithFirebase(...)` first.", "unauthorized");
582
+ }
583
+ const eligible = await this.list({ canHoldShare: true }, signal);
584
+ return { canHoldShare: eligible.some((member) => member.id === self.id), eligible };
585
+ }
461
586
  };
462
587
  function mapFirebaseMemberError(error) {
463
588
  if (error instanceof WaaskeyError && error.status === 401) {
@@ -484,6 +609,54 @@ var Onramp = class {
484
609
  }
485
610
  };
486
611
 
612
+ // src/passkey/assertion.ts
613
+ function isPasskeyAssertionSupported() {
614
+ return typeof globalThis.navigator !== "undefined" && typeof globalThis.navigator.credentials !== "undefined";
615
+ }
616
+ async function defaultAssertionCeremony() {
617
+ let mod;
618
+ try {
619
+ mod = await import('@simplewebauthn/browser');
620
+ } catch {
621
+ throw new WaaskeyError('Passkey step-up requires "@simplewebauthn/browser" installed, or pass a custom ceremony.', "unsupported");
622
+ }
623
+ return {
624
+ get: (options) => mod.startAuthentication({ optionsJSON: options })
625
+ };
626
+ }
627
+ async function getSigningAssertion(challenge, options = {}) {
628
+ if (options.ceremony === void 0 && !isPasskeyAssertionSupported()) {
629
+ throw new WaaskeyError("Passkey step-up is not available in this runtime (no WebAuthn API).", "unsupported");
630
+ }
631
+ const ceremony = options.ceremony ?? await defaultAssertionCeremony();
632
+ const rpId = options.rpId ?? (typeof location === "undefined" ? void 0 : location.hostname);
633
+ const requestOptions = {
634
+ challenge,
635
+ userVerification: "required",
636
+ ...rpId ? { rpId } : {},
637
+ ...options.credentialId ? { allowCredentials: [{ id: options.credentialId, type: "public-key" }] } : {}
638
+ };
639
+ try {
640
+ return await ceremony.get(requestOptions);
641
+ } catch (cause) {
642
+ if (cause instanceof WaaskeyError) throw cause;
643
+ const msg = cause instanceof Error ? cause.message : String(cause);
644
+ if (/cancel|abort|not allowed|user gesture/i.test(msg)) {
645
+ throw new WaaskeyError("Passkey authentication was cancelled by the user.", "aborted", { cause });
646
+ }
647
+ throw new WaaskeyError("Passkey step-up assertion failed.", "unsupported", { cause });
648
+ }
649
+ }
650
+
651
+ // src/passkey/recovery-factor.ts
652
+ function passkeyFactorEnrollment(credentialId) {
653
+ return { type: "passkey", credential: credentialId };
654
+ }
655
+ async function passkeyFactorVerification(challenge, options = {}) {
656
+ const assertion = await getSigningAssertion(challenge, options);
657
+ return { type: "passkey", token: JSON.stringify(assertion) };
658
+ }
659
+
487
660
  // src/storage/crypto.ts
488
661
  var PBKDF2_ITERATIONS = 21e4;
489
662
  var PBKDF2_HASH = "SHA-256";
@@ -685,8 +858,12 @@ var Recovery = class {
685
858
  async function buildRecoveryRegistration(params) {
686
859
  const recoveryCode = params.recoveryCode ?? generateRecoveryCode();
687
860
  const backup = params.passkey ? await sealRecoveryBackup(params.share, recoveryCode, params.passkey) : { ciphertext: await sealWithPassword(recoveryCode, params.share), keyWraps: void 0 };
861
+ if (params.passkeyFactor && !params.passkey) {
862
+ throw new WaaskeyError("passkeyFactor requires `passkey` \u2014 a factor for a credential this backup cannot open would be a lock-out.", "validation");
863
+ }
864
+ const strongFactor = params.passkeyFactor ? passkeyFactorEnrollment(params.passkey.credentialId) : { type: "recovery_code", credential: await sha256Hex(recoveryCode) };
688
865
  const factors = [
689
- { type: "recovery_code", credential: await sha256Hex(recoveryCode) },
866
+ strongFactor,
690
867
  { type: "totp", credential: params.totpSecret },
691
868
  { type: "email_otp", credential: params.email },
692
869
  ...params.extraFactors ?? []
@@ -1033,6 +1210,259 @@ function assertPublicKey(actual, expected, walletId) {
1033
1210
  function serializeCompletedShare(completed) {
1034
1211
  return JSON.stringify({ keyShare: completed.keyShare, sharedPublicKey: completed.sharedPublicKey });
1035
1212
  }
1213
+ var SessionKeys = class {
1214
+ constructor(http, walletUrl) {
1215
+ this.http = http;
1216
+ this.walletUrl = walletUrl;
1217
+ }
1218
+ http;
1219
+ walletUrl;
1220
+ /**
1221
+ * Ask a user for a permission, and get back a link to send them to (#113).
1222
+ *
1223
+ * The keypair is generated HERE and only its public half is registered, so nothing secret ever
1224
+ * travels: not in the request, not in the link, not through the wallet. The private key in the
1225
+ * result is the app's to keep — there is no way to retrieve it later, by design.
1226
+ *
1227
+ * The link is where the user answers. Open it, or render it as a QR code for a wallet on another
1228
+ * device. Then wait for the answer with {@link waitForDecision}.
1229
+ *
1230
+ * @example
1231
+ * ```ts
1232
+ * const req = await waaskey.sessionKeys.request({
1233
+ * walletId, label: 'Dungeon Quest — one battle',
1234
+ * scope: { allowedContracts: [game], maxCalls: 50, periodSeconds: 3600, maxCallsPerPeriod: 10 },
1235
+ * expiresAt: new Date(Date.now() + 24 * 3600_000),
1236
+ * });
1237
+ * showQrCode(req.url);
1238
+ * const { status, key, narrowed } = await waaskey.sessionKeys.waitForDecision(req.sessionKeyId);
1239
+ * ```
1240
+ */
1241
+ async request(params, signal) {
1242
+ if (!this.walletUrl) {
1243
+ throw new WaaskeyError("Waaskey: `walletUrl` is required to request a permission \u2014 pass it to the client, pointing at the wallet that answers.", "validation");
1244
+ }
1245
+ const privateKey = generatePrivateKey();
1246
+ const publicKey = `0x${toHex(secp256k1.getPublicKey(hexToBytes(privateKey), true))}`;
1247
+ const key = await this.http.request(
1248
+ "POST",
1249
+ "/aa/session-keys",
1250
+ {
1251
+ walletId: params.walletId,
1252
+ publicKey,
1253
+ permissions: params.scope,
1254
+ expiresAt: toIso(params.expiresAt),
1255
+ label: params.label,
1256
+ requesterId: params.requesterId,
1257
+ paymasterPolicyId: params.paymasterPolicyId
1258
+ },
1259
+ signal
1260
+ );
1261
+ return { sessionKeyId: key.id, privateKey, publicKey, url: permissionUrl(this.walletUrl, key.id), status: key.grantStatus ?? "approved" };
1262
+ }
1263
+ /** One permission as it now stands — including whether its owner has answered yet. */
1264
+ get(sessionKeyId, signal) {
1265
+ return this.http.request("GET", `/aa/session-keys/${encodeURIComponent(sessionKeyId)}`, void 0, signal);
1266
+ }
1267
+ /**
1268
+ * The app that asked for a permission — who it says it is, and whether its domain proved it.
1269
+ *
1270
+ * A wallet rendering a consent screen needs this, and hand-rolling the call is how a client ends
1271
+ * up showing `name` without `status` beside it — which reads as verified whether or not it is.
1272
+ */
1273
+ requester(requesterId, signal) {
1274
+ return this.http.request("GET", `/aa/requesters/${encodeURIComponent(requesterId)}`, void 0, signal);
1275
+ }
1276
+ /**
1277
+ * Wait for the user to answer a permission request (#113).
1278
+ *
1279
+ * Polls, rather than waiting on a redirect back: a user who answers on their phone, or closes the
1280
+ * tab, or takes a minute to read the screen would otherwise strand the app with no answer at all.
1281
+ *
1282
+ * Returns as soon as the wallet's owner has decided — `declined` is an answer, not an error, and
1283
+ * is returned rather than thrown so the app can say something useful. A request that is never
1284
+ * answered times out, which IS an error: it is indistinguishable from a user who walked away.
1285
+ *
1286
+ * `narrowed` says the user accepted less than was asked (they may narrow, never widen), so an app
1287
+ * can adapt to the smaller scope instead of failing opaquely on the first call outside it.
1288
+ */
1289
+ async waitForDecision(sessionKeyId, options = {}) {
1290
+ const { timeoutMs = 5 * 6e4, pollMs = 2e3, signal, asked } = options;
1291
+ const deadline = Date.now() + timeoutMs;
1292
+ for (; ; ) {
1293
+ const key = await this.get(sessionKeyId, signal);
1294
+ const status = key.grantStatus ?? "approved";
1295
+ if (status !== "pending") {
1296
+ return { status, key, narrowed: asked ? wasNarrowed(asked, key) : false };
1297
+ }
1298
+ if (Date.now() + pollMs >= deadline) {
1299
+ throw new WaaskeyError(`Waaskey: the permission request was not answered within ${Math.round(timeoutMs / 1e3)}s.`, "permission_request_timeout", {
1300
+ details: { sessionKeyId }
1301
+ });
1302
+ }
1303
+ await delay2(pollMs, signal);
1304
+ }
1305
+ }
1306
+ /** The tenant's active session keys, newest first (optionally for one wallet). */
1307
+ list(walletId, signal) {
1308
+ const query = walletId ? `?walletId=${encodeURIComponent(walletId)}` : "";
1309
+ return this.http.request("GET", `/aa/session-keys${query}`, void 0, signal);
1310
+ }
1311
+ /**
1312
+ * Sign a call with a session key and submit it as a UserOperation.
1313
+ *
1314
+ * The op is prepared server-side first, because its gas and paymaster fields are part of what the
1315
+ * signature covers: signing a locally-guessed op would produce a signature for an operation the
1316
+ * bundler never sees. Those exact fields are then sent back with the signature, so what was
1317
+ * signed is what is submitted.
1318
+ *
1319
+ * @example
1320
+ * ```ts
1321
+ * const { userOpHash } = await waaskey.sessionKeys.send({
1322
+ * sessionKeyId, privateKey, // the granted permission
1323
+ * sender, chainId: 'evm:1', // the smart account it acts for
1324
+ * callData, contract, selector, valueWei: '0x0',
1325
+ * });
1326
+ * ```
1327
+ */
1328
+ async send(params, signal) {
1329
+ const prepared = await this.http.request(
1330
+ "POST",
1331
+ "/aa/user-ops/prepare",
1332
+ { sender: params.sender, callData: params.callData, chainId: params.chainId, nonce: params.nonce },
1333
+ signal
1334
+ );
1335
+ const signature = signUserOpHash(prepared.userOpHash, params.privateKey);
1336
+ try {
1337
+ return await this.http.request(
1338
+ "POST",
1339
+ `/aa/session-keys/${encodeURIComponent(params.sessionKeyId)}/send`,
1340
+ {
1341
+ sender: params.sender,
1342
+ nonce: prepared.nonce,
1343
+ callData: params.callData,
1344
+ signature,
1345
+ chainId: params.chainId,
1346
+ entryPoint: prepared.entryPoint,
1347
+ contract: params.contract,
1348
+ selector: params.selector,
1349
+ valueWei: params.valueWei,
1350
+ // Exactly the op that was signed — see the note above.
1351
+ prepared: {
1352
+ initCode: prepared.initCode,
1353
+ callGasLimit: prepared.callGasLimit,
1354
+ verificationGasLimit: prepared.verificationGasLimit,
1355
+ preVerificationGas: prepared.preVerificationGas,
1356
+ maxFeePerGas: prepared.maxFeePerGas,
1357
+ maxPriorityFeePerGas: prepared.maxPriorityFeePerGas,
1358
+ paymasterAndData: prepared.paymasterAndData
1359
+ }
1360
+ },
1361
+ signal
1362
+ );
1363
+ } catch (error) {
1364
+ throw asPermissionError(error);
1365
+ }
1366
+ }
1367
+ };
1368
+ function signUserOpHash(userOpHash, privateKey) {
1369
+ const hash = hexToBytes(userOpHash);
1370
+ if (hash.length !== 32) {
1371
+ throw new WaaskeyError("Waaskey: userOpHash must be 32 bytes.", "validation");
1372
+ }
1373
+ const prefix = new TextEncoder().encode("Ethereum Signed Message:\n32");
1374
+ const digest = keccak_256(concat(prefix, hash));
1375
+ const signature = secp256k1.sign(digest, hexToBytes(privateKey), { prehash: false, format: "recovered" });
1376
+ const [recovery] = signature;
1377
+ if (recovery === void 0) {
1378
+ throw new WaaskeyError("Waaskey: could not sign the userOpHash.", "sign_failed");
1379
+ }
1380
+ return `0x${toHex(signature.slice(1))}${(27 + recovery).toString(16).padStart(2, "0")}`;
1381
+ }
1382
+ function asPermissionError(error) {
1383
+ if (!(error instanceof WaaskeyError) || error.status !== 422) return error;
1384
+ const code = fromServerCode(error.details) ?? fromWording(error.message);
1385
+ return code ? new WaaskeyError(error.message, code, { status: error.status, details: error.details }) : error;
1386
+ }
1387
+ function fromServerCode(details) {
1388
+ const code = details?.code;
1389
+ if (typeof code !== "string") return void 0;
1390
+ const byCode = {
1391
+ expired: "permission_expired",
1392
+ revoked: "permission_revoked",
1393
+ scope_chain: "permission_scope",
1394
+ scope_contract: "permission_scope",
1395
+ scope_selector: "permission_scope",
1396
+ value_exceeded: "permission_value_exceeded",
1397
+ calls_exhausted: "permission_exhausted",
1398
+ // Both rate bounds mean the same thing to a caller: wait for the next period, do not re-request
1399
+ // the grant. The message says which one ran out.
1400
+ rate_calls_exhausted: "permission_rate_limited",
1401
+ rate_value_exceeded: "permission_rate_limited"
1402
+ };
1403
+ return byCode[code];
1404
+ }
1405
+ function fromWording(message) {
1406
+ const reason = message.toLowerCase();
1407
+ return reason.includes("expired") ? "permission_expired" : reason.includes("inactive") ? "permission_revoked" : reason.includes("maxcalls") ? "permission_exhausted" : reason.includes("value exceeds") ? "permission_value_exceeded" : reason.includes("allowlist") || reason.includes("not permitted") ? "permission_scope" : void 0;
1408
+ }
1409
+ function generatePrivateKey() {
1410
+ return `0x${toHex(secp256k1.utils.randomSecretKey())}`;
1411
+ }
1412
+ function permissionUrl(walletUrl, sessionKeyId) {
1413
+ const base = walletUrl.endsWith("/") ? walletUrl.slice(0, -1) : walletUrl;
1414
+ return `${base}/permissions/${encodeURIComponent(sessionKeyId)}`;
1415
+ }
1416
+ function toIso(when) {
1417
+ return typeof when === "string" ? when : when.toISOString();
1418
+ }
1419
+ function wasNarrowed(asked, key) {
1420
+ const got = key.permissions ?? {};
1421
+ const tighter = (before, after) => {
1422
+ if (after === void 0) return false;
1423
+ if (before === void 0) return true;
1424
+ return BigInt(after) < BigInt(before);
1425
+ };
1426
+ const shorter = (before, after) => (after?.length ?? 0) > 0 && (before?.length ?? 0) !== (after?.length ?? 0);
1427
+ return tighter(asked.maxCalls, got.maxCalls) || tighter(asked.maxValueWei, got.maxValueWei) || tighter(asked.maxCallsPerPeriod, got.maxCallsPerPeriod) || tighter(asked.maxValueWeiPerPeriod, got.maxValueWeiPerPeriod) || shorter(asked.allowedContracts, got.allowedContracts) || shorter(asked.allowedSelectors, got.allowedSelectors) || shorter(asked.allowedChains, got.allowedChains);
1428
+ }
1429
+ function delay2(ms, signal) {
1430
+ return new Promise((resolve, reject) => {
1431
+ if (signal?.aborted) {
1432
+ reject(new WaaskeyError("Waaskey: the permission request was aborted.", "aborted"));
1433
+ return;
1434
+ }
1435
+ const timer = setTimeout(() => {
1436
+ signal?.removeEventListener("abort", onAbort);
1437
+ resolve();
1438
+ }, ms);
1439
+ function onAbort() {
1440
+ clearTimeout(timer);
1441
+ reject(new WaaskeyError("Waaskey: the permission request was aborted.", "aborted"));
1442
+ }
1443
+ signal?.addEventListener("abort", onAbort, { once: true });
1444
+ });
1445
+ }
1446
+ function hexToBytes(hex) {
1447
+ const clean = hex.startsWith("0x") || hex.startsWith("0X") ? hex.slice(2) : hex;
1448
+ if (clean.length % 2 !== 0 || /[^0-9a-fA-F]/.test(clean)) {
1449
+ throw new WaaskeyError("Waaskey: expected a hex string.", "validation");
1450
+ }
1451
+ const bytes = new Uint8Array(clean.length / 2);
1452
+ for (let i = 0; i < bytes.length; i++) {
1453
+ bytes[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
1454
+ }
1455
+ return bytes;
1456
+ }
1457
+ function concat(a, b) {
1458
+ const out = new Uint8Array(a.length + b.length);
1459
+ out.set(a);
1460
+ out.set(b, a.length);
1461
+ return out;
1462
+ }
1463
+ function toHex(bytes) {
1464
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
1465
+ }
1036
1466
 
1037
1467
  // src/custody.ts
1038
1468
  var CUSTODY_KINDS = /* @__PURE__ */ new Set(["user_device", "user_backup", "platform_signer", "platform_recovery", "external_party"]);
@@ -1169,45 +1599,6 @@ function deserializeShare(blob) {
1169
1599
  };
1170
1600
  }
1171
1601
 
1172
- // src/passkey/assertion.ts
1173
- function isPasskeyAssertionSupported() {
1174
- return typeof globalThis.navigator !== "undefined" && typeof globalThis.navigator.credentials !== "undefined";
1175
- }
1176
- async function defaultAssertionCeremony() {
1177
- let mod;
1178
- try {
1179
- mod = await import('@simplewebauthn/browser');
1180
- } catch {
1181
- throw new WaaskeyError('Passkey step-up requires "@simplewebauthn/browser" installed, or pass a custom ceremony.', "unsupported");
1182
- }
1183
- return {
1184
- get: (options) => mod.startAuthentication({ optionsJSON: options })
1185
- };
1186
- }
1187
- async function getSigningAssertion(challenge, options = {}) {
1188
- if (options.ceremony === void 0 && !isPasskeyAssertionSupported()) {
1189
- throw new WaaskeyError("Passkey step-up is not available in this runtime (no WebAuthn API).", "unsupported");
1190
- }
1191
- const ceremony = options.ceremony ?? await defaultAssertionCeremony();
1192
- const rpId = options.rpId ?? (typeof location === "undefined" ? void 0 : location.hostname);
1193
- const requestOptions = {
1194
- challenge,
1195
- userVerification: "required",
1196
- ...rpId ? { rpId } : {},
1197
- ...options.credentialId ? { allowCredentials: [{ id: options.credentialId, type: "public-key" }] } : {}
1198
- };
1199
- try {
1200
- return await ceremony.get(requestOptions);
1201
- } catch (cause) {
1202
- if (cause instanceof WaaskeyError) throw cause;
1203
- const msg = cause instanceof Error ? cause.message : String(cause);
1204
- if (/cancel|abort|not allowed|user gesture/i.test(msg)) {
1205
- throw new WaaskeyError("Passkey authentication was cancelled by the user.", "aborted", { cause });
1206
- }
1207
- throw new WaaskeyError("Passkey step-up assertion failed.", "unsupported", { cause });
1208
- }
1209
- }
1210
-
1211
1602
  // src/wallet.ts
1212
1603
  var DEVICE_ROLE = "device";
1213
1604
  var Wallet = class {
@@ -1651,7 +2042,7 @@ var Wallets = class {
1651
2042
  if (curve === "ed25519") {
1652
2043
  await this.runEddsaKeygen(mpc, shareStore, created.id, ceremony, deviceEncKeypair);
1653
2044
  } else {
1654
- await this.runSecpKeygen(mpc, shareStore, primePool, created.id, ceremony, curve, options.backup, signal);
2045
+ await this.runSecpKeygen(mpc, shareStore, primePool, created.id, ceremony, curve, options.backup, signal, options.onRecoveryEnrolled);
1655
2046
  }
1656
2047
  this.deps.analytics?.track("wallet.created", { walletId: created.id, chain: params.chain, curve });
1657
2048
  if (options.waitForActive === false) {
@@ -1751,13 +2142,15 @@ var Wallets = class {
1751
2142
  }
1752
2143
  const { signal } = options;
1753
2144
  throwIfAborted(signal);
2145
+ const deviceId = await this.resolveDeviceId(options.deviceId);
2146
+ const query = deviceId ? `?deviceId=${encodeURIComponent(deviceId)}` : "";
1754
2147
  const [ceremony, shareholders] = await Promise.all([
1755
- this.http.requestAsMember("GET", `/v1/wallets/${walletId}/ceremony/mine`, void 0, signal),
2148
+ this.http.requestAsMember("GET", `/v1/wallets/${walletId}/ceremony/mine${query}`, void 0, signal),
1756
2149
  this.http.requestAsMember("GET", `/v1/wallets/${walletId}/shareholders`, void 0, signal)
1757
2150
  ]);
1758
2151
  const roles = buildMemberRoster(shareholders, ceremony.parties);
1759
2152
  throwIfAborted(signal);
1760
- const joined = await this.http.requestAsMember("POST", `/v1/wallets/${walletId}/ceremony/join`, void 0, signal);
2153
+ const joined = await this.http.requestAsMember("POST", `/v1/wallets/${walletId}/ceremony/join${query}`, void 0, signal);
1761
2154
  throwIfAborted(signal);
1762
2155
  let keygen;
1763
2156
  try {
@@ -1774,7 +2167,7 @@ var Wallets = class {
1774
2167
  if (cause instanceof WaaskeyError) throw cause;
1775
2168
  throw new WaaskeyError("The member device keygen ceremony failed.", "keygen_failed", { cause });
1776
2169
  }
1777
- await shareStore.put(memberShareKey(walletId, membershipIdFromRole(ceremony.role)), serializeShare(keygen));
2170
+ await shareStore.put(memberShareKey(walletId, membershipIdFromRole(ceremony.role), deviceId), serializeShare(keygen));
1778
2171
  return joined;
1779
2172
  }
1780
2173
  /**
@@ -1805,12 +2198,13 @@ var Wallets = class {
1805
2198
  const { signal } = options;
1806
2199
  throwIfAborted(signal);
1807
2200
  await this.http.requestAsMember("POST", `/v1/wallets/${walletId}/sign-requests/${reqId}/approve`, void 0, signal);
1808
- const ceremony = await this.waitUntilReady(walletId, reqId, options);
2201
+ const deviceId = await this.resolveDeviceId(options.deviceId);
2202
+ const ceremony = await this.waitUntilReady(walletId, reqId, options, deviceId);
1809
2203
  const membershipId = membershipIdFromRole(ceremony.role);
1810
- const blob = await shareStore.get(memberShareKey(walletId, membershipId));
2204
+ const blob = await shareStore.get(memberShareKey(walletId, membershipId, deviceId));
1811
2205
  if (!blob) {
1812
2206
  throw new WaaskeyError(`No stored device share for wallet "${walletId}" / member "${membershipId}" \u2014 this device never completed joinCeremony.`, "share_not_found", {
1813
- details: { walletId, membershipId }
2207
+ details: { walletId, membershipId, ...deviceId ? { deviceId } : {} }
1814
2208
  });
1815
2209
  }
1816
2210
  const { keyShare } = deserializeShare(blob);
@@ -1893,7 +2287,7 @@ var Wallets = class {
1893
2287
  * Any additional party the client cannot drive (a non-`user_backup` role) is refused up front — leaving
1894
2288
  * it unjoined would hang the whole ceremony, so failing loud beats silently deadlocking.
1895
2289
  */
1896
- async runSecpKeygen(mpc, shareStore, primePool, walletId, ceremony, curve, backup, signal) {
2290
+ async runSecpKeygen(mpc, shareStore, primePool, walletId, ceremony, curve, backup, signal, onEnrolled) {
1897
2291
  const extras = ceremony.additionalParties ?? [];
1898
2292
  const unsupported = extras.filter((party) => party.role !== USER_BACKUP_ROLE2);
1899
2293
  if (unsupported.length > 0) {
@@ -1905,10 +2299,15 @@ var Wallets = class {
1905
2299
  }
1906
2300
  const userBackupParty = extras.find((party) => party.role === USER_BACKUP_ROLE2);
1907
2301
  if (!userBackupParty) {
1908
- const pregeneratedPrimes = primePool ? await primePool.take(curve) : void 0;
1909
- throwIfAborted(signal);
1910
- const [keygen] = await this.runKeygenParties(mpc, [{ ...ceremony, curve, pregeneratedPrimes }]);
1911
- await shareStore.put(walletId, serializeShare(keygen, ceremony.relayUrl));
2302
+ const lease = primePool ? await primePool.borrow(curve) : void 0;
2303
+ try {
2304
+ throwIfAborted(signal);
2305
+ const [keygen] = await this.runKeygenParties(mpc, [{ ...ceremony, curve, pregeneratedPrimes: lease?.primes }]);
2306
+ lease?.consume();
2307
+ await shareStore.put(walletId, serializeShare(keygen, ceremony.relayUrl));
2308
+ } finally {
2309
+ await lease?.release();
2310
+ }
1912
2311
  return;
1913
2312
  }
1914
2313
  if (!backup) {
@@ -1917,15 +2316,42 @@ var Wallets = class {
1917
2316
  "validation"
1918
2317
  );
1919
2318
  }
1920
- const [devicePrimes, backupPrimes] = primePool ? await Promise.all([primePool.take(curve), primePool.take(curve)]) : [void 0, void 0];
2319
+ const [deviceLease, backupLease] = primePool ? await Promise.all([primePool.borrow(curve), primePool.borrow(curve)]) : [void 0, void 0];
2320
+ try {
2321
+ await this.runNonCustodialKeygen(
2322
+ mpc,
2323
+ shareStore,
2324
+ walletId,
2325
+ ceremony,
2326
+ userBackupParty,
2327
+ curve,
2328
+ backup,
2329
+ signal,
2330
+ deviceLease?.primes,
2331
+ backupLease?.primes,
2332
+ () => {
2333
+ deviceLease?.consume();
2334
+ backupLease?.consume();
2335
+ },
2336
+ onEnrolled
2337
+ );
2338
+ } finally {
2339
+ await Promise.all([deviceLease?.release(), backupLease?.release()]);
2340
+ }
2341
+ }
2342
+ /** The `[device, server, user_backup]` ceremony itself — see {@link runSecpKeygen} for why the primes are leased. */
2343
+ async runNonCustodialKeygen(mpc, shareStore, walletId, ceremony, userBackupParty, curve, backup, signal, devicePrimes, backupPrimes, onDerived, onEnrolled) {
1921
2344
  throwIfAborted(signal);
1922
2345
  const [deviceKeygen, backupKeygen] = await this.runKeygenParties(mpc, [
1923
2346
  { ...ceremony, curve, pregeneratedPrimes: devicePrimes },
1924
2347
  { ...userBackupParty, curve, pregeneratedPrimes: backupPrimes }
1925
2348
  ]);
2349
+ onDerived();
1926
2350
  await shareStore.put(walletId, serializeShare(deviceKeygen, ceremony.relayUrl));
1927
2351
  throwIfAborted(signal);
1928
- const { payload } = await buildRecoveryRegistration({ share: serializeShare(backupKeygen), ...backup });
2352
+ const enrolment = await resolveRecoveryEnrolment(backup);
2353
+ onEnrolled?.(enrolment.outcome);
2354
+ const { payload } = await buildRecoveryRegistration({ share: serializeShare(backupKeygen), ...backup, ...enrolment.params });
1929
2355
  await shareStore.put(userBackupPendingKey(walletId), JSON.stringify(payload));
1930
2356
  await this.registerBackup(walletId, payload, signal);
1931
2357
  await shareStore.remove(userBackupPendingKey(walletId));
@@ -2157,14 +2583,26 @@ var Wallets = class {
2157
2583
  }
2158
2584
  }
2159
2585
  /** Poll the member's own sign ceremony until the t-of-n quorum is fixed AND this member is selected, or throw on timeout. */
2160
- async waitUntilReady(walletId, reqId, options) {
2586
+ /**
2587
+ * The device id to act as: an explicit one, else the client's stored id, else none.
2588
+ *
2589
+ * `undefined` is a legitimate answer, not a failure — a single-device member and every existing
2590
+ * consumer built before per-device shares work exactly as they did, on the un-suffixed key and
2591
+ * the un-parameterised endpoints.
2592
+ */
2593
+ async resolveDeviceId(explicit) {
2594
+ if (explicit) return explicit;
2595
+ return this.deps.devices ? this.deps.devices.stored() : void 0;
2596
+ }
2597
+ async waitUntilReady(walletId, reqId, options, deviceId) {
2161
2598
  const timeoutMs = options.readyTimeoutMs ?? DEFAULT_ACTIVATION_TIMEOUT_MS;
2162
2599
  const intervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
2163
2600
  const { signal } = options;
2164
2601
  const deadline = Date.now() + timeoutMs;
2165
2602
  for (; ; ) {
2166
2603
  throwIfAborted(signal);
2167
- const ceremony = await this.http.requestAsMember("GET", `/v1/wallets/${walletId}/sign-requests/${reqId}/ceremony/mine`, void 0, signal);
2604
+ const query = deviceId ? `?deviceId=${encodeURIComponent(deviceId)}` : "";
2605
+ const ceremony = await this.http.requestAsMember("GET", `/v1/wallets/${walletId}/sign-requests/${reqId}/ceremony/mine${query}`, void 0, signal);
2168
2606
  if (isReadyMemberSignCeremony(ceremony)) return ceremony;
2169
2607
  if (Date.now() >= deadline) {
2170
2608
  throw new WaaskeyError("The signing quorum did not fix (or did not select this member) before the timeout.", "sign_ceremony_timeout", { details: { walletId, reqId } });
@@ -2182,8 +2620,9 @@ async function restoreUserBackupShare(backup, opener) {
2182
2620
  }
2183
2621
  return deserializeShare(blob).keyShare;
2184
2622
  }
2185
- function memberShareKey(walletId, membershipId) {
2186
- return `${walletId}@member-${membershipId}`;
2623
+ function memberShareKey(walletId, membershipId, deviceId) {
2624
+ const base = `${walletId}@member-${membershipId}`;
2625
+ return deviceId ? `${base}:${deviceId}` : base;
2187
2626
  }
2188
2627
  function userBackupPendingKey(walletId) {
2189
2628
  return `${walletId}:user_backup_pending`;
@@ -2298,6 +2737,26 @@ function readEddsaEncRoster(ceremony, encKeypair) {
2298
2737
  function isReadyMemberSignCeremony(ceremony) {
2299
2738
  return ceremony.ready && ceremony.quorumRoles !== void 0 && ceremony.signerPosition !== void 0 && ceremony.participants !== void 0 && ceremony.digest !== void 0;
2300
2739
  }
2740
+ async function resolveRecoveryEnrolment(backup) {
2741
+ if (backup.passkey) {
2742
+ return {
2743
+ params: { passkey: backup.passkey, passkeyFactor: backup.passkeyFactor ?? true },
2744
+ outcome: { strongFactor: backup.passkeyFactor === false ? "recovery_code" : "passkey" }
2745
+ };
2746
+ }
2747
+ if (!backup.passkeyEnroller) {
2748
+ return { params: {}, outcome: { strongFactor: "recovery_code", fallbackReason: "no_passkey_enroller" } };
2749
+ }
2750
+ try {
2751
+ const credential = await backup.passkeyEnroller();
2752
+ if (!credential) {
2753
+ return { params: {}, outcome: { strongFactor: "recovery_code", fallbackReason: "device_cannot_prf" } };
2754
+ }
2755
+ return { params: { passkey: credential, passkeyFactor: true }, outcome: { strongFactor: "passkey" } };
2756
+ } catch {
2757
+ return { params: {}, outcome: { strongFactor: "recovery_code", fallbackReason: "enrolment_failed" } };
2758
+ }
2759
+ }
2301
2760
 
2302
2761
  // src/client.ts
2303
2762
  var Waaskey = class {
@@ -2313,8 +2772,12 @@ var Waaskey = class {
2313
2772
  auth;
2314
2773
  /** The `members` resource — org-member (dashboard "plane B") bearer login for headless consumers. */
2315
2774
  members;
2775
+ /** The `devices` resource — this client as a member DEVICE that can hold an MPC share (#53). */
2776
+ devices;
2316
2777
  /** The `onramp` resource — fund the wallet with fiat via a provider on-ramp. */
2317
2778
  onramp;
2779
+ /** The `sessionKeys` resource — act with a delegated, scoped permission (#112). */
2780
+ sessionKeys;
2318
2781
  /** Default fetch used by the optional {@link broadcast} helper (from `WaaskeyOptions.fetch`). */
2319
2782
  defaultFetch;
2320
2783
  constructor(options) {
@@ -2329,11 +2792,13 @@ var Waaskey = class {
2329
2792
  this.auth = new Auth(http);
2330
2793
  this.members = new Members(http);
2331
2794
  http.useMemberAccessToken(() => this.members.accessToken);
2332
- this.wallets = new Wallets(http, { mpc: options.mpc, shareStore: options.shareStore, primePool: options.primePool, analytics });
2795
+ this.devices = new Devices(http, options.deviceStore);
2796
+ this.wallets = new Wallets(http, { mpc: options.mpc, shareStore: options.shareStore, primePool: options.primePool, analytics, devices: this.devices });
2333
2797
  this.recovery = new Recovery(http, { shareStore: options.shareStore, analytics });
2334
2798
  this.reshare = new Reshare(http, { mpc: options.mpc, shareStore: options.shareStore, analytics });
2335
2799
  this.balances = new Balances(options.chains, options.fetch);
2336
2800
  this.onramp = new Onramp(http);
2801
+ this.sessionKeys = new SessionKeys(http, options.walletUrl);
2337
2802
  this.defaultFetch = options.fetch;
2338
2803
  }
2339
2804
  /**
@@ -2359,13 +2824,66 @@ function resolveSink(analytics, http) {
2359
2824
  return analytics ?? new HttpAnalyticsSink(http);
2360
2825
  }
2361
2826
 
2362
- // src/passkey/recovery-factor.ts
2363
- function passkeyFactorEnrollment(credentialId) {
2364
- return { type: "passkey", credential: credentialId };
2827
+ // src/secret-envelope.ts
2828
+ var KEK_INFO2 = "waaskey-unlock-kek-v1";
2829
+ function newUnlockSecret() {
2830
+ return bytesToBase64(randomBytes(32));
2831
+ }
2832
+ async function sealSecret(secret, methods) {
2833
+ const wraps = [];
2834
+ if (methods.passphrase !== void 0) {
2835
+ const salt = freshSalt();
2836
+ wraps.push({ method: "passphrase", wrapped: await seal(await deriveKey(methods.passphrase, salt), secret), salt: bytesToBase64(salt) });
2837
+ }
2838
+ if (methods.passkey) {
2839
+ const salt = base64ToBytes(methods.passkey.salt);
2840
+ wraps.push({
2841
+ method: "passkey_prf",
2842
+ wrapped: await seal(await deriveKeyFromBytes(base64ToBytes(methods.passkey.secret), salt, KEK_INFO2), secret),
2843
+ salt: methods.passkey.salt,
2844
+ credentialId: methods.passkey.credentialId
2845
+ });
2846
+ }
2847
+ if (wraps.length === 0) {
2848
+ throw new WaaskeyError("An unlock envelope needs at least one method \u2014 a secret nothing can unwrap is unrecoverable.", "validation");
2849
+ }
2850
+ return { wraps };
2365
2851
  }
2366
- async function passkeyFactorVerification(challenge, options = {}) {
2367
- const assertion = await getSigningAssertion(challenge, options);
2368
- return { type: "passkey", token: JSON.stringify(assertion) };
2852
+ async function addMethod(envelope, secret, method) {
2853
+ const added = await sealSecret(secret, method);
2854
+ const kinds = new Set(added.wraps.map((wrap) => wrap.method));
2855
+ return { wraps: [...envelope.wraps.filter((wrap) => !kinds.has(wrap.method)), ...added.wraps] };
2856
+ }
2857
+ function removeMethod(envelope, method) {
2858
+ const wraps = envelope.wraps.filter((wrap) => wrap.method !== method);
2859
+ if (wraps.length === 0) {
2860
+ throw new WaaskeyError(`Removing '${method}' would leave nothing that can unlock this device \u2014 enrol another method first.`, "validation");
2861
+ }
2862
+ return { wraps };
2863
+ }
2864
+ async function openSecret(envelope, opener) {
2865
+ const prfWrap = envelope.wraps.find((wrap) => wrap.method === "passkey_prf");
2866
+ if (opener.passkeySecret !== void 0 && prfWrap) {
2867
+ const kek = await deriveKeyFromBytes(base64ToBytes(opener.passkeySecret), base64ToBytes(prfWrap.salt), KEK_INFO2);
2868
+ return unwrap(prfWrap, kek, "passkey");
2869
+ }
2870
+ const passphraseWrap = envelope.wraps.find((wrap) => wrap.method === "passphrase");
2871
+ if (opener.passphrase !== void 0 && passphraseWrap) {
2872
+ const kek = await deriveKey(opener.passphrase, base64ToBytes(passphraseWrap.salt));
2873
+ return unwrap(passphraseWrap, kek, "passphrase");
2874
+ }
2875
+ const enrolled = envelope.wraps.map((wrap) => wrap.method).join(", ") || "none";
2876
+ throw new WaaskeyError(`Nothing supplied can unlock this device \u2014 it accepts [${enrolled}].`, "unauthorized");
2877
+ }
2878
+ function enrolledMethods(envelope) {
2879
+ return envelope.wraps.map((wrap) => wrap.method);
2880
+ }
2881
+ async function unwrap(wrap, kek, label) {
2882
+ try {
2883
+ return await open(kek, wrap.wrapped);
2884
+ } catch (cause) {
2885
+ throw new WaaskeyError(`Could not unlock with the ${label} \u2014 wrong ${label}?`, "unauthorized", { cause });
2886
+ }
2369
2887
  }
2370
2888
 
2371
2889
  // src/mpc/wasm-core.ts
@@ -2799,11 +3317,13 @@ var PrimePool = class {
2799
3317
  this.store = options.store ?? new MemoryPrimeStore();
2800
3318
  this.targetSize = Math.max(1, options.targetSize ?? 2);
2801
3319
  this.autoRefill = options.autoRefill ?? false;
3320
+ this.concurrency = Math.max(1, options.concurrency ?? 1);
2802
3321
  }
2803
3322
  core;
2804
3323
  store;
2805
3324
  targetSize;
2806
3325
  autoRefill;
3326
+ concurrency;
2807
3327
  /** Per-curve in-flight refill, so concurrent calls don't over-generate. */
2808
3328
  refilling = /* @__PURE__ */ new Map();
2809
3329
  /**
@@ -2818,8 +3338,10 @@ var PrimePool = class {
2818
3338
  return task;
2819
3339
  }
2820
3340
  async refill(curve) {
2821
- while (await this.store.size(curve) < this.targetSize) {
2822
- await this.store.add(curve, await this.core.pregeneratePrimes(curve));
3341
+ for (let missing = this.targetSize - await this.store.size(curve); missing > 0; missing = this.targetSize - await this.store.size(curve)) {
3342
+ const batch = Math.min(missing, this.concurrency);
3343
+ const generated = await Promise.all(Array.from({ length: batch }, () => this.core.pregeneratePrimes(curve)));
3344
+ for (const primes of generated) await this.store.add(curve, primes);
2823
3345
  }
2824
3346
  }
2825
3347
  /**
@@ -2834,6 +3356,28 @@ var PrimePool = class {
2834
3356
  if (this.autoRefill) void this.ensure(curve).catch(() => void 0);
2835
3357
  return primes;
2836
3358
  }
3359
+ /**
3360
+ * Claim primes as a LEASE, so a failed ceremony gives them back (#83).
3361
+ *
3362
+ * Prefer this over {@link take} anywhere the primes feed a ceremony that can fail: `take` hands
3363
+ * them over unconditionally, and a keygen that dies on a relay timeout then costs the next
3364
+ * attempt a full generation it did not need to pay.
3365
+ */
3366
+ async borrow(curve) {
3367
+ const primes = await this.take(curve);
3368
+ let settled = false;
3369
+ return {
3370
+ primes,
3371
+ consume: () => {
3372
+ settled = true;
3373
+ },
3374
+ release: async () => {
3375
+ if (settled) return;
3376
+ settled = true;
3377
+ await this.store.add(curve, primes);
3378
+ }
3379
+ };
3380
+ }
2837
3381
  };
2838
3382
 
2839
3383
  // src/storage/indexeddb-store.ts
@@ -3131,6 +3675,6 @@ function prfOutputToSecret(prfResult) {
3131
3675
  return btoa(binary);
3132
3676
  }
3133
3677
 
3134
- export { Analytics, Auth, Balances, CLIENT_WASM_VERSION, EncryptedShareStore, EvmRpcProvider, HttpAnalyticsSink, IndexedDbKeyValueStore, Members, MemoryKeyValueStore, MemoryPrimeStore, Onramp, PasskeyPrfSecretProvider, PrimePool, Recovery, Reshare, Waaskey, WaaskeyError, Wallet, Wallets, WasmMpcCore, broadcast, createVerifiedClientWasmLoader, epochShareKey, formatUnits, generateRecoveryCode, getSigningAssertion, isNonCustodial, isPasskeyAssertionSupported, isPasskeySupported, isPrfSupported, loadClientWasm, memberShareKey, openRecoveryBackup, passkeyFactorEnrollment, passkeyFactorVerification, sealRecoveryBackup, userBackupPendingKey, validateCustodyPolicy, verifyWasmIntegrity };
3678
+ export { Analytics, Auth, Balances, CLIENT_WASM_VERSION, Devices, EncryptedShareStore, EvmRpcProvider, HttpAnalyticsSink, IndexedDbKeyValueStore, Members, MemoryKeyValueStore, MemoryPrimeStore, Onramp, PasskeyPrfSecretProvider, PrimePool, Recovery, Reshare, SessionKeys, Waaskey, WaaskeyError, Wallet, Wallets, WasmMpcCore, addMethod, broadcast, createVerifiedClientWasmLoader, enrolledMethods, epochShareKey, formatUnits, generateRecoveryCode, getSigningAssertion, isNonCustodial, isPasskeyAssertionSupported, isPasskeySupported, isPrfSupported, loadClientWasm, memberShareKey, newUnlockSecret, openRecoveryBackup, openSecret, passkeyFactorEnrollment, passkeyFactorVerification, removeMethod, sealRecoveryBackup, sealSecret, signUserOpHash, userBackupPendingKey, validateCustodyPolicy, verifyWasmIntegrity };
3135
3679
  //# sourceMappingURL=index.js.map
3136
3680
  //# sourceMappingURL=index.js.map