decane-connect-kit 2.11.0 → 2.12.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.cjs CHANGED
@@ -43,6 +43,7 @@ __export(errors_exports, {
43
43
  NetworkError: () => NetworkError,
44
44
  NewDeviceError: () => NewDeviceError,
45
45
  OriginNotAllowedError: () => OriginNotAllowedError,
46
+ PromptPasswordNotConfiguredError: () => PromptPasswordNotConfiguredError,
46
47
  PromptPinNotConfiguredError: () => PromptPinNotConfiguredError,
47
48
  ProvisioningTimeoutError: () => ProvisioningTimeoutError,
48
49
  ReconnectRequiredError: () => ReconnectRequiredError,
@@ -52,11 +53,13 @@ __export(errors_exports, {
52
53
  ShareCorruptedError: () => ShareCorruptedError,
53
54
  StaleDeviceShareError: () => StaleDeviceShareError,
54
55
  UnlockRequiredError: () => UnlockRequiredError,
56
+ UnlockTokenRejectedError: () => UnlockTokenRejectedError,
55
57
  UserCancelledError: () => UserCancelledError,
56
58
  WebAuthnNotSupportedError: () => WebAuthnNotSupportedError,
57
- WrongPasswordError: () => WrongPasswordError
59
+ WrongPasswordError: () => WrongPasswordError,
60
+ WrongUnlockPasswordError: () => WrongUnlockPasswordError
58
61
  });
59
- var DecaneConnectError, InvalidApiKeyError, OriginNotAllowedError, SessionExpiredError, UnlockRequiredError, ReconnectRequiredError, UserCancelledError, WebAuthnNotSupportedError, InvalidPinError, CallSequenceError, NetworkError, ShareCorruptedError, ProvisioningTimeoutError, NewDeviceError, StaleDeviceShareError, PromptPinNotConfiguredError, AttestationError, RecoveryFailedError, InvalidRecoveryFileError, WrongPasswordError, RecoveryWalletMismatchError, ExportNotAuthorizedError;
62
+ var DecaneConnectError, InvalidApiKeyError, OriginNotAllowedError, SessionExpiredError, UnlockRequiredError, ReconnectRequiredError, UserCancelledError, WebAuthnNotSupportedError, InvalidPinError, CallSequenceError, NetworkError, ShareCorruptedError, ProvisioningTimeoutError, NewDeviceError, StaleDeviceShareError, PromptPinNotConfiguredError, PromptPasswordNotConfiguredError, WrongUnlockPasswordError, UnlockTokenRejectedError, AttestationError, RecoveryFailedError, InvalidRecoveryFileError, WrongPasswordError, RecoveryWalletMismatchError, ExportNotAuthorizedError;
60
63
  var init_errors = __esm({
61
64
  "src/social/errors.ts"() {
62
65
  "use strict";
@@ -151,6 +154,21 @@ var init_errors = __esm({
151
154
  super("PROMPT_PIN_NOT_CONFIGURED", "Passkey unavailable and no promptPin callback was provided in config");
152
155
  }
153
156
  };
157
+ PromptPasswordNotConfiguredError = class extends DecaneConnectError {
158
+ constructor() {
159
+ super("PROMPT_PASSWORD_NOT_CONFIGURED", "This device unlocks with a password and no promptUnlockPassword callback was provided in config");
160
+ }
161
+ };
162
+ WrongUnlockPasswordError = class extends DecaneConnectError {
163
+ constructor() {
164
+ super("WRONG_UNLOCK_PASSWORD", "That password did not unlock this device");
165
+ }
166
+ };
167
+ UnlockTokenRejectedError = class extends DecaneConnectError {
168
+ constructor() {
169
+ super("UNLOCK_TOKEN_REJECTED", "This device's unlock token is no longer valid \u2014 sign in normally to re-enable password unlock");
170
+ }
171
+ };
154
172
  AttestationError = class extends DecaneConnectError {
155
173
  constructor(msg) {
156
174
  super("ATTESTATION_FAILED", msg);
@@ -206,9 +224,11 @@ __export(index_exports, {
206
224
  InvalidApiKeyError: () => InvalidApiKeyError,
207
225
  InvalidPinError: () => InvalidPinError,
208
226
  InvalidRecoveryFileError: () => InvalidRecoveryFileError,
227
+ MIN_PASSWORD_LENGTH: () => MIN_PASSWORD_LENGTH,
209
228
  NetworkError: () => NetworkError,
210
229
  NewDeviceError: () => NewDeviceError,
211
230
  OriginNotAllowedError: () => OriginNotAllowedError,
231
+ PromptPasswordNotConfiguredError: () => PromptPasswordNotConfiguredError,
212
232
  PromptPinNotConfiguredError: () => PromptPinNotConfiguredError,
213
233
  ProvisioningTimeoutError: () => ProvisioningTimeoutError,
214
234
  ReconnectRequiredError: () => ReconnectRequiredError,
@@ -219,10 +239,13 @@ __export(index_exports, {
219
239
  SocialWalletModal: () => SocialWalletModal,
220
240
  StaleDeviceShareError: () => StaleDeviceShareError,
221
241
  UnlockRequiredError: () => UnlockRequiredError,
242
+ UnlockTokenRejectedError: () => UnlockTokenRejectedError,
222
243
  UserCancelledError: () => UserCancelledError,
223
244
  WalletSelector: () => WalletSelector,
245
+ WeakPasswordError: () => WeakPasswordError,
224
246
  WebAuthnNotSupportedError: () => WebAuthnNotSupportedError,
225
247
  WrongPasswordError: () => WrongPasswordError,
248
+ WrongUnlockPasswordError: () => WrongUnlockPasswordError,
226
249
  createDecane: () => createDecane,
227
250
  createDecaneConnect: () => createDecaneConnect,
228
251
  createEthereumProvider: () => createEthereumProvider,
@@ -238,6 +261,7 @@ __export(index_exports, {
238
261
  useSocialBalance: () => useSocialBalance,
239
262
  useSocialWallet: () => useSocialWallet,
240
263
  useWalletSelector: () => useWalletSelector,
264
+ validateUnlockPassword: () => validateUnlockPassword,
241
265
  verifyEmailCode: () => verifyEmailCode
242
266
  });
243
267
  module.exports = __toCommonJS(index_exports);
@@ -2184,6 +2208,32 @@ var ApiClient = class {
2184
2208
  body: JSON.stringify({ userId, response, origin })
2185
2209
  });
2186
2210
  }
2211
+ // ── Device unlock token (the "unlock with your password" credential) ──────
2212
+ /**
2213
+ * Issue this device an unlock token. Returned in the clear once and never
2214
+ * again — the backend stores only its hash — so the caller must encrypt and
2215
+ * persist it before the value is dropped.
2216
+ */
2217
+ async issueUnlockToken(jwt) {
2218
+ return this._request("/auth/unlock-token", {
2219
+ method: "POST",
2220
+ headers: { Authorization: `Bearer ${jwt}` }
2221
+ });
2222
+ }
2223
+ /** Redeem a decrypted unlock token for a session. */
2224
+ async exchangeUnlockToken(token) {
2225
+ return this._request("/auth/unlock-token/exchange", {
2226
+ method: "POST",
2227
+ body: JSON.stringify({ token })
2228
+ });
2229
+ }
2230
+ /** Turn the password path off account-wide, so a lost device stops working. */
2231
+ async revokeUnlockTokens(jwt) {
2232
+ await this._request("/auth/unlock-token", {
2233
+ method: "DELETE",
2234
+ headers: { Authorization: `Bearer ${jwt}` }
2235
+ });
2236
+ }
2187
2237
  async revoke(jwt) {
2188
2238
  await this._request("/auth/revoke", {
2189
2239
  method: "POST",
@@ -2832,16 +2882,86 @@ async function aesGcmDecrypt(key, ciphertext, iv) {
2832
2882
  return new Uint8Array(pt);
2833
2883
  }
2834
2884
 
2835
- // src/social/crypto/pin.ts
2885
+ // src/social/crypto/password.ts
2836
2886
  var PBKDF2_ITERATIONS = 6e5;
2837
2887
  function buf2(u) {
2838
2888
  return u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
2839
2889
  }
2890
+ var MIN_PASSWORD_LENGTH = 12;
2891
+ var TOO_PREDICTABLE = [
2892
+ /^(.)\1+$/,
2893
+ // one repeated character
2894
+ /^0?123456789?0?1?2?$/,
2895
+ // ascending runs
2896
+ /^(?:password|passw0rd)/i,
2897
+ /^(?:qwerty|azerty|asdfgh)/i,
2898
+ /^(?:letmein|welcome|iloveyou|admin)/i
2899
+ ];
2900
+ var WeakPasswordError = class extends Error {
2901
+ constructor(message) {
2902
+ super(message);
2903
+ this.name = "WeakPasswordError";
2904
+ }
2905
+ };
2906
+ function validateUnlockPassword(password) {
2907
+ if (typeof password !== "string" || password.length < MIN_PASSWORD_LENGTH) {
2908
+ throw new WeakPasswordError(
2909
+ `Password must be at least ${MIN_PASSWORD_LENGTH} characters`
2910
+ );
2911
+ }
2912
+ if (/^\d+$/.test(password)) {
2913
+ throw new WeakPasswordError("Password cannot be only digits \u2014 add letters or symbols");
2914
+ }
2915
+ if (/^\s|\s$/.test(password)) {
2916
+ throw new WeakPasswordError("Password cannot start or end with a space");
2917
+ }
2918
+ if (TOO_PREDICTABLE.some((re) => re.test(password))) {
2919
+ throw new WeakPasswordError("Password is too easy to guess \u2014 choose something less common");
2920
+ }
2921
+ }
2922
+ async function derivePasswordKeys(password, salt) {
2923
+ const base = await crypto.subtle.importKey(
2924
+ "raw",
2925
+ buf2(new TextEncoder().encode(password)),
2926
+ "PBKDF2",
2927
+ false,
2928
+ ["deriveBits"]
2929
+ );
2930
+ const master = await crypto.subtle.deriveBits(
2931
+ { name: "PBKDF2", salt: buf2(salt), iterations: PBKDF2_ITERATIONS, hash: "SHA-256" },
2932
+ base,
2933
+ 256
2934
+ );
2935
+ const hkdfKey = await crypto.subtle.importKey("raw", master, "HKDF", false, ["deriveKey"]);
2936
+ const expand = (label) => crypto.subtle.deriveKey(
2937
+ {
2938
+ name: "HKDF",
2939
+ hash: "SHA-256",
2940
+ salt: buf2(salt),
2941
+ info: buf2(new TextEncoder().encode(label))
2942
+ },
2943
+ hkdfKey,
2944
+ { name: "AES-GCM", length: 256 },
2945
+ false,
2946
+ ["encrypt", "decrypt"]
2947
+ );
2948
+ const [shareKey, tokenKey] = await Promise.all([
2949
+ expand("decane:unlock:share"),
2950
+ expand("decane:unlock:token")
2951
+ ]);
2952
+ return { shareKey, tokenKey };
2953
+ }
2954
+
2955
+ // src/social/crypto/pin.ts
2956
+ var PBKDF2_ITERATIONS2 = 6e5;
2957
+ function buf3(u) {
2958
+ return u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
2959
+ }
2840
2960
  async function deriveKeyFromPin(pin, salt) {
2841
2961
  const pinBytes = new TextEncoder().encode(pin);
2842
- const base = await crypto.subtle.importKey("raw", buf2(pinBytes), "PBKDF2", false, ["deriveKey"]);
2962
+ const base = await crypto.subtle.importKey("raw", buf3(pinBytes), "PBKDF2", false, ["deriveKey"]);
2843
2963
  return crypto.subtle.deriveKey(
2844
- { name: "PBKDF2", salt: buf2(salt), iterations: PBKDF2_ITERATIONS, hash: "SHA-256" },
2964
+ { name: "PBKDF2", salt: buf3(salt), iterations: PBKDF2_ITERATIONS2, hash: "SHA-256" },
2845
2965
  base,
2846
2966
  { name: "AES-GCM", length: 256 },
2847
2967
  false,
@@ -2858,7 +2978,7 @@ init_errors();
2858
2978
  function fromB64u(s) {
2859
2979
  return Uint8Array.from(atob(s.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0));
2860
2980
  }
2861
- function buf3(u) {
2981
+ function buf4(u) {
2862
2982
  return u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
2863
2983
  }
2864
2984
  function isPasskeySupported() {
@@ -2889,7 +3009,7 @@ async function authenticateWithPasskey(opts) {
2889
3009
  ...opts.optionsJSON,
2890
3010
  extensions: {
2891
3011
  ...opts.optionsJSON.extensions,
2892
- prf: { eval: { first: buf3(opts.prfSalt) } }
3012
+ prf: { eval: { first: buf4(opts.prfSalt) } }
2893
3013
  }
2894
3014
  };
2895
3015
  let response;
@@ -2913,11 +3033,11 @@ async function getPrfOutput(opts) {
2913
3033
  try {
2914
3034
  cred = await navigator.credentials.get({
2915
3035
  publicKey: {
2916
- challenge: buf3(challenge),
3036
+ challenge: buf4(challenge),
2917
3037
  rpId: opts.rpId,
2918
- allowCredentials: [{ id: buf3(opts.credentialId), type: "public-key" }],
3038
+ allowCredentials: [{ id: buf4(opts.credentialId), type: "public-key" }],
2919
3039
  userVerification: "required",
2920
- extensions: { prf: { eval: { first: buf3(opts.prfSalt) } } }
3040
+ extensions: { prf: { eval: { first: buf4(opts.prfSalt) } } }
2921
3041
  }
2922
3042
  });
2923
3043
  } catch (e) {
@@ -3284,7 +3404,7 @@ async function fetchWrappedShareBackup(api, jwt) {
3284
3404
 
3285
3405
  // src/social/recovery/recoveryShareFile.ts
3286
3406
  init_errors();
3287
- var PBKDF2_ITERATIONS2 = 6e5;
3407
+ var PBKDF2_ITERATIONS3 = 6e5;
3288
3408
  var FILE_VERSION = 2;
3289
3409
  function b642(u) {
3290
3410
  let s = "";
@@ -3294,7 +3414,7 @@ function b642(u) {
3294
3414
  function unb642(s) {
3295
3415
  return Uint8Array.from(atob(s), (c) => c.charCodeAt(0));
3296
3416
  }
3297
- function buf4(u) {
3417
+ function buf5(u) {
3298
3418
  return u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
3299
3419
  }
3300
3420
  async function deriveFileKey(password, salt, iterations) {
@@ -3306,7 +3426,7 @@ async function deriveFileKey(password, salt, iterations) {
3306
3426
  ["deriveKey"]
3307
3427
  );
3308
3428
  return crypto.subtle.deriveKey(
3309
- { name: "PBKDF2", salt: buf4(salt), iterations, hash: "SHA-256" },
3429
+ { name: "PBKDF2", salt: buf5(salt), iterations, hash: "SHA-256" },
3310
3430
  base,
3311
3431
  { name: "AES-GCM", length: 256 },
3312
3432
  false,
@@ -3321,16 +3441,16 @@ async function buildRecoveryShareFile(share, password, addresses, shareSetVersio
3321
3441
  }
3322
3442
  const salt = crypto.getRandomValues(new Uint8Array(16));
3323
3443
  const iv = crypto.getRandomValues(new Uint8Array(12));
3324
- const key = await deriveFileKey(password, salt, PBKDF2_ITERATIONS2);
3444
+ const key = await deriveFileKey(password, salt, PBKDF2_ITERATIONS3);
3325
3445
  const ct = new Uint8Array(
3326
- await crypto.subtle.encrypt({ name: "AES-GCM", iv: buf4(iv) }, key, buf4(share))
3446
+ await crypto.subtle.encrypt({ name: "AES-GCM", iv: buf5(iv) }, key, buf5(share))
3327
3447
  );
3328
3448
  return {
3329
3449
  version: FILE_VERSION,
3330
3450
  type: "decane-recovery-share",
3331
3451
  shareSetVersion,
3332
3452
  kdf: "pbkdf2-sha256",
3333
- kdfParams: { iterations: PBKDF2_ITERATIONS2, salt: b642(salt) },
3453
+ kdfParams: { iterations: PBKDF2_ITERATIONS3, salt: b642(salt) },
3334
3454
  cipher: "aes-256-gcm",
3335
3455
  iv: b642(iv),
3336
3456
  ciphertext: b642(ct),
@@ -3361,7 +3481,7 @@ async function decryptRecoveryShareFile(file, password) {
3361
3481
  let share;
3362
3482
  try {
3363
3483
  share = new Uint8Array(
3364
- await crypto.subtle.decrypt({ name: "AES-GCM", iv: buf4(iv) }, key, buf4(ct))
3484
+ await crypto.subtle.decrypt({ name: "AES-GCM", iv: buf5(iv) }, key, buf5(ct))
3365
3485
  );
3366
3486
  } catch {
3367
3487
  throw new WrongPasswordError();
@@ -3436,14 +3556,14 @@ async function recoverDeviceShare(input) {
3436
3556
 
3437
3557
  // src/social/recovery/portableBackup.ts
3438
3558
  init_errors();
3439
- var PBKDF2_ITERATIONS3 = 6e5;
3559
+ var PBKDF2_ITERATIONS4 = 6e5;
3440
3560
  var FILE_VERSION2 = 1;
3441
3561
  function b643(u) {
3442
3562
  let s = "";
3443
3563
  for (const b of u) s += String.fromCharCode(b);
3444
3564
  return btoa(s);
3445
3565
  }
3446
- function buf5(u) {
3566
+ function buf6(u) {
3447
3567
  return u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
3448
3568
  }
3449
3569
  async function deriveFileKey2(password, salt, iterations) {
@@ -3455,7 +3575,7 @@ async function deriveFileKey2(password, salt, iterations) {
3455
3575
  ["deriveKey"]
3456
3576
  );
3457
3577
  return crypto.subtle.deriveKey(
3458
- { name: "PBKDF2", salt: buf5(salt), iterations, hash: "SHA-256" },
3578
+ { name: "PBKDF2", salt: buf6(salt), iterations, hash: "SHA-256" },
3459
3579
  base,
3460
3580
  { name: "AES-GCM", length: 256 },
3461
3581
  false,
@@ -3475,16 +3595,16 @@ async function buildPortableBackup(deviceShare, recoveryShare, password, address
3475
3595
  );
3476
3596
  const salt = crypto.getRandomValues(new Uint8Array(16));
3477
3597
  const iv = crypto.getRandomValues(new Uint8Array(12));
3478
- const key = await deriveFileKey2(password, salt, PBKDF2_ITERATIONS3);
3598
+ const key = await deriveFileKey2(password, salt, PBKDF2_ITERATIONS4);
3479
3599
  const ct = new Uint8Array(
3480
- await crypto.subtle.encrypt({ name: "AES-GCM", iv: buf5(iv) }, key, buf5(plaintext))
3600
+ await crypto.subtle.encrypt({ name: "AES-GCM", iv: buf6(iv) }, key, buf6(plaintext))
3481
3601
  );
3482
3602
  return {
3483
3603
  version: FILE_VERSION2,
3484
3604
  type: "decane-portable-backup",
3485
3605
  shareSetVersion,
3486
3606
  kdf: "pbkdf2-sha256",
3487
- kdfParams: { iterations: PBKDF2_ITERATIONS3, salt: b643(salt) },
3607
+ kdfParams: { iterations: PBKDF2_ITERATIONS4, salt: b643(salt) },
3488
3608
  cipher: "aes-256-gcm",
3489
3609
  iv: b643(iv),
3490
3610
  ciphertext: b643(ct),
@@ -3691,7 +3811,24 @@ async function createDecaneConnect(config) {
3691
3811
  const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
3692
3812
  await storage.putShare({ ...stored, wrappedDeviceShare, iv });
3693
3813
  }
3814
+ function hasPasswordUnlock(stored) {
3815
+ return !!stored && stored.unlockMethod === "password" && !!stored.passwordSalt && !!stored.wrappedUnlockToken && !!stored.unlockTokenIv;
3816
+ }
3817
+ async function passwordDeviceCandidate() {
3818
+ if (persisted?.userId) {
3819
+ const stored = await storage.getShare(persisted.userId);
3820
+ return hasPasswordUnlock(stored) ? stored : null;
3821
+ }
3822
+ const candidates = (await storage.listShares()).filter(hasPasswordUnlock);
3823
+ return candidates.length === 1 ? candidates[0] : null;
3824
+ }
3694
3825
  async function getWrappingKey(stored) {
3826
+ if (stored.unlockMethod === "password") {
3827
+ if (!config.promptUnlockPassword) throw new PromptPasswordNotConfiguredError();
3828
+ const password = await config.promptUnlockPassword({ kind: "unlock" });
3829
+ const { shareKey } = await derivePasswordKeys(password, stored.passwordSalt);
3830
+ return shareKey;
3831
+ }
3695
3832
  if (stored.unlockMethod === "passkey") {
3696
3833
  const prfOutput = await getPrfOutput({
3697
3834
  rpId,
@@ -4222,6 +4359,127 @@ async function createDecaneConnect(config) {
4222
4359
  if (!candidate) throw new NewDeviceError();
4223
4360
  return passkeyLogin(candidate);
4224
4361
  },
4362
+ /**
4363
+ * Turn on "unlock with your password" for this device.
4364
+ *
4365
+ * Re-wraps the device share under the password and stores an unlock token
4366
+ * encrypted beside it, replacing whatever wrapping was there. Requires a
4367
+ * live session, because issuing the token needs a JWT.
4368
+ *
4369
+ * The share is only overwritten once the new ciphertext is in hand, so a
4370
+ * failure part-way leaves the device exactly as it was rather than holding
4371
+ * a share nothing can open.
4372
+ */
4373
+ async setUnlockPassword(password) {
4374
+ if (!auth) throw new ReconnectRequiredError();
4375
+ validateUnlockPassword(password);
4376
+ const stored = await storage.getShare(auth.userId);
4377
+ if (!stored) throw new NewDeviceError();
4378
+ const deviceShare = await aesGcmDecrypt(
4379
+ await getWrappingKey(stored),
4380
+ stored.wrappedDeviceShare,
4381
+ stored.iv
4382
+ );
4383
+ try {
4384
+ const passwordSalt = generateRandomBytes(16);
4385
+ const { shareKey, tokenKey } = await derivePasswordKeys(password, passwordSalt);
4386
+ const { token } = await api.issueUnlockToken(auth.jwt);
4387
+ const unlockTokenIv = generateRandomBytes(12);
4388
+ const wrappedUnlockToken = await aesGcmEncrypt(
4389
+ tokenKey,
4390
+ new TextEncoder().encode(token),
4391
+ unlockTokenIv
4392
+ );
4393
+ const iv = generateRandomBytes(12);
4394
+ const wrappedDeviceShare = await aesGcmEncrypt(shareKey, deviceShare, iv);
4395
+ await storage.putShare({
4396
+ ...stored,
4397
+ unlockMethod: "password",
4398
+ wrappedDeviceShare,
4399
+ iv,
4400
+ passwordSalt,
4401
+ passwordIterations: PBKDF2_ITERATIONS,
4402
+ wrappedUnlockToken,
4403
+ unlockTokenIv,
4404
+ // The old wrapping's metadata would otherwise sit there describing a
4405
+ // key that no longer opens anything.
4406
+ credentialId: void 0,
4407
+ prfSalt: void 0,
4408
+ pinSalt: void 0,
4409
+ pinIterations: void 0
4410
+ });
4411
+ } finally {
4412
+ zeroize(deviceShare);
4413
+ }
4414
+ },
4415
+ /** True when this device can open a session from a password alone. */
4416
+ async canUnlockWithPassword() {
4417
+ return await passwordDeviceCandidate() !== null;
4418
+ },
4419
+ /**
4420
+ * Open a session from the password alone — no identity provider, no passkey.
4421
+ *
4422
+ * The password decrypts the unlock token, the token buys a JWT, and the
4423
+ * same password unwraps the device share. Both halves come from one
4424
+ * derivation, so one prompt does the whole job.
4425
+ */
4426
+ async unlockWithPassword(password) {
4427
+ const stored = await passwordDeviceCandidate();
4428
+ if (!stored) throw new NewDeviceError();
4429
+ events.emit("wallet-unlocking");
4430
+ const { shareKey, tokenKey } = await derivePasswordKeys(password, stored.passwordSalt);
4431
+ let token;
4432
+ try {
4433
+ token = new TextDecoder().decode(
4434
+ await aesGcmDecrypt(tokenKey, stored.wrappedUnlockToken, stored.unlockTokenIv)
4435
+ );
4436
+ } catch {
4437
+ throw new WrongUnlockPasswordError();
4438
+ }
4439
+ let jwt;
4440
+ try {
4441
+ ({ jwt } = await api.exchangeUnlockToken(token));
4442
+ } catch (e) {
4443
+ if (e instanceof DecaneConnectError && e.code === "NETWORK") throw e;
4444
+ throw new UnlockTokenRejectedError();
4445
+ }
4446
+ const deviceShare = await aesGcmDecrypt(shareKey, stored.wrappedDeviceShare, stored.iv);
4447
+ const rawAddresses = await api.getAddresses(jwt);
4448
+ const recordedAddresses = rawAddresses.evm && rawAddresses.solana ? {
4449
+ evm: rawAddresses.evm,
4450
+ solana: rawAddresses.solana,
4451
+ tron: rawAddresses.tron ?? evmToTronAddress2(rawAddresses.evm)
4452
+ } : null;
4453
+ const addresses = await session.start(deviceShare, jwt, recordedAddresses);
4454
+ zeroize(deviceShare);
4455
+ auth = { jwt, userId: stored.userId, addresses };
4456
+ persisted = { userId: stored.userId, addresses };
4457
+ savePersisted(PERSIST_KEY, persisted);
4458
+ events.emit("connected");
4459
+ persistLiveSession();
4460
+ return {
4461
+ addresses,
4462
+ isNewUser: false,
4463
+ ...stored.profile ? { profile: stored.profile } : {}
4464
+ };
4465
+ },
4466
+ /**
4467
+ * Turn the password path off.
4468
+ *
4469
+ * Revokes the token account-wide first — that is what makes a lost device
4470
+ * safe, and it must happen even if this device's local cleanup fails.
4471
+ */
4472
+ async removeUnlockPassword() {
4473
+ if (!auth) throw new ReconnectRequiredError();
4474
+ await api.revokeUnlockTokens(auth.jwt);
4475
+ const stored = await storage.getShare(auth.userId);
4476
+ if (!stored || stored.unlockMethod !== "password") return;
4477
+ await storage.putShare({
4478
+ ...stored,
4479
+ wrappedUnlockToken: void 0,
4480
+ unlockTokenIv: void 0
4481
+ });
4482
+ },
4225
4483
  async hasPasskey() {
4226
4484
  if (!auth) return false;
4227
4485
  const stored = await storage.getShare(auth.userId);
@@ -4565,8 +4823,8 @@ async function createDecaneConnect(config) {
4565
4823
  };
4566
4824
  return client;
4567
4825
  }
4568
- function bufferToB64u(buf6) {
4569
- return btoa(String.fromCharCode(...buf6)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
4826
+ function bufferToB64u(buf7) {
4827
+ return btoa(String.fromCharCode(...buf7)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
4570
4828
  }
4571
4829
  function b64uToBuffer(b64u) {
4572
4830
  const b644 = b64u.replace(/-/g, "+").replace(/_/g, "/");
@@ -7023,6 +7281,26 @@ function DecaneKit({ config, children }) {
7023
7281
  if (!sdk) throw new Error("Social wallet not connected");
7024
7282
  return sdk.addPasskey();
7025
7283
  }, []);
7284
+ const socialSetUnlockPassword = (0, import_react5.useCallback)(async (password) => {
7285
+ const sdk = socialRef.current;
7286
+ if (!sdk) throw new Error("Social wallet not connected");
7287
+ return sdk.setUnlockPassword(password);
7288
+ }, []);
7289
+ const socialCanUnlockWithPassword = (0, import_react5.useCallback)(async () => {
7290
+ const sdk = socialRef.current;
7291
+ if (!sdk) return false;
7292
+ return sdk.canUnlockWithPassword();
7293
+ }, []);
7294
+ const socialUnlockWithPassword = (0, import_react5.useCallback)(async (password) => {
7295
+ const sdk = socialRef.current;
7296
+ if (!sdk) throw new Error("Social wallet not connected");
7297
+ applyResult(await sdk.unlockWithPassword(password));
7298
+ }, []);
7299
+ const socialRemoveUnlockPassword = (0, import_react5.useCallback)(async () => {
7300
+ const sdk = socialRef.current;
7301
+ if (!sdk) throw new Error("Social wallet not connected");
7302
+ return sdk.removeUnlockPassword();
7303
+ }, []);
7026
7304
  const socialHasRecoveryShare = (0, import_react5.useCallback)(async () => {
7027
7305
  const sdk = socialRef.current;
7028
7306
  if (!sdk) throw new Error("Social wallet not connected");
@@ -7086,6 +7364,10 @@ function DecaneKit({ config, children }) {
7086
7364
  socialHasRecoveryShare,
7087
7365
  socialHasPasskey,
7088
7366
  socialAddPasskey,
7367
+ socialSetUnlockPassword,
7368
+ socialCanUnlockWithPassword,
7369
+ socialUnlockWithPassword,
7370
+ socialRemoveUnlockPassword,
7089
7371
  socialShouldPromptRecovery,
7090
7372
  socialDismissRecoveryPrompt,
7091
7373
  socialModalOpen,
@@ -7372,6 +7654,10 @@ function useSocialWallet() {
7372
7654
  socialHasRecoveryShare,
7373
7655
  socialHasPasskey,
7374
7656
  socialAddPasskey,
7657
+ socialSetUnlockPassword,
7658
+ socialCanUnlockWithPassword,
7659
+ socialUnlockWithPassword,
7660
+ socialRemoveUnlockPassword,
7375
7661
  socialShouldPromptRecovery,
7376
7662
  socialDismissRecoveryPrompt,
7377
7663
  openSocialModal,
@@ -7469,6 +7755,37 @@ function useSocialWallet() {
7469
7755
  throw e;
7470
7756
  }
7471
7757
  }, [socialAddPasskey]);
7758
+ const setUnlockPassword = (0, import_react6.useCallback)(async (password) => {
7759
+ setError(null);
7760
+ try {
7761
+ await socialSetUnlockPassword(password);
7762
+ } catch (e) {
7763
+ setError(e.message);
7764
+ throw e;
7765
+ }
7766
+ }, [socialSetUnlockPassword]);
7767
+ const canUnlockWithPassword = (0, import_react6.useCallback)(
7768
+ () => socialCanUnlockWithPassword(),
7769
+ [socialCanUnlockWithPassword]
7770
+ );
7771
+ const unlockWithPassword = (0, import_react6.useCallback)(async (password) => {
7772
+ setError(null);
7773
+ try {
7774
+ await socialUnlockWithPassword(password);
7775
+ } catch (e) {
7776
+ setError(e.message);
7777
+ throw e;
7778
+ }
7779
+ }, [socialUnlockWithPassword]);
7780
+ const removeUnlockPassword = (0, import_react6.useCallback)(async () => {
7781
+ setError(null);
7782
+ try {
7783
+ await socialRemoveUnlockPassword();
7784
+ } catch (e) {
7785
+ setError(e.message);
7786
+ throw e;
7787
+ }
7788
+ }, [socialRemoveUnlockPassword]);
7472
7789
  const hasRecoveryShare = (0, import_react6.useCallback)(
7473
7790
  () => run(() => socialHasRecoveryShare()),
7474
7791
  [socialHasRecoveryShare]
@@ -7525,6 +7842,13 @@ function useSocialWallet() {
7525
7842
  // Passkey management for THIS device
7526
7843
  hasPasskey,
7527
7844
  addPasskey,
7845
+ // "Unlock with your password": setUnlockPassword needs a live session;
7846
+ // canUnlockWithPassword and unlockWithPassword are the signed-out pair that
7847
+ // open one without an identity provider or a passkey.
7848
+ setUnlockPassword,
7849
+ canUnlockWithPassword,
7850
+ unlockWithPassword,
7851
+ removeUnlockPassword,
7528
7852
  // Recovery (2-of-3 Shamir share set)
7529
7853
  rotateShares,
7530
7854
  exportPortableBackup,
@@ -7834,9 +8158,11 @@ function useSignTypedData() {
7834
8158
  InvalidApiKeyError,
7835
8159
  InvalidPinError,
7836
8160
  InvalidRecoveryFileError,
8161
+ MIN_PASSWORD_LENGTH,
7837
8162
  NetworkError,
7838
8163
  NewDeviceError,
7839
8164
  OriginNotAllowedError,
8165
+ PromptPasswordNotConfiguredError,
7840
8166
  PromptPinNotConfiguredError,
7841
8167
  ProvisioningTimeoutError,
7842
8168
  ReconnectRequiredError,
@@ -7847,10 +8173,13 @@ function useSignTypedData() {
7847
8173
  SocialWalletModal,
7848
8174
  StaleDeviceShareError,
7849
8175
  UnlockRequiredError,
8176
+ UnlockTokenRejectedError,
7850
8177
  UserCancelledError,
7851
8178
  WalletSelector,
8179
+ WeakPasswordError,
7852
8180
  WebAuthnNotSupportedError,
7853
8181
  WrongPasswordError,
8182
+ WrongUnlockPasswordError,
7854
8183
  createDecane,
7855
8184
  createDecaneConnect,
7856
8185
  createEthereumProvider,
@@ -7866,6 +8195,7 @@ function useSignTypedData() {
7866
8195
  useSocialBalance,
7867
8196
  useSocialWallet,
7868
8197
  useWalletSelector,
8198
+ validateUnlockPassword,
7869
8199
  verifyEmailCode
7870
8200
  });
7871
8201
  //# sourceMappingURL=index.cjs.map