@kingdomchain/wallet-client 0.6.2 → 0.8.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
@@ -1940,6 +1940,7 @@ var EMBEDDED_WALLET_VAULT_VERSION = 1;
1940
1940
  var EMBEDDED_WALLET_KDF_PRF = "webauthn-prf";
1941
1941
  var EMBEDDED_WALLET_KDF_PBKDF2 = "pbkdf2-passkey";
1942
1942
  var EMBEDDED_WALLET_KDF_PIN = "pbkdf2-pin";
1943
+ var EMBEDDED_WALLET_KDF_DEVICE_PIN = "device-pin";
1943
1944
  var EMBEDDED_WALLET_KEY_HD_DERIVED = "hd-derived";
1944
1945
  var EMBEDDED_WALLET_KEY_DIRECT_ENTROPY = "direct-entropy";
1945
1946
  var EMBEDDED_WALLET_RECOVERY_PHRASE_GENERAL_WARNING = {
@@ -1994,6 +1995,23 @@ var EmbeddedWalletWeakPasscodeError = class extends Error {
1994
1995
  this.reason = reason;
1995
1996
  }
1996
1997
  };
1998
+ var EmbeddedWalletPasskeyUnsupportedError = class extends EmbeddedWalletVaultError {
1999
+ code = "passkey_prf_unsupported";
2000
+ constructor(message = "This device cannot protect the wallet with a passkey; keep using the passcode") {
2001
+ super(message);
2002
+ this.name = "EmbeddedWalletPasskeyUnsupportedError";
2003
+ }
2004
+ };
2005
+ var EmbeddedWalletQuickUnlockError = class extends Error {
2006
+ code;
2007
+ attemptsLeft;
2008
+ constructor(code, message, attemptsLeft) {
2009
+ super(message);
2010
+ this.name = "EmbeddedWalletQuickUnlockError";
2011
+ this.code = code;
2012
+ this.attemptsLeft = attemptsLeft;
2013
+ }
2014
+ };
1997
2015
  var DEFAULT_HD_PATH = "m/44'/118'/0'/0/0";
1998
2016
  async function embeddedPlaintextToSigner(plaintext, prefix = BECH32_PREFIX) {
1999
2017
  if (plaintext.privateKey.length !== 32) {
@@ -2321,6 +2339,30 @@ function assertVaultPermitsTransaction(vault) {
2321
2339
  throw new EmbeddedWalletBackupRequiredError();
2322
2340
  }
2323
2341
  }
2342
+ async function encryptPlaintextWithKek(payload, kek) {
2343
+ const iv = crypto.getRandomValues(new Uint8Array(AES_GCM_IV_BYTES));
2344
+ const aesKey = await importAesKey(kek);
2345
+ const ciphertext = await crypto.subtle.encrypt(
2346
+ { name: "AES-GCM", iv },
2347
+ aesKey,
2348
+ new Uint8Array(serializePlaintext(payload))
2349
+ );
2350
+ return { iv: toBase64(iv), ciphertext: toBase64(new Uint8Array(ciphertext)) };
2351
+ }
2352
+ async function decryptPlaintextWithKek(iv, ciphertext, kek) {
2353
+ const aesKey = await importAesKey(kek);
2354
+ let plaintextBytes;
2355
+ try {
2356
+ plaintextBytes = await crypto.subtle.decrypt(
2357
+ { name: "AES-GCM", iv: new Uint8Array(fromBase64(iv)) },
2358
+ aesKey,
2359
+ new Uint8Array(fromBase64(ciphertext))
2360
+ );
2361
+ } catch (err) {
2362
+ throw new EmbeddedWalletDecryptError(err);
2363
+ }
2364
+ return deserializePlaintext(new Uint8Array(plaintextBytes));
2365
+ }
2324
2366
 
2325
2367
  // src/embeddedWallet/prf.ts
2326
2368
  function detectWebAuthnPrfCapabilities() {
@@ -2331,15 +2373,6 @@ function detectWebAuthnPrfCapabilities() {
2331
2373
  prfFallbackReason: "WebAuthn is not available in this environment"
2332
2374
  };
2333
2375
  }
2334
- const ua = typeof navigator !== "undefined" ? navigator.userAgent : "";
2335
- const isSafari = /Safari/i.test(ua) && !/Chrome|Chromium|CriOS|FxiOS/i.test(ua);
2336
- if (isSafari) {
2337
- return {
2338
- webAuthnAvailable: true,
2339
- prfLikelySupported: false,
2340
- prfFallbackReason: "Safari does not support WebAuthn PRF for cross-device QR flows; use PBKDF2 fallback"
2341
- };
2342
- }
2343
2376
  const getCaps = PublicKeyCredential.getClientCapabilities;
2344
2377
  if (typeof getCaps !== "function") {
2345
2378
  return {
@@ -2370,6 +2403,11 @@ function selectEmbeddedWalletKdf(caps, prfOutput) {
2370
2403
 
2371
2404
  // src/embeddedWallet/backup.ts
2372
2405
  function exportEncryptedBackup(vault) {
2406
+ if (vault.kdf === "device-pin") {
2407
+ throw new Error(
2408
+ "A PIN wallet is bound to this device and has no backup file; its backup is the 24-word recovery phrase"
2409
+ );
2410
+ }
2373
2411
  return {
2374
2412
  format: "kingdom-os-embedded-wallet-backup",
2375
2413
  version: EMBEDDED_WALLET_VAULT_VERSION,
@@ -2519,9 +2557,11 @@ function decodeCredentialId(encoded) {
2519
2557
 
2520
2558
  // src/embeddedWallet/storage.ts
2521
2559
  var DB_NAME = "kingdom-os-wallet";
2522
- var DB_VERSION = 2;
2560
+ var DB_VERSION = 4;
2523
2561
  var VAULT_STORE_NAME = "vault";
2524
2562
  var LOCKOUT_STORE_NAME = "passcode-lockout";
2563
+ var SESSION_STORE_NAME = "session-cache";
2564
+ var QUICK_UNLOCK_STORE_NAME = "quick-unlock";
2525
2565
  var VAULT_LEGACY_KEY = "primary";
2526
2566
  function resolveVaultStorageKey(scopeId) {
2527
2567
  const trimmed = scopeId?.trim();
@@ -2539,6 +2579,12 @@ function openDb() {
2539
2579
  if (!db.objectStoreNames.contains(LOCKOUT_STORE_NAME)) {
2540
2580
  db.createObjectStore(LOCKOUT_STORE_NAME);
2541
2581
  }
2582
+ if (!db.objectStoreNames.contains(SESSION_STORE_NAME)) {
2583
+ db.createObjectStore(SESSION_STORE_NAME);
2584
+ }
2585
+ if (!db.objectStoreNames.contains(QUICK_UNLOCK_STORE_NAME)) {
2586
+ db.createObjectStore(QUICK_UNLOCK_STORE_NAME);
2587
+ }
2542
2588
  };
2543
2589
  request.onsuccess = () => resolve(request.result);
2544
2590
  });
@@ -2613,6 +2659,149 @@ async function clearPasscodeLockoutRecord(publicAddress) {
2613
2659
  });
2614
2660
  db.close();
2615
2661
  }
2662
+ async function saveSessionCacheRecord(record, options = {}) {
2663
+ const key = resolveVaultStorageKey(options.scopeId);
2664
+ const db = await openDb();
2665
+ await new Promise((resolve, reject) => {
2666
+ const tx = db.transaction(SESSION_STORE_NAME, "readwrite");
2667
+ tx.objectStore(SESSION_STORE_NAME).put(record, key);
2668
+ tx.oncomplete = () => resolve();
2669
+ tx.onerror = () => reject(tx.error ?? new Error("IndexedDB write failed"));
2670
+ });
2671
+ db.close();
2672
+ }
2673
+ async function loadSessionCacheRecord(options = {}) {
2674
+ const key = resolveVaultStorageKey(options.scopeId);
2675
+ const db = await openDb();
2676
+ const record = await new Promise((resolve, reject) => {
2677
+ const tx = db.transaction(SESSION_STORE_NAME, "readonly");
2678
+ const request = tx.objectStore(SESSION_STORE_NAME).get(key);
2679
+ request.onsuccess = () => resolve(request.result ?? null);
2680
+ request.onerror = () => reject(request.error ?? new Error("IndexedDB read failed"));
2681
+ });
2682
+ db.close();
2683
+ return record;
2684
+ }
2685
+ async function clearSessionCacheRecord(options = {}) {
2686
+ const key = resolveVaultStorageKey(options.scopeId);
2687
+ const db = await openDb();
2688
+ await new Promise((resolve, reject) => {
2689
+ const tx = db.transaction(SESSION_STORE_NAME, "readwrite");
2690
+ tx.objectStore(SESSION_STORE_NAME).delete(key);
2691
+ tx.oncomplete = () => resolve();
2692
+ tx.onerror = () => reject(tx.error ?? new Error("IndexedDB delete failed"));
2693
+ });
2694
+ db.close();
2695
+ }
2696
+ async function saveQuickUnlockRecord(record, options = {}) {
2697
+ const key = resolveVaultStorageKey(options.scopeId);
2698
+ const db = await openDb();
2699
+ await new Promise((resolve, reject) => {
2700
+ const tx = db.transaction(QUICK_UNLOCK_STORE_NAME, "readwrite");
2701
+ tx.objectStore(QUICK_UNLOCK_STORE_NAME).put(record, key);
2702
+ tx.oncomplete = () => resolve();
2703
+ tx.onerror = () => reject(tx.error ?? new Error("IndexedDB write failed"));
2704
+ });
2705
+ db.close();
2706
+ }
2707
+ async function loadQuickUnlockRecord(options = {}) {
2708
+ const key = resolveVaultStorageKey(options.scopeId);
2709
+ const db = await openDb();
2710
+ const record = await new Promise((resolve, reject) => {
2711
+ const tx = db.transaction(QUICK_UNLOCK_STORE_NAME, "readonly");
2712
+ const request = tx.objectStore(QUICK_UNLOCK_STORE_NAME).get(key);
2713
+ request.onsuccess = () => resolve(request.result ?? null);
2714
+ request.onerror = () => reject(request.error ?? new Error("IndexedDB read failed"));
2715
+ });
2716
+ db.close();
2717
+ return record;
2718
+ }
2719
+ async function clearQuickUnlockRecord(options = {}) {
2720
+ const key = resolveVaultStorageKey(options.scopeId);
2721
+ const db = await openDb();
2722
+ await new Promise((resolve, reject) => {
2723
+ const tx = db.transaction(QUICK_UNLOCK_STORE_NAME, "readwrite");
2724
+ tx.objectStore(QUICK_UNLOCK_STORE_NAME).delete(key);
2725
+ tx.oncomplete = () => resolve();
2726
+ tx.onerror = () => reject(tx.error ?? new Error("IndexedDB delete failed"));
2727
+ });
2728
+ db.close();
2729
+ }
2730
+ var EMBEDDED_WALLET_SESSION_TTL_MS = 12 * 60 * 60 * 1e3;
2731
+ var AES_GCM_IV_BYTES2 = 12;
2732
+ function serializeSessionPlaintext(plaintext) {
2733
+ const wire = {
2734
+ privateKey: toBase64(plaintext.privateKey),
2735
+ hdPath: plaintext.hdPath,
2736
+ createdAt: plaintext.createdAt,
2737
+ keyType: plaintext.keyType ?? EMBEDDED_WALLET_KEY_DIRECT_ENTROPY
2738
+ };
2739
+ if (plaintext.mnemonic) {
2740
+ wire.mnemonic = plaintext.mnemonic;
2741
+ }
2742
+ return new TextEncoder().encode(JSON.stringify(wire));
2743
+ }
2744
+ function deserializeSessionPlaintext(bytes) {
2745
+ const wire = JSON.parse(new TextDecoder().decode(bytes));
2746
+ const plaintext = {
2747
+ privateKey: fromBase64(wire.privateKey),
2748
+ hdPath: wire.hdPath,
2749
+ createdAt: wire.createdAt,
2750
+ keyType: wire.keyType ?? EMBEDDED_WALLET_KEY_DIRECT_ENTROPY
2751
+ };
2752
+ if (wire.mnemonic) {
2753
+ plaintext.mnemonic = wire.mnemonic;
2754
+ }
2755
+ return plaintext;
2756
+ }
2757
+ async function generateNonExtractableSessionKey() {
2758
+ return crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, false, [
2759
+ "encrypt",
2760
+ "decrypt"
2761
+ ]);
2762
+ }
2763
+ async function cacheUnlockedSession(options) {
2764
+ const { plaintext, ttlMs, ...storageOptions } = options;
2765
+ const key = await generateNonExtractableSessionKey();
2766
+ const iv = crypto.getRandomValues(new Uint8Array(AES_GCM_IV_BYTES2));
2767
+ const material = serializeSessionPlaintext(plaintext);
2768
+ const ciphertext = await crypto.subtle.encrypt(
2769
+ { name: "AES-GCM", iv },
2770
+ key,
2771
+ new Uint8Array(material)
2772
+ );
2773
+ const record = {
2774
+ key,
2775
+ iv: toBase64(iv),
2776
+ ciphertext: toBase64(new Uint8Array(ciphertext)),
2777
+ expiresAt: Date.now() + (ttlMs ?? EMBEDDED_WALLET_SESSION_TTL_MS)
2778
+ };
2779
+ await saveSessionCacheRecord(record, storageOptions);
2780
+ }
2781
+ async function restoreCachedSessionPlaintext(options = {}) {
2782
+ const record = await loadSessionCacheRecord(options);
2783
+ if (!record) {
2784
+ return null;
2785
+ }
2786
+ if (typeof record.expiresAt !== "number" || Date.now() >= record.expiresAt) {
2787
+ await clearSessionCacheRecord(options).catch(() => void 0);
2788
+ return null;
2789
+ }
2790
+ try {
2791
+ const plaintextBytes = await crypto.subtle.decrypt(
2792
+ { name: "AES-GCM", iv: new Uint8Array(fromBase64(record.iv)) },
2793
+ record.key,
2794
+ new Uint8Array(fromBase64(record.ciphertext))
2795
+ );
2796
+ return deserializeSessionPlaintext(new Uint8Array(plaintextBytes));
2797
+ } catch {
2798
+ await clearSessionCacheRecord(options).catch(() => void 0);
2799
+ return null;
2800
+ }
2801
+ }
2802
+ async function clearUnlockedSession(options = {}) {
2803
+ await clearSessionCacheRecord(options);
2804
+ }
2616
2805
 
2617
2806
  // src/embeddedWallet/lockout.ts
2618
2807
  var PASSCODE_LOCKOUT_FAILURE_THRESHOLD = 5;
@@ -2777,10 +2966,264 @@ function assertPasscodeAllowedForCreation(passcode) {
2777
2966
  throw new EmbeddedWalletWeakPasscodeError(strength.blockReason ?? "too_weak");
2778
2967
  }
2779
2968
  }
2969
+ var QUICK_UNLOCK_PIN_LENGTH = 6;
2970
+ var QUICK_UNLOCK_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
2971
+ var QUICK_UNLOCK_MAX_ATTEMPTS = 5;
2972
+ var QUICK_UNLOCK_PBKDF2_ITERATIONS = 6e5;
2973
+ var AES_GCM_IV_BYTES3 = 12;
2974
+ function assertQuickUnlockPinAllowed(pin) {
2975
+ if (typeof pin !== "string" || !new RegExp(`^\\d{${QUICK_UNLOCK_PIN_LENGTH}}$`).test(pin)) {
2976
+ throw new EmbeddedWalletQuickUnlockError(
2977
+ "invalid_pin",
2978
+ `PIN must be exactly ${QUICK_UNLOCK_PIN_LENGTH} digits`
2979
+ );
2980
+ }
2981
+ if (isTrivialPin(pin)) {
2982
+ throw new EmbeddedWalletQuickUnlockError(
2983
+ "invalid_pin",
2984
+ "PIN is too easy to guess; avoid repeats and sequences like 123456"
2985
+ );
2986
+ }
2987
+ }
2988
+ function isTrivialPin(pin) {
2989
+ const digits = pin.split("").map(Number);
2990
+ const steps = digits.slice(1).map((d, i) => d - digits[i]);
2991
+ if (steps.every((s) => s === 0)) return true;
2992
+ if (steps.every((s) => s === 1) || steps.every((s) => s === -1)) return true;
2993
+ for (const period of [2, 3]) {
2994
+ if (pin === pin.slice(0, period).repeat(pin.length / period)) return true;
2995
+ }
2996
+ return false;
2997
+ }
2998
+ async function derivePinKey(pin, salt, iterations) {
2999
+ const material = await crypto.subtle.importKey(
3000
+ "raw",
3001
+ new TextEncoder().encode(pin),
3002
+ "PBKDF2",
3003
+ false,
3004
+ ["deriveKey"]
3005
+ );
3006
+ return crypto.subtle.deriveKey(
3007
+ { name: "PBKDF2", salt: new Uint8Array(salt), iterations, hash: "SHA-256" },
3008
+ material,
3009
+ { name: "AES-GCM", length: 256 },
3010
+ false,
3011
+ ["encrypt", "decrypt"]
3012
+ );
3013
+ }
3014
+ async function createQuickUnlockRecord(options) {
3015
+ const { plaintext, publicAddress, pin, ttlMs, iterations, ...storageOptions } = options;
3016
+ assertQuickUnlockPinAllowed(pin);
3017
+ const rounds = iterations ?? QUICK_UNLOCK_PBKDF2_ITERATIONS;
3018
+ const salt = crypto.getRandomValues(new Uint8Array(32));
3019
+ const pinKey = await derivePinKey(pin, salt, rounds);
3020
+ const deviceKey = await crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, false, [
3021
+ "encrypt",
3022
+ "decrypt"
3023
+ ]);
3024
+ const innerIv = crypto.getRandomValues(new Uint8Array(AES_GCM_IV_BYTES3));
3025
+ const inner = await crypto.subtle.encrypt(
3026
+ { name: "AES-GCM", iv: innerIv },
3027
+ pinKey,
3028
+ new Uint8Array(serializePlaintext(plaintext))
3029
+ );
3030
+ const outerIv = crypto.getRandomValues(new Uint8Array(AES_GCM_IV_BYTES3));
3031
+ const outer = await crypto.subtle.encrypt({ name: "AES-GCM", iv: outerIv }, deviceKey, inner);
3032
+ const record = {
3033
+ deviceKey,
3034
+ publicAddress,
3035
+ salt: toBase64(salt),
3036
+ iterations: rounds,
3037
+ innerIv: toBase64(innerIv),
3038
+ outerIv: toBase64(outerIv),
3039
+ ciphertext: toBase64(new Uint8Array(outer)),
3040
+ expiresAt: Date.now() + (ttlMs ?? QUICK_UNLOCK_TTL_MS),
3041
+ failedAttempts: 0
3042
+ };
3043
+ await saveQuickUnlockRecord(record, storageOptions);
3044
+ return record;
3045
+ }
3046
+ async function openQuickUnlockRecord(options) {
3047
+ const { pin, ...storageOptions } = options;
3048
+ if (typeof pin !== "string" || !/^\d+$/.test(pin) || pin.length !== QUICK_UNLOCK_PIN_LENGTH) {
3049
+ throw new EmbeddedWalletQuickUnlockError(
3050
+ "invalid_pin",
3051
+ `PIN must be exactly ${QUICK_UNLOCK_PIN_LENGTH} digits`
3052
+ );
3053
+ }
3054
+ const record = await loadQuickUnlockRecord(storageOptions);
3055
+ if (!record) {
3056
+ throw new EmbeddedWalletQuickUnlockError("not_set", "No PIN is set up on this device");
3057
+ }
3058
+ if (typeof record.expiresAt !== "number" || Date.now() >= record.expiresAt) {
3059
+ throw new EmbeddedWalletQuickUnlockError(
3060
+ "expired",
3061
+ "PIN expired; unlock with Face ID or your passcode"
3062
+ );
3063
+ }
3064
+ const attempt = (record.failedAttempts ?? 0) + 1;
3065
+ await saveQuickUnlockRecord({ ...record, failedAttempts: attempt }, storageOptions);
3066
+ let inner;
3067
+ try {
3068
+ inner = await crypto.subtle.decrypt(
3069
+ { name: "AES-GCM", iv: new Uint8Array(fromBase64(record.outerIv)) },
3070
+ record.deviceKey,
3071
+ new Uint8Array(fromBase64(record.ciphertext))
3072
+ );
3073
+ } catch {
3074
+ await clearQuickUnlockRecord(storageOptions).catch(() => void 0);
3075
+ throw new EmbeddedWalletQuickUnlockError("not_set", "PIN record is unusable on this device");
3076
+ }
3077
+ const pinKey = await derivePinKey(pin, fromBase64(record.salt), record.iterations);
3078
+ let plaintextBytes;
3079
+ try {
3080
+ plaintextBytes = await crypto.subtle.decrypt(
3081
+ { name: "AES-GCM", iv: new Uint8Array(fromBase64(record.innerIv)) },
3082
+ pinKey,
3083
+ inner
3084
+ );
3085
+ } catch {
3086
+ if (attempt >= QUICK_UNLOCK_MAX_ATTEMPTS) {
3087
+ await clearQuickUnlockRecord(storageOptions).catch(() => void 0);
3088
+ throw new EmbeddedWalletQuickUnlockError(
3089
+ "wiped",
3090
+ "Too many wrong PINs; unlock with Face ID or your passcode"
3091
+ );
3092
+ }
3093
+ throw new EmbeddedWalletQuickUnlockError(
3094
+ "wrong_pin",
3095
+ "Wrong PIN",
3096
+ QUICK_UNLOCK_MAX_ATTEMPTS - attempt
3097
+ );
3098
+ }
3099
+ await saveQuickUnlockRecord({ ...record, failedAttempts: 0 }, storageOptions);
3100
+ return {
3101
+ plaintext: deserializePlaintext(new Uint8Array(plaintextBytes)),
3102
+ publicAddress: record.publicAddress
3103
+ };
3104
+ }
3105
+ async function getQuickUnlockStatus(options = {}) {
3106
+ const record = await loadQuickUnlockRecord(options);
3107
+ if (!record) {
3108
+ return {
3109
+ enabled: false,
3110
+ expired: false,
3111
+ expiresAt: null,
3112
+ attemptsLeft: QUICK_UNLOCK_MAX_ATTEMPTS,
3113
+ publicAddress: null
3114
+ };
3115
+ }
3116
+ return {
3117
+ enabled: true,
3118
+ expired: typeof record.expiresAt !== "number" || Date.now() >= record.expiresAt,
3119
+ expiresAt: record.expiresAt,
3120
+ attemptsLeft: Math.max(0, QUICK_UNLOCK_MAX_ATTEMPTS - (record.failedAttempts ?? 0)),
3121
+ publicAddress: record.publicAddress
3122
+ };
3123
+ }
3124
+ async function refreshQuickUnlockExpiry(options) {
3125
+ const { publicAddress, ttlMs, ...storageOptions } = options;
3126
+ const record = await loadQuickUnlockRecord(storageOptions);
3127
+ if (!record) return;
3128
+ if (record.publicAddress !== publicAddress) {
3129
+ await clearQuickUnlockRecord(storageOptions);
3130
+ return;
3131
+ }
3132
+ await saveQuickUnlockRecord(
3133
+ { ...record, expiresAt: Date.now() + (ttlMs ?? QUICK_UNLOCK_TTL_MS), failedAttempts: 0 },
3134
+ storageOptions
3135
+ );
3136
+ }
3137
+ async function clearQuickUnlock(options = {}) {
3138
+ await clearQuickUnlockRecord(options);
3139
+ }
3140
+ var DEVICE_PIN_PBKDF2_ITERATIONS = 6e5;
3141
+ var AES_GCM_IV_BYTES4 = 12;
3142
+ function vaultUsesPinUnlock(vault) {
3143
+ return vault.kdf === EMBEDDED_WALLET_KDF_DEVICE_PIN;
3144
+ }
3145
+ async function encryptPlaintextWithDevicePin(plaintext, pin, options) {
3146
+ assertQuickUnlockPinAllowed(pin);
3147
+ const iterations = options.iterations ?? DEVICE_PIN_PBKDF2_ITERATIONS;
3148
+ const salt = crypto.getRandomValues(new Uint8Array(32));
3149
+ const pinKey = await derivePinKey(pin, salt, iterations);
3150
+ const deviceKey = await crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, false, [
3151
+ "encrypt",
3152
+ "decrypt"
3153
+ ]);
3154
+ const iv = crypto.getRandomValues(new Uint8Array(AES_GCM_IV_BYTES4));
3155
+ const inner = await crypto.subtle.encrypt(
3156
+ { name: "AES-GCM", iv },
3157
+ pinKey,
3158
+ new Uint8Array(serializePlaintext(plaintext))
3159
+ );
3160
+ const deviceIv = crypto.getRandomValues(new Uint8Array(AES_GCM_IV_BYTES4));
3161
+ const outer = await crypto.subtle.encrypt({ name: "AES-GCM", iv: deviceIv }, deviceKey, inner);
3162
+ const acknowledged = options.backupAcknowledged ?? false;
3163
+ return {
3164
+ version: EMBEDDED_WALLET_VAULT_VERSION,
3165
+ kdf: EMBEDDED_WALLET_KDF_DEVICE_PIN,
3166
+ credentialId: "",
3167
+ publicAddress: options.publicAddress,
3168
+ iv: toBase64(iv),
3169
+ ciphertext: toBase64(new Uint8Array(outer)),
3170
+ kdfSalt: toBase64(salt),
3171
+ kdfIterations: iterations,
3172
+ prfSalt: "",
3173
+ backupAcknowledged: acknowledged,
3174
+ backupAcknowledgedAt: acknowledged ? (/* @__PURE__ */ new Date()).toISOString() : void 0,
3175
+ createdAt: plaintext.createdAt,
3176
+ deviceKey,
3177
+ deviceIv: toBase64(deviceIv)
3178
+ };
3179
+ }
3180
+ async function decryptDevicePinVault(vault, pin) {
3181
+ if (!vaultUsesPinUnlock(vault)) {
3182
+ throw new EmbeddedWalletVaultError("Vault does not use PIN unlock");
3183
+ }
3184
+ if (typeof pin !== "string" || !new RegExp(`^\\d{${QUICK_UNLOCK_PIN_LENGTH}}$`).test(pin)) {
3185
+ throw new EmbeddedWalletQuickUnlockError(
3186
+ "invalid_pin",
3187
+ `PIN must be exactly ${QUICK_UNLOCK_PIN_LENGTH} digits`
3188
+ );
3189
+ }
3190
+ if (!vault.deviceKey || !vault.deviceIv) {
3191
+ throw new EmbeddedWalletVaultError(
3192
+ "This wallet's device key is missing; recover it with your 24 words"
3193
+ );
3194
+ }
3195
+ let inner;
3196
+ try {
3197
+ inner = await crypto.subtle.decrypt(
3198
+ { name: "AES-GCM", iv: new Uint8Array(fromBase64(vault.deviceIv)) },
3199
+ vault.deviceKey,
3200
+ new Uint8Array(fromBase64(vault.ciphertext))
3201
+ );
3202
+ } catch (err) {
3203
+ throw new EmbeddedWalletVaultError(
3204
+ "This wallet's device layer is damaged; recover it with your 24 words",
3205
+ err
3206
+ );
3207
+ }
3208
+ const pinKey = await derivePinKey(pin, fromBase64(vault.kdfSalt), vault.kdfIterations);
3209
+ let plaintext;
3210
+ try {
3211
+ plaintext = await crypto.subtle.decrypt(
3212
+ { name: "AES-GCM", iv: new Uint8Array(fromBase64(vault.iv)) },
3213
+ pinKey,
3214
+ inner
3215
+ );
3216
+ } catch (err) {
3217
+ throw new EmbeddedWalletDecryptError(err);
3218
+ }
3219
+ return deserializePlaintext(new Uint8Array(plaintext));
3220
+ }
2780
3221
 
2781
3222
  // src/embeddedWallet/wallet.ts
2782
3223
  var activeSession = null;
2783
3224
  var activeScopeId;
3225
+ var activePlaintext = null;
3226
+ var activeUnlockWasStrong = false;
2784
3227
  function resolveScopeId(scopeId) {
2785
3228
  const trimmed = scopeId?.trim();
2786
3229
  return trimmed || void 0;
@@ -2797,6 +3240,12 @@ function vaultUsesPasskeyUnlock(vault) {
2797
3240
  function vaultUsesPasscodeUnlock(vault) {
2798
3241
  return vault.kdf === EMBEDDED_WALLET_KDF_PIN;
2799
3242
  }
3243
+ function vaultHasPasskeyUnlock(vault) {
3244
+ return Boolean(vault.passkeyWrap) || vaultUsesPasskeyUnlock(vault);
3245
+ }
3246
+ function vaultPersistsSessionCache(vault) {
3247
+ return !(vaultUsesPinUnlock(vault) || Boolean(vault.passkeyWrap));
3248
+ }
2800
3249
  function isPasscodeDecryptFailure(err) {
2801
3250
  return err instanceof EmbeddedWalletDecryptError;
2802
3251
  }
@@ -2819,6 +3268,28 @@ async function decryptVaultWithPasscodeProtected(vault, passcode) {
2819
3268
  throw err;
2820
3269
  }
2821
3270
  }
3271
+ async function decryptVaultWithPinProtected(vault, pin) {
3272
+ if (!pin) {
3273
+ throw new EmbeddedWalletVaultError("PIN is required to unlock this wallet");
3274
+ }
3275
+ await assertPasscodeUnlockAllowed(vault.publicAddress);
3276
+ try {
3277
+ const plaintext = await decryptDevicePinVault(vault, pin);
3278
+ await recordPasscodeUnlockSuccess(vault.publicAddress);
3279
+ return plaintext;
3280
+ } catch (err) {
3281
+ if (isPasscodeDecryptFailure(err)) {
3282
+ await recordPasscodeUnlockFailure(vault.publicAddress);
3283
+ }
3284
+ throw err;
3285
+ }
3286
+ }
3287
+ async function decryptVaultWithSecret(vault, secret) {
3288
+ if (vaultUsesPinUnlock(vault)) {
3289
+ return decryptVaultWithPinProtected(vault, secret.pin);
3290
+ }
3291
+ return decryptVaultWithPasscodeProtected(vault, secret.passcode);
3292
+ }
2822
3293
  async function deriveKekFromPasskeyMaterial(vault, prfOutput, authenticatorData, clientDataJSON) {
2823
3294
  const caps = detectWebAuthnPrfCapabilities();
2824
3295
  const kdf = selectEmbeddedWalletKdf(caps, prfOutput);
@@ -2874,55 +3345,61 @@ async function encryptPlaintextWithPasscode(plaintext, passcode, backupAcknowled
2874
3345
  });
2875
3346
  return { address: account.address, vault };
2876
3347
  }
2877
- async function encryptPlaintextWithPasskeyRegistration(plaintext, registration, backupAcknowledged = false) {
2878
- const previewSigner = await embeddedPlaintextToSigner(plaintext);
2879
- const [account] = await previewSigner.getAccounts();
2880
- const caps = detectWebAuthnPrfCapabilities();
2881
- const kdf = selectEmbeddedWalletKdf(caps, registration.prfOutput);
2882
- let kek;
2883
- let kdfSalt;
2884
- let prfSalt;
2885
- if (kdf === EMBEDDED_WALLET_KDF_PRF && registration.prfOutput) {
2886
- kek = await deriveKekFromPrf(registration.prfOutput);
2887
- prfSalt = registration.prfSalt;
2888
- } else {
2889
- kdfSalt = createPbkdf2Salt();
2890
- const material = createPasskeyMaterialFromAssertion(
2891
- registration.authenticatorData,
2892
- registration.clientDataJSON
2893
- );
2894
- kek = await deriveKekFromPbkdf2(material, kdfSalt);
3348
+ async function encryptPlaintextWithSecret(plaintext, secret, backupAcknowledged = false) {
3349
+ if (secret.pin != null) {
3350
+ const signer = await embeddedPlaintextToSigner(plaintext);
3351
+ const [account] = await signer.getAccounts();
3352
+ const vault = await encryptPlaintextWithDevicePin(plaintext, secret.pin, {
3353
+ publicAddress: account.address,
3354
+ backupAcknowledged
3355
+ });
3356
+ return { address: account.address, vault };
2895
3357
  }
2896
- const vault = await encryptWalletPayload(plaintext, {
2897
- kdf,
2898
- credentialId: registration.credentialId,
2899
- publicAddress: account.address,
2900
- kek,
2901
- kdfSalt,
2902
- prfSalt,
2903
- backupAcknowledged
2904
- });
2905
- return { address: account.address, vault };
3358
+ if (secret.passcode == null) {
3359
+ throw new EmbeddedWalletVaultError("A PIN (or legacy passcode) is required");
3360
+ }
3361
+ return encryptPlaintextWithPasscode(plaintext, secret.passcode, backupAcknowledged);
2906
3362
  }
2907
3363
  async function unlockVaultToSession(vault, options = {}) {
2908
- if (vaultUsesPasscodeUnlock(vault)) {
2909
- const plaintext2 = await decryptVaultWithPasscodeProtected(vault, options.passcode);
2910
- const signer2 = await embeddedPlaintextToSigner(plaintext2);
2911
- const [account2] = await signer2.getAccounts();
2912
- const session2 = { address: account2.address, vault, signer: signer2 };
2913
- activeSession = session2;
2914
- activeScopeId = scopeFromOptions(options);
2915
- return session2;
2916
- }
2917
- const credentialId = decodeCredentialId(vault.credentialId);
2918
- const prfSalt = vault.prfSalt ? decodeCredentialId(vault.prfSalt) : void 0;
2919
- const assertion = await assertPasskey({ credentialId, rpId: void 0, prfSalt });
2920
- const plaintext = await decryptVaultWithUnlockMaterial(vault, void 0, assertion);
3364
+ const scopeId = scopeFromOptions(options);
3365
+ let plaintext;
3366
+ if (vaultUsesPinUnlock(vault)) {
3367
+ plaintext = await decryptVaultWithPinProtected(vault, options.pin);
3368
+ } else if (vaultUsesPasscodeUnlock(vault)) {
3369
+ plaintext = await decryptVaultWithPasscodeProtected(vault, options.passcode);
3370
+ } else {
3371
+ const credentialId = decodeCredentialId(vault.credentialId);
3372
+ const prfSalt = vault.prfSalt ? decodeCredentialId(vault.prfSalt) : void 0;
3373
+ const assertion = await assertPasskey({ credentialId, rpId: void 0, prfSalt });
3374
+ plaintext = await decryptVaultWithUnlockMaterial(vault, void 0, assertion);
3375
+ }
3376
+ return establishSession(vault, plaintext, scopeId, { strong: true });
3377
+ }
3378
+ async function establishSession(vault, plaintext, scopeId, options) {
2921
3379
  const signer = await embeddedPlaintextToSigner(plaintext);
2922
3380
  const [account] = await signer.getAccounts();
2923
3381
  const session = { address: account.address, vault, signer };
2924
3382
  activeSession = session;
2925
- activeScopeId = scopeFromOptions(options);
3383
+ activeScopeId = scopeId;
3384
+ activePlaintext = plaintext;
3385
+ activeUnlockWasStrong = options.strong;
3386
+ if (vaultPersistsSessionCache(vault)) {
3387
+ try {
3388
+ await cacheUnlockedSession({ scopeId, plaintext });
3389
+ } catch {
3390
+ }
3391
+ } else {
3392
+ try {
3393
+ await clearUnlockedSession({ scopeId });
3394
+ } catch {
3395
+ }
3396
+ }
3397
+ if (options.strong) {
3398
+ try {
3399
+ await refreshQuickUnlockExpiry({ scopeId, publicAddress: account.address });
3400
+ } catch {
3401
+ }
3402
+ }
2926
3403
  return session;
2927
3404
  }
2928
3405
  async function hasEmbeddedWallet(options = {}) {
@@ -2962,7 +3439,7 @@ async function createEmbeddedWallet(options) {
2962
3439
  }
2963
3440
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
2964
3441
  const plaintext = await createHdDerivedPlaintext(createdAt);
2965
- const { address, vault } = await encryptPlaintextWithPasscode(plaintext, options.passcode);
3442
+ const { address, vault } = await encryptPlaintextWithSecret(plaintext, options);
2966
3443
  await saveVaultToIndexedDb(vault, { scopeId });
2967
3444
  lockEmbeddedWallet();
2968
3445
  activeScopeId = scopeId;
@@ -2975,7 +3452,11 @@ async function revealEmbeddedRecoveryPhrase(options = {}) {
2975
3452
  throw new EmbeddedWalletVaultError("No embedded wallet found on this device");
2976
3453
  }
2977
3454
  let plaintext;
2978
- if (vaultUsesPasscodeUnlock(vault)) {
3455
+ if ((vaultUsesPasscodeUnlock(vault) || vaultUsesPinUnlock(vault)) && !options.passcode && !options.pin && vault.passkeyWrap) {
3456
+ plaintext = await decryptPasskeyWrap(vault);
3457
+ } else if (vaultUsesPinUnlock(vault)) {
3458
+ plaintext = await decryptVaultWithPinProtected(vault, options.pin);
3459
+ } else if (vaultUsesPasscodeUnlock(vault)) {
2979
3460
  plaintext = await decryptVaultWithPasscodeProtected(vault, options.passcode);
2980
3461
  } else {
2981
3462
  const credentialId = decodeCredentialId(vault.credentialId);
@@ -3031,7 +3512,7 @@ async function recoverEmbeddedWalletFromMnemonic(options) {
3031
3512
  "Recovered wallet address does not match expectedAddress"
3032
3513
  );
3033
3514
  }
3034
- const result = await encryptPlaintextWithPasscode(plaintext, options.passcode);
3515
+ const result = await encryptPlaintextWithSecret(plaintext, options);
3035
3516
  if (result.address !== address) {
3036
3517
  throw new EmbeddedWalletVaultError("Internal error: recovered address mismatch");
3037
3518
  }
@@ -3040,41 +3521,220 @@ async function recoverEmbeddedWalletFromMnemonic(options) {
3040
3521
  activeScopeId = scopeId;
3041
3522
  return { address: result.address, vault: result.vault, keyType };
3042
3523
  }
3524
+ function activePlaintextFor(scopeId, vault) {
3525
+ if (!activePlaintext || !activeSession) return null;
3526
+ if (activeScopeId !== scopeId) return null;
3527
+ if (activeSession.address !== vault.publicAddress) return null;
3528
+ return activePlaintext;
3529
+ }
3043
3530
  async function enableEmbeddedPasskeyUnlock(options) {
3044
3531
  const scopeId = scopeFromOptions(options) ?? activeScopeId;
3045
3532
  const vault = await loadVaultForScope(scopeId);
3046
3533
  if (!vault) {
3047
3534
  throw new EmbeddedWalletVaultError("No embedded wallet found on this device");
3048
3535
  }
3049
- if (vaultUsesPasskeyUnlock(vault)) {
3536
+ if (vaultHasPasskeyUnlock(vault)) {
3050
3537
  throw new EmbeddedWalletVaultError("Passkey unlock is already enabled for this wallet");
3051
3538
  }
3052
- if (!vaultUsesPasscodeUnlock(vault)) {
3539
+ if (!vaultUsesPasscodeUnlock(vault) && !vaultUsesPinUnlock(vault)) {
3053
3540
  throw new EmbeddedWalletVaultError("Unsupported vault unlock method for passkey upgrade");
3054
3541
  }
3055
- const addressBefore = vault.publicAddress;
3056
- const backupAcknowledged = vault.backupAcknowledged;
3057
- const plaintext = await decryptVaultWithPasscodeProtected(vault, options.passcode);
3542
+ const plaintext = options.passcode != null || options.pin != null ? await decryptVaultWithSecret(vault, options) : activePlaintextFor(scopeId, vault);
3543
+ if (!plaintext) {
3544
+ throw new EmbeddedWalletVaultError("Unlock the wallet (or pass the passcode) before adding Face ID");
3545
+ }
3546
+ const prfSalt = crypto.getRandomValues(new Uint8Array(32));
3058
3547
  const registration = await registerPasskey({
3059
3548
  userId: options.userId,
3060
3549
  userName: options.userName,
3061
3550
  displayName: options.displayName,
3062
- rpId: options.rpId
3551
+ rpId: options.rpId,
3552
+ prfSalt
3063
3553
  });
3064
- const result = await encryptPlaintextWithPasskeyRegistration(
3554
+ let prfOutput = registration.prfOutput;
3555
+ if (!prfOutput) {
3556
+ try {
3557
+ const assertion = await assertPasskey({
3558
+ credentialId: registration.credentialId,
3559
+ rpId: options.rpId,
3560
+ prfSalt
3561
+ });
3562
+ prfOutput = assertion.prfOutput;
3563
+ } catch {
3564
+ prfOutput = null;
3565
+ }
3566
+ }
3567
+ if (!prfOutput) {
3568
+ throw new EmbeddedWalletPasskeyUnsupportedError();
3569
+ }
3570
+ const previewSigner = await embeddedPlaintextToSigner(plaintext);
3571
+ const [account] = await previewSigner.getAccounts();
3572
+ if (account.address !== vault.publicAddress) {
3573
+ throw new EmbeddedWalletVaultError("Passkey upgrade must not change wallet address");
3574
+ }
3575
+ const kek = await deriveKekFromPrf(prfOutput);
3576
+ const wrapped = await encryptPlaintextWithKek(plaintext, kek);
3577
+ const updated = {
3578
+ ...vault,
3579
+ passkeyWrap: {
3580
+ credentialId: encodeCredentialId(registration.credentialId),
3581
+ prfSalt: encodeCredentialId(prfSalt),
3582
+ rpId: options.rpId,
3583
+ iv: wrapped.iv,
3584
+ ciphertext: wrapped.ciphertext,
3585
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
3586
+ }
3587
+ };
3588
+ await saveVaultToIndexedDb(updated, { scopeId });
3589
+ if (activeSession && activeSession.address === updated.publicAddress) {
3590
+ activeSession = { ...activeSession, vault: updated };
3591
+ }
3592
+ return { address: updated.publicAddress, vault: updated };
3593
+ }
3594
+ async function setEmbeddedWalletPin(options) {
3595
+ const scopeId = scopeFromOptions(options) ?? activeScopeId;
3596
+ const vault = await loadVaultForScope(scopeId);
3597
+ if (!vault) {
3598
+ throw new EmbeddedWalletVaultError("No embedded wallet found on this device");
3599
+ }
3600
+ if (!vaultUsesPasscodeUnlock(vault) && !vaultUsesPinUnlock(vault)) {
3601
+ throw new EmbeddedWalletVaultError("This wallet type cannot switch to a PIN");
3602
+ }
3603
+ let plaintext = null;
3604
+ if (options.currentPin != null || options.passcode != null) {
3605
+ plaintext = await decryptVaultWithSecret(vault, {
3606
+ pin: options.currentPin,
3607
+ passcode: options.passcode
3608
+ });
3609
+ } else if (activeUnlockWasStrong) {
3610
+ plaintext = activePlaintextFor(scopeId, vault);
3611
+ }
3612
+ if (!plaintext) {
3613
+ throw new EmbeddedWalletVaultError("Unlock the wallet before changing the PIN");
3614
+ }
3615
+ const result = await encryptPlaintextWithSecret(
3065
3616
  plaintext,
3066
- registration,
3067
- backupAcknowledged
3617
+ { pin: options.pin },
3618
+ vault.backupAcknowledged
3068
3619
  );
3069
- if (result.address !== addressBefore) {
3070
- throw new EmbeddedWalletVaultError("Passkey upgrade must not change wallet address");
3620
+ if (result.address !== vault.publicAddress) {
3621
+ throw new EmbeddedWalletVaultError("Changing the PIN must not change the wallet address");
3622
+ }
3623
+ const updated = {
3624
+ ...result.vault,
3625
+ backupAcknowledgedAt: vault.backupAcknowledgedAt,
3626
+ createdAt: vault.createdAt,
3627
+ passkeyWrap: vault.passkeyWrap
3628
+ };
3629
+ if (!updated.passkeyWrap) delete updated.passkeyWrap;
3630
+ await saveVaultToIndexedDb(updated, { scopeId });
3631
+ await clearQuickUnlock({ scopeId }).catch(() => void 0);
3632
+ if (activeSession && activeSession.address === updated.publicAddress) {
3633
+ activeSession = { ...activeSession, vault: updated };
3071
3634
  }
3072
- await saveVaultToIndexedDb(result.vault, { scopeId });
3073
- if (activeSession) {
3074
- activeSession = { ...activeSession, vault: result.vault };
3635
+ return { address: updated.publicAddress, vault: updated };
3636
+ }
3637
+ async function disableEmbeddedPasskeyUnlock(options = {}) {
3638
+ const scopeId = scopeFromOptions(options) ?? activeScopeId;
3639
+ const vault = await loadVaultForScope(scopeId);
3640
+ if (!vault) {
3641
+ throw new EmbeddedWalletVaultError("No embedded wallet found on this device");
3075
3642
  }
3076
- lockEmbeddedWallet();
3077
- return result;
3643
+ if (!vault.passkeyWrap) {
3644
+ return vault;
3645
+ }
3646
+ const updated = { ...vault };
3647
+ delete updated.passkeyWrap;
3648
+ await saveVaultToIndexedDb(updated, { scopeId });
3649
+ if (activeSession && activeSession.address === updated.publicAddress) {
3650
+ activeSession = { ...activeSession, vault: updated };
3651
+ }
3652
+ return updated;
3653
+ }
3654
+ async function decryptPasskeyWrap(vault) {
3655
+ const wrap = vault.passkeyWrap;
3656
+ if (!wrap) {
3657
+ throw new EmbeddedWalletVaultError("Face ID is not set up for this wallet");
3658
+ }
3659
+ const prfSalt = decodeCredentialId(wrap.prfSalt);
3660
+ const assertion = await assertPasskey({
3661
+ credentialId: decodeCredentialId(wrap.credentialId),
3662
+ rpId: wrap.rpId,
3663
+ prfSalt
3664
+ });
3665
+ if (!assertion.prfOutput) {
3666
+ throw new EmbeddedWalletPasskeyUnsupportedError(
3667
+ "This browser did not return the passkey secret; unlock with your passcode"
3668
+ );
3669
+ }
3670
+ const kek = await deriveKekFromPrf(assertion.prfOutput);
3671
+ const plaintext = await decryptPlaintextWithKek(wrap.iv, wrap.ciphertext, kek);
3672
+ const signer = await embeddedPlaintextToSigner(plaintext);
3673
+ const [account] = await signer.getAccounts();
3674
+ if (account.address !== vault.publicAddress) {
3675
+ throw new EmbeddedWalletVaultError("Passkey opened a key for a different address");
3676
+ }
3677
+ return plaintext;
3678
+ }
3679
+ async function unlockEmbeddedWalletWithPasskey(options = {}) {
3680
+ const scopeId = scopeFromOptions(options) ?? activeScopeId;
3681
+ const vault = await loadVaultForScope(scopeId);
3682
+ if (!vault) {
3683
+ throw new EmbeddedWalletVaultError("No embedded wallet found on this device");
3684
+ }
3685
+ if (vaultUsesPasskeyUnlock(vault)) {
3686
+ return unlockVaultToSession(vault, { scopeId });
3687
+ }
3688
+ const plaintext = await decryptPasskeyWrap(vault);
3689
+ return establishSession(vault, plaintext, scopeId, { strong: true });
3690
+ }
3691
+ async function setEmbeddedQuickUnlockPin(options) {
3692
+ const scopeId = scopeFromOptions(options) ?? activeScopeId;
3693
+ const vault = await loadVaultForScope(scopeId);
3694
+ if (!vault) {
3695
+ throw new EmbeddedWalletVaultError("No embedded wallet found on this device");
3696
+ }
3697
+ const plaintext = activePlaintextFor(scopeId, vault);
3698
+ if (!plaintext || !activeUnlockWasStrong) {
3699
+ throw new EmbeddedWalletQuickUnlockError(
3700
+ "no_session",
3701
+ "Unlock with Face ID or your passcode before choosing a PIN"
3702
+ );
3703
+ }
3704
+ await createQuickUnlockRecord({
3705
+ scopeId,
3706
+ plaintext,
3707
+ publicAddress: vault.publicAddress,
3708
+ pin: options.pin
3709
+ });
3710
+ return getQuickUnlockStatus({ scopeId });
3711
+ }
3712
+ async function unlockEmbeddedWalletWithPin(options) {
3713
+ const scopeId = scopeFromOptions(options) ?? activeScopeId;
3714
+ const vault = await loadVaultForScope(scopeId);
3715
+ if (!vault) {
3716
+ throw new EmbeddedWalletVaultError("No embedded wallet found on this device");
3717
+ }
3718
+ const opened = await openQuickUnlockRecord({ scopeId, pin: options.pin });
3719
+ if (opened.publicAddress !== vault.publicAddress) {
3720
+ await clearQuickUnlock({ scopeId }).catch(() => void 0);
3721
+ throw new EmbeddedWalletQuickUnlockError("not_set", "PIN belongs to a different wallet");
3722
+ }
3723
+ const signer = await embeddedPlaintextToSigner(opened.plaintext);
3724
+ const [account] = await signer.getAccounts();
3725
+ if (account.address !== vault.publicAddress) {
3726
+ await clearQuickUnlock({ scopeId }).catch(() => void 0);
3727
+ throw new EmbeddedWalletQuickUnlockError("not_set", "PIN record does not match this wallet");
3728
+ }
3729
+ return establishSession(vault, opened.plaintext, scopeId, { strong: false });
3730
+ }
3731
+ async function getEmbeddedQuickUnlockStatus(options = {}) {
3732
+ const scopeId = scopeFromOptions(options) ?? activeScopeId;
3733
+ return getQuickUnlockStatus({ scopeId });
3734
+ }
3735
+ async function clearEmbeddedQuickUnlockPin(options = {}) {
3736
+ const scopeId = scopeFromOptions(options) ?? activeScopeId;
3737
+ await clearQuickUnlock({ scopeId });
3078
3738
  }
3079
3739
  async function unlockEmbeddedWallet(options = {}) {
3080
3740
  const scopeId = scopeFromOptions(options) ?? activeScopeId;
@@ -3084,9 +3744,37 @@ async function unlockEmbeddedWallet(options = {}) {
3084
3744
  }
3085
3745
  return unlockVaultToSession(vault, options);
3086
3746
  }
3747
+ async function tryRestoreUnlockedSession(options = {}) {
3748
+ const scopeId = scopeFromOptions(options) ?? activeScopeId;
3749
+ const plaintext = await restoreCachedSessionPlaintext({ scopeId });
3750
+ if (!plaintext) {
3751
+ return null;
3752
+ }
3753
+ const vault = await loadVaultForScope(scopeId);
3754
+ if (!vault) {
3755
+ await clearUnlockedSession({ scopeId }).catch(() => void 0);
3756
+ return null;
3757
+ }
3758
+ const signer = await embeddedPlaintextToSigner(plaintext);
3759
+ const [account] = await signer.getAccounts();
3760
+ if (account.address !== vault.publicAddress) {
3761
+ await clearUnlockedSession({ scopeId }).catch(() => void 0);
3762
+ return null;
3763
+ }
3764
+ const session = { address: account.address, vault, signer };
3765
+ activeSession = session;
3766
+ activeScopeId = scopeId;
3767
+ activePlaintext = plaintext;
3768
+ activeUnlockWasStrong = false;
3769
+ return session;
3770
+ }
3087
3771
  function lockEmbeddedWallet() {
3772
+ const scopeId = activeScopeId;
3088
3773
  activeSession = null;
3089
3774
  activeScopeId = void 0;
3775
+ activePlaintext = null;
3776
+ activeUnlockWasStrong = false;
3777
+ void clearUnlockedSession({ scopeId }).catch(() => void 0);
3090
3778
  }
3091
3779
  function getEmbeddedWalletSession() {
3092
3780
  return activeSession;
@@ -3113,7 +3801,7 @@ async function acknowledgeEmbeddedBackup(options = {}) {
3113
3801
  if (activeSession) {
3114
3802
  activeSession = { ...activeSession, vault: updated };
3115
3803
  }
3116
- return exportEncryptedBackup(updated);
3804
+ return vaultUsesPinUnlock(updated) ? null : exportEncryptedBackup(updated);
3117
3805
  }
3118
3806
  function downloadEmbeddedBackupFile(backup, filename) {
3119
3807
  const blob = new Blob([serializeBackupFile(backup)], { type: "application/json" });
@@ -3141,6 +3829,8 @@ async function importEmbeddedBackup(json, options = {}) {
3141
3829
  async function clearEmbeddedWallet(options = {}) {
3142
3830
  const scopeId = scopeFromOptions(options) ?? activeScopeId;
3143
3831
  lockEmbeddedWallet();
3832
+ await clearUnlockedSession({ scopeId });
3833
+ await clearQuickUnlock({ scopeId }).catch(() => void 0);
3144
3834
  await clearVaultFromIndexedDb({ scopeId });
3145
3835
  }
3146
3836
 
@@ -3423,6 +4113,6 @@ function sleep(ms) {
3423
4113
  return new Promise((resolve) => setTimeout(resolve, ms));
3424
4114
  }
3425
4115
 
3426
- export { ATTESTATION_TYPE_CITIZENSHIP_APPLICATION, ATTESTATION_TYPE_RECOVERY, ApplicationStatus, BECH32_PREFIX, CHAIN_COIN_TYPE, CitizenTxError, DEFAULT_CHAIN_ID, DEFAULT_REST_URL, DEFAULT_RPC_URL, EMBEDDED_WALLET_KDF_PBKDF2, EMBEDDED_WALLET_KDF_PIN, EMBEDDED_WALLET_KDF_PRF, EMBEDDED_WALLET_KEY_DIRECT_ENTROPY, EMBEDDED_WALLET_KEY_HD_DERIVED, EMBEDDED_WALLET_PASSCODE_MAX_LENGTH, EMBEDDED_WALLET_PASSCODE_MIN_LENGTH, EMBEDDED_WALLET_PASSKEY_PBKDF2_ITERATIONS, EMBEDDED_WALLET_PIN_PBKDF2_ITERATIONS, EMBEDDED_WALLET_RECOVERY_PHRASE_DIRECT_ENTROPY_WARNING, EMBEDDED_WALLET_RECOVERY_PHRASE_GENERAL_WARNING, EMBEDDED_WALLET_VAULT_VERSION, EmbeddedWalletBackupRequiredError, EmbeddedWalletDecryptError, EmbeddedWalletLockoutError, EmbeddedWalletVaultError, EmbeddedWalletWeakPasscodeError, InvalidChainConfigError, MsgClaimReward, MsgSubmitApplication, MsgSubmitRecoveryAttestation, PASSCODE_LOCKOUT_FAILURE_THRESHOLD, PASSCODE_LOCKOUT_INITIAL_MS, PASSCODE_LOCKOUT_MAX_MS, RecoveryAttestationStatus, RestQueryError, RpcConnectionError, UKDR_DECIMALS, UKDR_DISPLAY_DENOM, UKDR_MINIMAL_DENOM, VAULT_LEGACY_KEY, WALLETCONNECT_COSMOS_METHODS, WALLETCONNECT_COSMOS_NAMESPACE, WEBAUTHN_RP_NAME, WalletAccountError, WalletConnectNotInitializedError, WalletConnectSessionError, WalletConnectSigningError, WalletConnectionError, WalletConnectionRejectedError, WalletNotInstalledError, WebAuthnUnavailableError, WebAuthnUserCancelledError, acknowledgeEmbeddedBackup, addressFromHdMnemonic, addressFromPrivateKey, assertPasscodeAllowedForCreation, assertPasscodeUnlockAllowed, assertPasskey, assertValidPasscode, assertValidRecoveryMnemonic, assertVaultPermitsTransaction, attestationSignPayload, broadcastAndConfirmTx, buildClaimRewardMsg, buildKingdomChainInfo, buildRequiredCosmosNamespaces, buildSendUkdrMsg, buildSubmitApplicationMsg, citizenTxErrorMessage, clearEmbeddedWallet, clearPasscodeLockoutRecord, clearVaultFromIndexedDb, computePasscodeLockoutDurationMs, connectCosmostation, connectKeplr, connectLeap, connectWalletConnect, createBrowserSigningClient, createEmbeddedWallet, createHdDerivedPlaintext, createKingdomChainRegistry, createPasskeyMaterialFromAssertion, createPbkdf2Salt, createWalletConnectOfflineSigner, decodeCredentialId, decodeDirectEntropyMnemonic, decryptWalletPayload, defaultEmbeddedHdPath, defaultUkdrTxFee, deriveKekFromPasscode, deriveKekFromPbkdf2, deriveKekFromPrf, derivePrivateKeyFromHdMnemonic, detectRecoveryMnemonicKeyType, detectWebAuthnPrfCapabilities, disconnectWalletConnect, downloadEmbeddedBackupFile, embeddedPlaintextToSigner, enableEmbeddedPasskeyUnlock, encodeCredentialId, encodeDirectEntropyMnemonic, encryptWalletPayload, evaluatePasscodeStrength, exportEncryptedBackup, fetchFeeGrantGranter, fetchUkdrBalance, fetchUnclaimedFinalizedRewards, formatUkdrAsKdr, getCosmostationKeplrApi, getEmbeddedOfflineSigner, getEmbeddedWalletSession, getPasscodeLockoutState, hasEmbeddedWallet, hasLegacyEmbeddedWallet, hasMixedCharacterClasses, importEmbeddedBackup, initWalletConnectClient, isCosmostationAvailable, isKeplrAvailable, isLeapAvailable, loadPasscodeLockoutRecord, loadVaultFromIndexedDb, lockEmbeddedWallet, markVaultBackupAcknowledged, migrateLegacyEmbeddedWallet, normalizePasscode, normalizeRecoveryMnemonic, parseEncryptedBackup, parseKdrToUkdr, parseWalletConnectAccount, plaintextFromRecoveryMnemonic, providerSeedFromHex, publicKeyFromPrivateSeed, readPrfOutput, recordPasscodeUnlockFailure, recordPasscodeUnlockSuccess, recoverEmbeddedWalletFromMnemonic, recoveryMnemonicAddress, recoveryMnemonicFromPlaintext, registerPasskey, resolvePlaintextKeyType, resolveVaultStorageKey, restoreWalletConnectSession, revealEmbeddedRecoveryPhrase, savePasscodeLockoutRecord, saveVaultToIndexedDb, selectEmbeddedWalletKdf, serializeBackupFile, signAttestationPayload, signCitizenshipAttestation, suggestAndConnect, toWalletConnectCosmosChainId, ukdrCoin, unlockEmbeddedWallet, validateChainConfig, vaultUsesPasscodeUnlock, vaultUsesPasskeyUnlock };
4116
+ export { ATTESTATION_TYPE_CITIZENSHIP_APPLICATION, ATTESTATION_TYPE_RECOVERY, ApplicationStatus, BECH32_PREFIX, CHAIN_COIN_TYPE, CitizenTxError, DEFAULT_CHAIN_ID, DEFAULT_REST_URL, DEFAULT_RPC_URL, DEVICE_PIN_PBKDF2_ITERATIONS, EMBEDDED_WALLET_KDF_DEVICE_PIN, EMBEDDED_WALLET_KDF_PBKDF2, EMBEDDED_WALLET_KDF_PIN, EMBEDDED_WALLET_KDF_PRF, EMBEDDED_WALLET_KEY_DIRECT_ENTROPY, EMBEDDED_WALLET_KEY_HD_DERIVED, EMBEDDED_WALLET_PASSCODE_MAX_LENGTH, EMBEDDED_WALLET_PASSCODE_MIN_LENGTH, EMBEDDED_WALLET_PASSKEY_PBKDF2_ITERATIONS, EMBEDDED_WALLET_PIN_PBKDF2_ITERATIONS, EMBEDDED_WALLET_RECOVERY_PHRASE_DIRECT_ENTROPY_WARNING, EMBEDDED_WALLET_RECOVERY_PHRASE_GENERAL_WARNING, EMBEDDED_WALLET_SESSION_TTL_MS, EMBEDDED_WALLET_VAULT_VERSION, EmbeddedWalletBackupRequiredError, EmbeddedWalletDecryptError, EmbeddedWalletLockoutError, EmbeddedWalletPasskeyUnsupportedError, EmbeddedWalletQuickUnlockError, EmbeddedWalletVaultError, EmbeddedWalletWeakPasscodeError, InvalidChainConfigError, MsgClaimReward, MsgSubmitApplication, MsgSubmitRecoveryAttestation, PASSCODE_LOCKOUT_FAILURE_THRESHOLD, PASSCODE_LOCKOUT_INITIAL_MS, PASSCODE_LOCKOUT_MAX_MS, QUICK_UNLOCK_MAX_ATTEMPTS, QUICK_UNLOCK_PBKDF2_ITERATIONS, QUICK_UNLOCK_PIN_LENGTH, QUICK_UNLOCK_TTL_MS, RecoveryAttestationStatus, RestQueryError, RpcConnectionError, UKDR_DECIMALS, UKDR_DISPLAY_DENOM, UKDR_MINIMAL_DENOM, VAULT_LEGACY_KEY, WALLETCONNECT_COSMOS_METHODS, WALLETCONNECT_COSMOS_NAMESPACE, WEBAUTHN_RP_NAME, WalletAccountError, WalletConnectNotInitializedError, WalletConnectSessionError, WalletConnectSigningError, WalletConnectionError, WalletConnectionRejectedError, WalletNotInstalledError, WebAuthnUnavailableError, WebAuthnUserCancelledError, acknowledgeEmbeddedBackup, addressFromHdMnemonic, addressFromPrivateKey, assertPasscodeAllowedForCreation, assertPasscodeUnlockAllowed, assertPasskey, assertQuickUnlockPinAllowed, assertValidPasscode, assertValidRecoveryMnemonic, assertVaultPermitsTransaction, attestationSignPayload, broadcastAndConfirmTx, buildClaimRewardMsg, buildKingdomChainInfo, buildRequiredCosmosNamespaces, buildSendUkdrMsg, buildSubmitApplicationMsg, cacheUnlockedSession, citizenTxErrorMessage, clearEmbeddedQuickUnlockPin, clearEmbeddedWallet, clearPasscodeLockoutRecord, clearQuickUnlock, clearQuickUnlockRecord, clearSessionCacheRecord, clearUnlockedSession, clearVaultFromIndexedDb, computePasscodeLockoutDurationMs, connectCosmostation, connectKeplr, connectLeap, connectWalletConnect, createBrowserSigningClient, createEmbeddedWallet, createHdDerivedPlaintext, createKingdomChainRegistry, createPasskeyMaterialFromAssertion, createPbkdf2Salt, createQuickUnlockRecord, createWalletConnectOfflineSigner, decodeCredentialId, decodeDirectEntropyMnemonic, decryptDevicePinVault, decryptPlaintextWithKek, decryptWalletPayload, defaultEmbeddedHdPath, defaultUkdrTxFee, deriveKekFromPasscode, deriveKekFromPbkdf2, deriveKekFromPrf, derivePinKey, derivePrivateKeyFromHdMnemonic, deserializePlaintext, detectRecoveryMnemonicKeyType, detectWebAuthnPrfCapabilities, disableEmbeddedPasskeyUnlock, disconnectWalletConnect, downloadEmbeddedBackupFile, embeddedPlaintextToSigner, enableEmbeddedPasskeyUnlock, encodeCredentialId, encodeDirectEntropyMnemonic, encryptPlaintextWithDevicePin, encryptPlaintextWithKek, encryptWalletPayload, evaluatePasscodeStrength, exportEncryptedBackup, fetchFeeGrantGranter, fetchUkdrBalance, fetchUnclaimedFinalizedRewards, formatUkdrAsKdr, getCosmostationKeplrApi, getEmbeddedOfflineSigner, getEmbeddedQuickUnlockStatus, getEmbeddedWalletSession, getPasscodeLockoutState, getQuickUnlockStatus, hasEmbeddedWallet, hasLegacyEmbeddedWallet, hasMixedCharacterClasses, importEmbeddedBackup, initWalletConnectClient, isCosmostationAvailable, isKeplrAvailable, isLeapAvailable, isTrivialPin, loadPasscodeLockoutRecord, loadQuickUnlockRecord, loadSessionCacheRecord, loadVaultFromIndexedDb, lockEmbeddedWallet, markVaultBackupAcknowledged, migrateLegacyEmbeddedWallet, normalizePasscode, normalizeRecoveryMnemonic, openQuickUnlockRecord, parseEncryptedBackup, parseKdrToUkdr, parseWalletConnectAccount, plaintextFromRecoveryMnemonic, providerSeedFromHex, publicKeyFromPrivateSeed, readPrfOutput, recordPasscodeUnlockFailure, recordPasscodeUnlockSuccess, recoverEmbeddedWalletFromMnemonic, recoveryMnemonicAddress, recoveryMnemonicFromPlaintext, refreshQuickUnlockExpiry, registerPasskey, resolvePlaintextKeyType, resolveVaultStorageKey, restoreCachedSessionPlaintext, restoreWalletConnectSession, revealEmbeddedRecoveryPhrase, savePasscodeLockoutRecord, saveQuickUnlockRecord, saveSessionCacheRecord, saveVaultToIndexedDb, selectEmbeddedWalletKdf, serializeBackupFile, serializePlaintext, setEmbeddedQuickUnlockPin, setEmbeddedWalletPin, signAttestationPayload, signCitizenshipAttestation, suggestAndConnect, toWalletConnectCosmosChainId, tryRestoreUnlockedSession, ukdrCoin, unlockEmbeddedWallet, unlockEmbeddedWalletWithPasskey, unlockEmbeddedWalletWithPin, validateChainConfig, vaultHasPasskeyUnlock, vaultUsesPasscodeUnlock, vaultUsesPasskeyUnlock, vaultUsesPinUnlock };
3427
4117
  //# sourceMappingURL=index.js.map
3428
4118
  //# sourceMappingURL=index.js.map