@waaskey/sdk 0.3.2 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -517,6 +517,17 @@ async function deriveKey(secret, salt) {
517
517
  "decrypt"
518
518
  ]);
519
519
  }
520
+ async function deriveKeyFromBytes(material, salt, info) {
521
+ const s = subtle();
522
+ const baseKey = await s.importKey("raw", material, "HKDF", false, ["deriveKey"]);
523
+ return s.deriveKey(
524
+ { name: "HKDF", hash: "SHA-256", salt, info: new TextEncoder().encode(info) },
525
+ baseKey,
526
+ { name: "AES-GCM", length: AES_KEY_BITS },
527
+ false,
528
+ ["encrypt", "decrypt"]
529
+ );
530
+ }
520
531
  async function seal(key, plaintext) {
521
532
  const iv = randomBytes(IV_BYTES);
522
533
  const data = new TextEncoder().encode(plaintext);
@@ -559,6 +570,60 @@ function base64ToBytes(base64) {
559
570
  return bytes;
560
571
  }
561
572
 
573
+ // src/recovery-envelope.ts
574
+ var KEK_INFO = "waaskey-backup-kek-v1";
575
+ var DEK_INFO = "waaskey-backup-dek-v1";
576
+ async function sealRecoveryBackup(share, recoveryCode, passkey) {
577
+ const dataKeyBytes = randomBytes(32);
578
+ const dataKey = await deriveKeyFromBytes(dataKeyBytes, new Uint8Array(0), DEK_INFO);
579
+ const ciphertext = await seal(dataKey, share);
580
+ const codeSalt = freshSalt();
581
+ const keyWraps = [
582
+ {
583
+ method: "passphrase",
584
+ wrapped: await seal(await deriveKey(recoveryCode, codeSalt), bytesToBase64(dataKeyBytes)),
585
+ salt: bytesToBase64(codeSalt)
586
+ }
587
+ ];
588
+ if (passkey) {
589
+ const prfSalt = base64ToBytes(passkey.salt);
590
+ keyWraps.push({
591
+ method: "passkey_prf",
592
+ wrapped: await seal(await deriveKeyFromBytes(base64ToBytes(passkey.secret), prfSalt, KEK_INFO), bytesToBase64(dataKeyBytes)),
593
+ salt: passkey.salt,
594
+ credentialId: passkey.credentialId
595
+ });
596
+ }
597
+ return { ciphertext, keyWraps };
598
+ }
599
+ async function openRecoveryBackup(envelope, opener) {
600
+ const prfWrap = envelope.keyWraps.find((w) => w.method === "passkey_prf");
601
+ if (opener.passkeySecret !== void 0 && prfWrap) {
602
+ const kek = await deriveKeyFromBytes(base64ToBytes(opener.passkeySecret), base64ToBytes(prfWrap.salt), KEK_INFO);
603
+ return openWith(envelope.ciphertext, prfWrap, kek, "passkey");
604
+ }
605
+ const codeWrap = envelope.keyWraps.find((w) => w.method === "passphrase");
606
+ if (opener.recoveryCode !== void 0 && codeWrap) {
607
+ const kek = await deriveKey(opener.recoveryCode, base64ToBytes(codeWrap.salt));
608
+ return openWith(envelope.ciphertext, codeWrap, kek, "recovery code");
609
+ }
610
+ const enrolled = envelope.keyWraps.map((w) => w.method).join(", ") || "none";
611
+ throw new WaaskeyError(`No key available to open this backup \u2014 it is wrapped for [${enrolled}] and none of those was supplied.`, "recovery_failed");
612
+ }
613
+ async function openWith(ciphertext, wrap, kek, label) {
614
+ let dataKeyBytes;
615
+ try {
616
+ dataKeyBytes = base64ToBytes(await open(kek, wrap.wrapped));
617
+ } catch (cause) {
618
+ throw new WaaskeyError(`Could not unwrap the backup key with the ${label} \u2014 wrong ${label}?`, "recovery_failed", { cause });
619
+ }
620
+ try {
621
+ return await open(await deriveKeyFromBytes(dataKeyBytes, new Uint8Array(0), DEK_INFO), ciphertext);
622
+ } catch (cause) {
623
+ throw new WaaskeyError("The backup key does not open this backup \u2014 the stored ciphertext and key wrap do not belong together.", "recovery_failed", { cause });
624
+ }
625
+ }
626
+
562
627
  // src/recovery.ts
563
628
  var MIN_FACTORS = 3;
564
629
  var Recovery = class {
@@ -596,7 +661,7 @@ var Recovery = class {
596
661
  { challengeId: params.challengeId, verifications: await hashRecoveryFactors(params.verifications) },
597
662
  options.signal
598
663
  );
599
- const share = await this.decrypt(res.ciphertext, params.recoveryCode);
664
+ const share = await this.decrypt(res, params);
600
665
  await this.deps.shareStore?.put(walletId, share);
601
666
  this.deps.analytics?.track("wallet.recovered", { walletId });
602
667
  return { share, refreshedAt: res.refreshedAt };
@@ -611,20 +676,15 @@ var Recovery = class {
611
676
  * device-loss so a leaked backup can't be replayed against a still-valid old share.
612
677
  */
613
678
  async retrieveShare(walletId, params, options = {}) {
614
- const ciphertext = await fetchRecoveryCiphertext(this.http, walletId, params, options.signal);
615
- return this.decrypt(ciphertext, params.recoveryCode);
679
+ return this.decrypt(await fetchRecoveryBackup(this.http, walletId, params, options.signal), params);
616
680
  }
617
- async decrypt(ciphertext, recoveryCode) {
618
- try {
619
- return await openWithPassword(recoveryCode, ciphertext);
620
- } catch (cause) {
621
- throw new WaaskeyError("Could not decrypt the recovery share \u2014 wrong recovery code?", "recovery_failed", { cause });
622
- }
681
+ decrypt(backup, params) {
682
+ return openRecoveryBlob(backup, params);
623
683
  }
624
684
  };
625
685
  async function buildRecoveryRegistration(params) {
626
686
  const recoveryCode = params.recoveryCode ?? generateRecoveryCode();
627
- const ciphertext = await sealWithPassword(recoveryCode, params.share);
687
+ const backup = params.passkey ? await sealRecoveryBackup(params.share, recoveryCode, params.passkey) : { ciphertext: await sealWithPassword(recoveryCode, params.share), keyWraps: void 0 };
628
688
  const factors = [
629
689
  { type: "recovery_code", credential: await sha256Hex(recoveryCode) },
630
690
  { type: "totp", credential: params.totpSecret },
@@ -634,19 +694,31 @@ async function buildRecoveryRegistration(params) {
634
694
  if (factors.length < MIN_FACTORS) {
635
695
  throw new WaaskeyError(`Recovery requires at least ${MIN_FACTORS} factors.`, "validation");
636
696
  }
637
- return { recoveryCode, payload: { ciphertext, factors } };
697
+ return { recoveryCode, payload: { ciphertext: backup.ciphertext, factors, ...backup.keyWraps ? { keyWraps: backup.keyWraps } : {} } };
638
698
  }
639
699
  function postRecoveryRegistration(http, walletId, payload, signal) {
640
700
  return http.request("POST", `/v1/wallets/${walletId}/recovery/register`, payload, signal);
641
701
  }
642
- async function fetchRecoveryCiphertext(http, walletId, params, signal) {
643
- const res = await http.request(
702
+ async function fetchRecoveryBackup(http, walletId, params, signal) {
703
+ return http.request(
644
704
  "POST",
645
705
  `/v1/wallets/${walletId}/recovery/verify`,
646
706
  { challengeId: params.challengeId, verifications: await hashRecoveryFactors(params.verifications) },
647
707
  signal
648
708
  );
649
- return res.ciphertext;
709
+ }
710
+ async function openRecoveryBlob(backup, opener) {
711
+ if (backup.keyWraps?.length) {
712
+ return openRecoveryBackup({ ciphertext: backup.ciphertext, keyWraps: backup.keyWraps }, opener);
713
+ }
714
+ if (opener.recoveryCode === void 0) {
715
+ throw new WaaskeyError("This backup predates passkey wrapping and can only be opened with the recovery code.", "recovery_failed");
716
+ }
717
+ try {
718
+ return await openWithPassword(opener.recoveryCode, backup.ciphertext);
719
+ } catch (cause) {
720
+ throw new WaaskeyError("Could not decrypt the recovery share \u2014 wrong recovery code?", "recovery_failed", { cause });
721
+ }
650
722
  }
651
723
  async function registerRecoveryShare(http, walletId, params, signal) {
652
724
  const { recoveryCode, payload } = await buildRecoveryRegistration(params);
@@ -692,6 +764,47 @@ function toMpcCurve(curve) {
692
764
  }
693
765
  return curve;
694
766
  }
767
+ var DEVICE_ENC_STORE_KEY = "__waaskey_device_enc_v1__";
768
+ async function getOrCreateDeviceEncKeypair(shareStore) {
769
+ const existing = await shareStore.get(DEVICE_ENC_STORE_KEY);
770
+ if (existing !== null) return parseDeviceEncKeypair(existing);
771
+ const keypair = generateDeviceEncKeypair();
772
+ await shareStore.put(DEVICE_ENC_STORE_KEY, JSON.stringify(keypair));
773
+ return keypair;
774
+ }
775
+ function generateDeviceEncKeypair() {
776
+ const secret = x25519.utils.randomSecretKey();
777
+ const publicKey = x25519.getPublicKey(secret);
778
+ if (isAllZero(publicKey)) {
779
+ throw new WaaskeyError("Generated an all-zero X25519 encryption public key \u2014 refusing to seal keygen packages to it.", "keygen_failed");
780
+ }
781
+ return { secretHex: bytesToHex(secret), publicHex: bytesToHex(publicKey) };
782
+ }
783
+ function parseDeviceEncKeypair(blob) {
784
+ let parsed;
785
+ try {
786
+ parsed = JSON.parse(blob);
787
+ } catch (cause) {
788
+ throw new WaaskeyError("Stored device encryption keypair is corrupt \u2014 it is not valid JSON.", "keygen_failed", { cause });
789
+ }
790
+ if (!isHex32(parsed.secretHex) || !isHex32(parsed.publicHex)) {
791
+ throw new WaaskeyError("Stored device encryption keypair is malformed \u2014 expected 32-byte (64 hex) secret + public keys.", "keygen_failed", {
792
+ details: { keys: Object.keys(parsed) }
793
+ });
794
+ }
795
+ return { secretHex: parsed.secretHex, publicHex: parsed.publicHex };
796
+ }
797
+ function isHex32(v) {
798
+ return typeof v === "string" && /^[0-9a-fA-F]{64}$/.test(v);
799
+ }
800
+ function isAllZero(bytes) {
801
+ return bytes.every((byte) => byte === 0);
802
+ }
803
+ function bytesToHex(bytes) {
804
+ let hex = "";
805
+ for (const byte of bytes) hex += byte.toString(16).padStart(2, "0");
806
+ return hex;
807
+ }
695
808
 
696
809
  // src/internal.ts
697
810
  function throwIfAborted(signal) {
@@ -717,6 +830,7 @@ function delay(ms, signal) {
717
830
  }
718
831
 
719
832
  // src/reshare.ts
833
+ var USER_BACKUP_ROLE = "user_backup";
720
834
  var Reshare = class {
721
835
  constructor(http, deps = {}) {
722
836
  this.http = http;
@@ -724,6 +838,116 @@ var Reshare = class {
724
838
  }
725
839
  http;
726
840
  deps;
841
+ /**
842
+ * Rotate the wallet onto a NEW committee, keeping its address, and drive this device's whole half of
843
+ * the ceremony (#488).
844
+ *
845
+ * The client is not a bystander here: the platform holds ONE share and cannot reach the threshold, so
846
+ * without this device's deal there is no ceremony at all.
847
+ *
848
+ * 1. Open the ceremony with the committee asked for (an empty request re-issues the current one).
849
+ * 2. DEAL from this device's current-epoch share — pure and local; every sub-share leaves sealed.
850
+ * 3. For each CLIENT-held new holder — identified by custody KIND, never by role name — fetch its
851
+ * material and assemble its new-epoch core locally, refusing any share that reproduces a different
852
+ * public key.
853
+ * 4. Acknowledge each one. The `user_backup` holder's ack carries its new share re-sealed under the
854
+ * user's EXISTING recovery code, so the wallet's no-lock-out property survives the rotation; the
855
+ * last acknowledgement drives the backend's atomic cutover.
856
+ *
857
+ * The assembled cores are BARE (no aux): the wallet is `reshareAuxPending` until aux-completion, which
858
+ * is {@link complete}'s job — and which re-assembles from the material itself, so nothing here needs
859
+ * to be kept on the device between the two. Nothing here can leave the device worse off — the old-epoch share is
860
+ * untouched, and a ceremony that fails cuts nothing over.
861
+ */
862
+ async rotate(walletId, params = {}, options = {}) {
863
+ const { mpc, shareStore } = this.deps;
864
+ if (!mpc || !shareStore) {
865
+ throw new WaaskeyError("Driving a reshare requires a device MPC core and a share store \u2014 pass `mpc` and `shareStore` to `new Waaskey(...)`.", "device_core_required");
866
+ }
867
+ if (!mpc.runReshareDeal || !mpc.runReshareAssemble) {
868
+ throw new WaaskeyError("The configured MPC core does not support reshare \u2014 use a client-wasm built with the `reshare` feature.", "unsupported");
869
+ }
870
+ const { signal } = options;
871
+ throwIfAborted(signal);
872
+ const wallet = await this.http.request("GET", `/v1/wallets/${walletId}`, void 0, signal);
873
+ if (!wallet.publicKey) {
874
+ throw new WaaskeyError("The wallet has no public key yet \u2014 only an ACTIVE wallet can be reshared.", "reshare_failed", { details: { walletId } });
875
+ }
876
+ const share = await shareStore.get(epochShareKey(walletId, wallet.keyEpoch));
877
+ if (!share) {
878
+ throw new WaaskeyError(`This device holds no share for wallet "${walletId}" at epoch ${wallet.keyEpoch} \u2014 it cannot deal the reshare.`, "reshare_failed");
879
+ }
880
+ throwIfAborted(signal);
881
+ const ceremony = await this.http.request("POST", `/v1/wallets/${walletId}/reshare-embedded`, params, signal);
882
+ const encKeypair = await getOrCreateDeviceEncKeypair(shareStore);
883
+ const curve = toMpcCurve(ceremony.curve);
884
+ const clientHolders = ceremony.newParties.filter((_role, i) => isClientCustody(ceremony.newCustodyKinds[i]));
885
+ try {
886
+ throwIfAborted(signal);
887
+ const dealt = await mpc.runReshareDeal({
888
+ curve,
889
+ share,
890
+ quorumIndices: ceremony.quorumIndices,
891
+ newPreimages: ceremony.newPreimages,
892
+ newThreshold: ceremony.newThreshold,
893
+ recipientPubkeys: ceremony.recipientPubkeys
894
+ });
895
+ await this.http.request(
896
+ "POST",
897
+ `/v1/wallets/${walletId}/reshare-drop/${ceremony.id}/deal`,
898
+ { commitments: dealt.commitments, sealedSubShares: dealt.sealedSubShares },
899
+ signal
900
+ );
901
+ let { status } = ceremony;
902
+ for (const role of clientHolders) {
903
+ throwIfAborted(signal);
904
+ const material = await this.http.request(
905
+ "GET",
906
+ `/v1/wallets/${walletId}/reshare-drop/${ceremony.id}/assembly?role=${encodeURIComponent(role)}`,
907
+ void 0,
908
+ signal
909
+ );
910
+ const assembled = await mpc.runReshareAssemble({
911
+ curve,
912
+ newPosition: material.newPosition,
913
+ newPreimages: material.newPreimages,
914
+ newThreshold: material.newThreshold,
915
+ wallet: material.wallet,
916
+ commitments: material.commitments,
917
+ subShares: material.subShares,
918
+ encryptionSecret: encKeypair.secretHex
919
+ });
920
+ assertPublicKey(assembled.sharedPublicKey, wallet.publicKey, walletId);
921
+ const body = role === USER_BACKUP_ROLE ? { backupCiphertext: await this.sealBackup(assembled.core, params.recoveryCode) } : {};
922
+ const ack = await this.http.request(
923
+ "POST",
924
+ `/v1/wallets/${walletId}/reshare-drop/${ceremony.id}/assembled?role=${encodeURIComponent(role)}`,
925
+ body,
926
+ signal
927
+ );
928
+ ({ status } = ack);
929
+ }
930
+ this.deps.analytics?.track("wallet.reshared", { walletId, curve: ceremony.curve });
931
+ return { walletId, ceremonyId: ceremony.id, status, keyEpoch: ceremony.keyEpoch, assembledRoles: clientHolders, sharedPublicKey: wallet.publicKey };
932
+ } catch (cause) {
933
+ if (cause instanceof WaaskeyError) throw cause;
934
+ throw new WaaskeyError("The reshare ceremony failed.", "reshare_failed", { cause });
935
+ }
936
+ }
937
+ /**
938
+ * Re-seal the new `user_backup` core under the user's EXISTING recovery code. The code never leaves
939
+ * the device — only the ciphertext does — which is the same trust model registration has, and the
940
+ * reason the server cannot check WHICH code sealed it.
941
+ */
942
+ async sealBackup(core, recoveryCode) {
943
+ if (!recoveryCode) {
944
+ throw new WaaskeyError(
945
+ "This committee keeps a `user_backup` holder, so its new share must be re-sealed under the wallet existing recovery code \u2014 pass `recoveryCode` to `reshare.rotate(...)`.",
946
+ "reshare_failed"
947
+ );
948
+ }
949
+ return sealWithPassword(recoveryCode, core);
950
+ }
727
951
  /**
728
952
  * Complete this device's share for a device-retaining reshare and make the wallet signable on the
729
953
  * device under the new epoch.
@@ -754,6 +978,7 @@ var Reshare = class {
754
978
  if (!wallet.publicKey) {
755
979
  throw new WaaskeyError("The wallet has no public key yet \u2014 it must be an ACTIVE reshared wallet to complete on device.", "reshare_failed", { details: { walletId } });
756
980
  }
981
+ const encKeypair = await getOrCreateDeviceEncKeypair(shareStore);
757
982
  throwIfAborted(signal);
758
983
  let completed;
759
984
  try {
@@ -764,7 +989,8 @@ var Reshare = class {
764
989
  newThreshold: material.newThreshold,
765
990
  wallet: material.wallet,
766
991
  commitments: material.commitments,
767
- subShares: material.subShares
992
+ subShares: material.subShares,
993
+ encryptionSecret: encKeypair.secretHex
768
994
  });
769
995
  assertPublicKey(assembled.sharedPublicKey, wallet.publicKey, walletId);
770
996
  throwIfAborted(signal);
@@ -794,6 +1020,9 @@ var Reshare = class {
794
1020
  function epochShareKey(walletId, keyEpoch) {
795
1021
  return keyEpoch <= 1 ? walletId : `${walletId}@epoch-${keyEpoch}`;
796
1022
  }
1023
+ function isClientCustody(kind) {
1024
+ return kind !== void 0 && kind !== "platform_signer" && kind !== "platform_recovery";
1025
+ }
797
1026
  function assertPublicKey(actual, expected, walletId) {
798
1027
  if (actual !== expected) {
799
1028
  throw new WaaskeyError("The reshared share reproduced a DIFFERENT public key than the wallet \u2014 aborting (device share not stored).", "reshare_pubkey_mismatch", {
@@ -868,47 +1097,6 @@ function deserializeEddsaShare(blob) {
868
1097
  }
869
1098
  return { keyPackage: parsed.keyPackage, publicKeyPackage: parsed.publicKeyPackage };
870
1099
  }
871
- var DEVICE_ENC_STORE_KEY = "__waaskey_device_enc_v1__";
872
- async function getOrCreateDeviceEncKeypair(shareStore) {
873
- const existing = await shareStore.get(DEVICE_ENC_STORE_KEY);
874
- if (existing !== null) return parseDeviceEncKeypair(existing);
875
- const keypair = generateDeviceEncKeypair();
876
- await shareStore.put(DEVICE_ENC_STORE_KEY, JSON.stringify(keypair));
877
- return keypair;
878
- }
879
- function generateDeviceEncKeypair() {
880
- const secret = x25519.utils.randomSecretKey();
881
- const publicKey = x25519.getPublicKey(secret);
882
- if (isAllZero(publicKey)) {
883
- throw new WaaskeyError("Generated an all-zero X25519 encryption public key \u2014 refusing to seal keygen packages to it.", "keygen_failed");
884
- }
885
- return { secretHex: bytesToHex(secret), publicHex: bytesToHex(publicKey) };
886
- }
887
- function parseDeviceEncKeypair(blob) {
888
- let parsed;
889
- try {
890
- parsed = JSON.parse(blob);
891
- } catch (cause) {
892
- throw new WaaskeyError("Stored device encryption keypair is corrupt \u2014 it is not valid JSON.", "keygen_failed", { cause });
893
- }
894
- if (!isHex32(parsed.secretHex) || !isHex32(parsed.publicHex)) {
895
- throw new WaaskeyError("Stored device encryption keypair is malformed \u2014 expected 32-byte (64 hex) secret + public keys.", "keygen_failed", {
896
- details: { keys: Object.keys(parsed) }
897
- });
898
- }
899
- return { secretHex: parsed.secretHex, publicHex: parsed.publicHex };
900
- }
901
- function isHex32(v) {
902
- return typeof v === "string" && /^[0-9a-fA-F]{64}$/.test(v);
903
- }
904
- function isAllZero(bytes) {
905
- return bytes.every((byte) => byte === 0);
906
- }
907
- function bytesToHex(bytes) {
908
- let hex = "";
909
- for (const byte of bytes) hex += byte.toString(16).padStart(2, "0");
910
- return hex;
911
- }
912
1100
 
913
1101
  // src/session-sign.ts
914
1102
  function toDeviceSignParams(ceremony, share, digest) {
@@ -1395,7 +1583,7 @@ function normalizeDigest(digest) {
1395
1583
  // src/wallets.ts
1396
1584
  var BACKUP_REGISTER_ATTEMPTS = 3;
1397
1585
  var BACKUP_REGISTER_BACKOFF_MS = 200;
1398
- var USER_BACKUP_ROLE = "user_backup";
1586
+ var USER_BACKUP_ROLE2 = "user_backup";
1399
1587
  var DEFAULT_ACTIVATION_TIMEOUT_MS = 6e4;
1400
1588
  var DEFAULT_POLL_INTERVAL_MS = 1e3;
1401
1589
  var Wallets = class {
@@ -1707,7 +1895,7 @@ var Wallets = class {
1707
1895
  */
1708
1896
  async runSecpKeygen(mpc, shareStore, primePool, walletId, ceremony, curve, backup, signal) {
1709
1897
  const extras = ceremony.additionalParties ?? [];
1710
- const unsupported = extras.filter((party) => party.role !== USER_BACKUP_ROLE);
1898
+ const unsupported = extras.filter((party) => party.role !== USER_BACKUP_ROLE2);
1711
1899
  if (unsupported.length > 0) {
1712
1900
  throw new WaaskeyError(
1713
1901
  `The keygen ceremony carries an additional client party this SDK cannot drive (${unsupported.map((party) => party.role).join(", ")}) \u2014 refusing to keygen, since leaving a party unjoined would hang the ceremony.`,
@@ -1715,7 +1903,7 @@ var Wallets = class {
1715
1903
  { details: { roles: unsupported.map((party) => party.role) } }
1716
1904
  );
1717
1905
  }
1718
- const userBackupParty = extras.find((party) => party.role === USER_BACKUP_ROLE);
1906
+ const userBackupParty = extras.find((party) => party.role === USER_BACKUP_ROLE2);
1719
1907
  if (!userBackupParty) {
1720
1908
  const pregeneratedPrimes = primePool ? await primePool.take(curve) : void 0;
1721
1909
  throwIfAborted(signal);
@@ -1835,8 +2023,8 @@ var Wallets = class {
1835
2023
  const { signal } = options;
1836
2024
  throwIfAborted(signal);
1837
2025
  const digest = normalizeDigest(params.digest);
1838
- const ciphertext = await this.fetchUserBackupCiphertext(walletId, params, signal);
1839
- const share = await restoreUserBackupShare(ciphertext, params.recoveryCode);
2026
+ const backup = await this.fetchUserBackup(walletId, params, signal);
2027
+ const share = await restoreUserBackupShare(backup, params);
1840
2028
  throwIfAborted(signal);
1841
2029
  const body = { digest };
1842
2030
  if (params.chainId !== void 0) body["chainId"] = params.chainId;
@@ -1855,13 +2043,15 @@ var Wallets = class {
1855
2043
  return signature;
1856
2044
  }
1857
2045
  /**
1858
- * Retrieve the sealed `user_backup` ciphertext via the recovery gate ({@link fetchRecoveryCiphertext}),
1859
- * mapping a "no recovery share registered" (404) to the actionable `share_not_found` this wallet has no
1860
- * client-held `user_backup` backup to co-sign with (its device-loss recovery is the custodial path instead).
2046
+ * Retrieve the sealed `user_backup` backup via the recovery gate ({@link fetchRecoveryBackup}) — the
2047
+ * ciphertext AND its key wraps (#510), since a backup registered with a passkey is an envelope and
2048
+ * the wraps are what open it. Maps a "no recovery share registered" (404) to the actionable
2049
+ * `share_not_found` — this wallet has no client-held `user_backup` backup to co-sign with (its
2050
+ * device-loss recovery is the custodial path instead).
1861
2051
  */
1862
- async fetchUserBackupCiphertext(walletId, params, signal) {
2052
+ async fetchUserBackup(walletId, params, signal) {
1863
2053
  try {
1864
- return await fetchRecoveryCiphertext(this.http, walletId, params, signal);
2054
+ return await fetchRecoveryBackup(this.http, walletId, params, signal);
1865
2055
  } catch (cause) {
1866
2056
  if (cause instanceof WaaskeyError && cause.code === "not_found") {
1867
2057
  throw new WaaskeyError(
@@ -1983,12 +2173,12 @@ var Wallets = class {
1983
2173
  }
1984
2174
  }
1985
2175
  };
1986
- async function restoreUserBackupShare(ciphertext, recoveryCode) {
2176
+ async function restoreUserBackupShare(backup, opener) {
1987
2177
  let blob;
1988
2178
  try {
1989
- blob = await openWithPassword(recoveryCode, ciphertext);
2179
+ blob = await openRecoveryBlob(backup, opener);
1990
2180
  } catch (cause) {
1991
- throw new WaaskeyError("Could not open the user_backup backup \u2014 wrong recovery code?", "invalid_recovery_code", { cause });
2181
+ throw new WaaskeyError("Could not open the user_backup backup \u2014 wrong recovery code or passkey?", "invalid_recovery_code", { cause });
1992
2182
  }
1993
2183
  return deserializeShare(blob).keyShare;
1994
2184
  }
@@ -2110,13 +2300,12 @@ function isReadyMemberSignCeremony(ceremony) {
2110
2300
  }
2111
2301
 
2112
2302
  // src/client.ts
2113
- var DEFAULT_BASE_URL = "https://api.waaskey.com";
2114
2303
  var Waaskey = class {
2115
2304
  /** The `wallets` resource. */
2116
2305
  wallets;
2117
2306
  /** The `recovery` resource — multi-factor, client-encrypted wallet recovery. */
2118
2307
  recovery;
2119
- /** The `reshare` resource — device-side completion of a device-retaining reshare (#318). */
2308
+ /** The `reshare` resource — this device's half of a committee rotation (#488) and the aux-completion that follows (#318). */
2120
2309
  reshare;
2121
2310
  /** The `balances` resource — client-side balance reads from a chain provider (no backend). */
2122
2311
  balances;
@@ -2132,7 +2321,10 @@ var Waaskey = class {
2132
2321
  if (!options?.apiKey) {
2133
2322
  throw new Error("Waaskey: `apiKey` is required.");
2134
2323
  }
2135
- const http = new HttpClient(options.apiKey, options.baseUrl ?? DEFAULT_BASE_URL, options.fetch);
2324
+ if (!options.baseUrl) {
2325
+ throw new Error("Waaskey: `baseUrl` is required \u2014 pass the URL of your Waaskey API (there is no default).");
2326
+ }
2327
+ const http = new HttpClient(options.apiKey, options.baseUrl, options.fetch);
2136
2328
  const analytics = new Analytics(resolveSink(options.analytics, http));
2137
2329
  this.auth = new Auth(http);
2138
2330
  this.members = new Members(http);
@@ -2167,6 +2359,15 @@ function resolveSink(analytics, http) {
2167
2359
  return analytics ?? new HttpAnalyticsSink(http);
2168
2360
  }
2169
2361
 
2362
+ // src/passkey/recovery-factor.ts
2363
+ function passkeyFactorEnrollment(credentialId) {
2364
+ return { type: "passkey", credential: credentialId };
2365
+ }
2366
+ async function passkeyFactorVerification(challenge, options = {}) {
2367
+ const assertion = await getSigningAssertion(challenge, options);
2368
+ return { type: "passkey", token: JSON.stringify(assertion) };
2369
+ }
2370
+
2170
2371
  // src/mpc/wasm-core.ts
2171
2372
  var WasmMpcCore = class {
2172
2373
  constructor(load) {
@@ -2232,7 +2433,10 @@ var WasmMpcCore = class {
2232
2433
  new_threshold: params.newThreshold,
2233
2434
  wallet: params.wallet,
2234
2435
  commitments: params.commitments,
2235
- sub_shares: params.subShares
2436
+ sub_shares: params.subShares,
2437
+ // The core opens the SEALED sub-shares with this before it can verify or assemble anything;
2438
+ // omitting it (as this did until #488) makes the call fail on its own parameter parse.
2439
+ encryption_secret: params.encryptionSecret
2236
2440
  })
2237
2441
  );
2238
2442
  if (!raw || typeof raw.core_json !== "string") {
@@ -2240,6 +2444,26 @@ var WasmMpcCore = class {
2240
2444
  }
2241
2445
  return { core: raw.core_json, sharedPublicKey: decodePublicKey(raw.shared_public_key_json) };
2242
2446
  }
2447
+ async runReshareDeal(params) {
2448
+ const wasm = await this.init();
2449
+ if (!wasm.reshareDeal) {
2450
+ throw new Error(reshareFeatureHint("reshareDeal"));
2451
+ }
2452
+ const raw = await wasm.reshareDeal(
2453
+ JSON.stringify({
2454
+ curve: params.curve,
2455
+ share: JSON.parse(params.share),
2456
+ quorum_indices: params.quorumIndices,
2457
+ new_preimages: params.newPreimages,
2458
+ new_threshold: params.newThreshold,
2459
+ recipient_pubkeys: params.recipientPubkeys
2460
+ })
2461
+ );
2462
+ if (!raw || !Array.isArray(raw.sealed_sub_shares_hex)) {
2463
+ throw new Error("client-wasm returned an unexpected reshareDeal result");
2464
+ }
2465
+ return { commitments: raw.commitments_json, sealedSubShares: raw.sealed_sub_shares_hex };
2466
+ }
2243
2467
  async runCompleteReshare(params) {
2244
2468
  const wasm = await this.init();
2245
2469
  if (!wasm.completeReshare) {
@@ -2907,6 +3131,6 @@ function prfOutputToSecret(prfResult) {
2907
3131
  return btoa(binary);
2908
3132
  }
2909
3133
 
2910
- 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, userBackupPendingKey, validateCustodyPolicy, verifyWasmIntegrity };
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 };
2911
3135
  //# sourceMappingURL=index.js.map
2912
3136
  //# sourceMappingURL=index.js.map