decane-connect-kit 2.10.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,
@@ -4041,6 +4178,16 @@ async function createDecaneConnect(config) {
4041
4178
  const candidates = (await storage.listShares()).map(usablePasskeyShare).filter(Boolean);
4042
4179
  return candidates.length === 1 ? candidates[0] : null;
4043
4180
  }
4181
+ async function rememberProfile(userId, profile) {
4182
+ const incoming = Object.fromEntries(Object.entries(profile).filter(([, v]) => v));
4183
+ if (!Object.keys(incoming).length) return;
4184
+ try {
4185
+ const stored = await storage.getShare(userId);
4186
+ if (!stored) return;
4187
+ await storage.putShare({ ...stored, profile: { ...stored.profile, ...incoming } });
4188
+ } catch {
4189
+ }
4190
+ }
4044
4191
  function usablePasskeyShare(stored) {
4045
4192
  if (!stored || stored.unlockMethod !== "passkey") return null;
4046
4193
  if (!stored.credentialId || !stored.prfSalt) return null;
@@ -4078,7 +4225,11 @@ async function createDecaneConnect(config) {
4078
4225
  savePersisted(PERSIST_KEY, persisted);
4079
4226
  events.emit("connected");
4080
4227
  persistLiveSession();
4081
- return { addresses, isNewUser: false };
4228
+ return {
4229
+ addresses,
4230
+ isNewUser: false,
4231
+ ...stored.profile ? { profile: stored.profile } : {}
4232
+ };
4082
4233
  }
4083
4234
  async function handleAuthResult(result) {
4084
4235
  let addresses;
@@ -4094,6 +4245,7 @@ async function createDecaneConnect(config) {
4094
4245
  savePersisted(PERSIST_KEY, persisted);
4095
4246
  events.emit("connected");
4096
4247
  persistLiveSession();
4248
+ if (result.profile) await rememberProfile(result.userId, result.profile);
4097
4249
  return {
4098
4250
  addresses,
4099
4251
  isNewUser: result.isNewUser,
@@ -4207,6 +4359,127 @@ async function createDecaneConnect(config) {
4207
4359
  if (!candidate) throw new NewDeviceError();
4208
4360
  return passkeyLogin(candidate);
4209
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
+ },
4210
4483
  async hasPasskey() {
4211
4484
  if (!auth) return false;
4212
4485
  const stored = await storage.getShare(auth.userId);
@@ -4550,8 +4823,8 @@ async function createDecaneConnect(config) {
4550
4823
  };
4551
4824
  return client;
4552
4825
  }
4553
- function bufferToB64u(buf6) {
4554
- 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, "");
4555
4828
  }
4556
4829
  function b64uToBuffer(b64u) {
4557
4830
  const b644 = b64u.replace(/-/g, "+").replace(/_/g, "/");
@@ -7008,6 +7281,26 @@ function DecaneKit({ config, children }) {
7008
7281
  if (!sdk) throw new Error("Social wallet not connected");
7009
7282
  return sdk.addPasskey();
7010
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
+ }, []);
7011
7304
  const socialHasRecoveryShare = (0, import_react5.useCallback)(async () => {
7012
7305
  const sdk = socialRef.current;
7013
7306
  if (!sdk) throw new Error("Social wallet not connected");
@@ -7071,6 +7364,10 @@ function DecaneKit({ config, children }) {
7071
7364
  socialHasRecoveryShare,
7072
7365
  socialHasPasskey,
7073
7366
  socialAddPasskey,
7367
+ socialSetUnlockPassword,
7368
+ socialCanUnlockWithPassword,
7369
+ socialUnlockWithPassword,
7370
+ socialRemoveUnlockPassword,
7074
7371
  socialShouldPromptRecovery,
7075
7372
  socialDismissRecoveryPrompt,
7076
7373
  socialModalOpen,
@@ -7300,6 +7597,11 @@ function useSocialAuth() {
7300
7597
  sessionExpiresAt: socialAuth.sessionExpiresAt,
7301
7598
  // True when this device can skip the full sign-in and resume with a passkey.
7302
7599
  canUsePasskey: socialAuth.canUsePasskey,
7600
+ // Who the provider said the user is — { name?, email?, picture? }, or null.
7601
+ // A passkey sign-in returns the profile recorded on this device when the
7602
+ // user first signed in, so it carries the same name and email a Google
7603
+ // sign-in does rather than coming back anonymous.
7604
+ profile: socialAuth.profile,
7303
7605
  // Long-running work in progress, for driving your own status UI.
7304
7606
  phase: socialAuth.phase,
7305
7607
  isCreatingWallet: socialAuth.phase === "creating",
@@ -7352,6 +7654,10 @@ function useSocialWallet() {
7352
7654
  socialHasRecoveryShare,
7353
7655
  socialHasPasskey,
7354
7656
  socialAddPasskey,
7657
+ socialSetUnlockPassword,
7658
+ socialCanUnlockWithPassword,
7659
+ socialUnlockWithPassword,
7660
+ socialRemoveUnlockPassword,
7355
7661
  socialShouldPromptRecovery,
7356
7662
  socialDismissRecoveryPrompt,
7357
7663
  openSocialModal,
@@ -7449,6 +7755,37 @@ function useSocialWallet() {
7449
7755
  throw e;
7450
7756
  }
7451
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]);
7452
7789
  const hasRecoveryShare = (0, import_react6.useCallback)(
7453
7790
  () => run(() => socialHasRecoveryShare()),
7454
7791
  [socialHasRecoveryShare]
@@ -7505,6 +7842,13 @@ function useSocialWallet() {
7505
7842
  // Passkey management for THIS device
7506
7843
  hasPasskey,
7507
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,
7508
7852
  // Recovery (2-of-3 Shamir share set)
7509
7853
  rotateShares,
7510
7854
  exportPortableBackup,
@@ -7814,9 +8158,11 @@ function useSignTypedData() {
7814
8158
  InvalidApiKeyError,
7815
8159
  InvalidPinError,
7816
8160
  InvalidRecoveryFileError,
8161
+ MIN_PASSWORD_LENGTH,
7817
8162
  NetworkError,
7818
8163
  NewDeviceError,
7819
8164
  OriginNotAllowedError,
8165
+ PromptPasswordNotConfiguredError,
7820
8166
  PromptPinNotConfiguredError,
7821
8167
  ProvisioningTimeoutError,
7822
8168
  ReconnectRequiredError,
@@ -7827,10 +8173,13 @@ function useSignTypedData() {
7827
8173
  SocialWalletModal,
7828
8174
  StaleDeviceShareError,
7829
8175
  UnlockRequiredError,
8176
+ UnlockTokenRejectedError,
7830
8177
  UserCancelledError,
7831
8178
  WalletSelector,
8179
+ WeakPasswordError,
7832
8180
  WebAuthnNotSupportedError,
7833
8181
  WrongPasswordError,
8182
+ WrongUnlockPasswordError,
7834
8183
  createDecane,
7835
8184
  createDecaneConnect,
7836
8185
  createEthereumProvider,
@@ -7846,6 +8195,7 @@ function useSignTypedData() {
7846
8195
  useSocialBalance,
7847
8196
  useSocialWallet,
7848
8197
  useWalletSelector,
8198
+ validateUnlockPassword,
7849
8199
  verifyEmailCode
7850
8200
  });
7851
8201
  //# sourceMappingURL=index.cjs.map