decane-connect-kit 2.11.0 → 2.12.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
@@ -21,6 +21,7 @@ __export(errors_exports, {
21
21
  NetworkError: () => NetworkError,
22
22
  NewDeviceError: () => NewDeviceError,
23
23
  OriginNotAllowedError: () => OriginNotAllowedError,
24
+ PromptPasswordNotConfiguredError: () => PromptPasswordNotConfiguredError,
24
25
  PromptPinNotConfiguredError: () => PromptPinNotConfiguredError,
25
26
  ProvisioningTimeoutError: () => ProvisioningTimeoutError,
26
27
  ReconnectRequiredError: () => ReconnectRequiredError,
@@ -30,11 +31,13 @@ __export(errors_exports, {
30
31
  ShareCorruptedError: () => ShareCorruptedError,
31
32
  StaleDeviceShareError: () => StaleDeviceShareError,
32
33
  UnlockRequiredError: () => UnlockRequiredError,
34
+ UnlockTokenRejectedError: () => UnlockTokenRejectedError,
33
35
  UserCancelledError: () => UserCancelledError,
34
36
  WebAuthnNotSupportedError: () => WebAuthnNotSupportedError,
35
- WrongPasswordError: () => WrongPasswordError
37
+ WrongPasswordError: () => WrongPasswordError,
38
+ WrongUnlockPasswordError: () => WrongUnlockPasswordError
36
39
  });
37
- var DecaneConnectError, InvalidApiKeyError, OriginNotAllowedError, SessionExpiredError, UnlockRequiredError, ReconnectRequiredError, UserCancelledError, WebAuthnNotSupportedError, InvalidPinError, CallSequenceError, NetworkError, ShareCorruptedError, ProvisioningTimeoutError, NewDeviceError, StaleDeviceShareError, PromptPinNotConfiguredError, AttestationError, RecoveryFailedError, InvalidRecoveryFileError, WrongPasswordError, RecoveryWalletMismatchError, ExportNotAuthorizedError;
40
+ 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;
38
41
  var init_errors = __esm({
39
42
  "src/social/errors.ts"() {
40
43
  "use strict";
@@ -129,6 +132,21 @@ var init_errors = __esm({
129
132
  super("PROMPT_PIN_NOT_CONFIGURED", "Passkey unavailable and no promptPin callback was provided in config");
130
133
  }
131
134
  };
135
+ PromptPasswordNotConfiguredError = class extends DecaneConnectError {
136
+ constructor() {
137
+ super("PROMPT_PASSWORD_NOT_CONFIGURED", "This device unlocks with a password and no promptUnlockPassword callback was provided in config");
138
+ }
139
+ };
140
+ WrongUnlockPasswordError = class extends DecaneConnectError {
141
+ constructor() {
142
+ super("WRONG_UNLOCK_PASSWORD", "That password did not unlock this device");
143
+ }
144
+ };
145
+ UnlockTokenRejectedError = class extends DecaneConnectError {
146
+ constructor() {
147
+ super("UNLOCK_TOKEN_REJECTED", "This device's unlock token is no longer valid \u2014 sign in normally to re-enable password unlock");
148
+ }
149
+ };
132
150
  AttestationError = class extends DecaneConnectError {
133
151
  constructor(msg) {
134
152
  super("ATTESTATION_FAILED", msg);
@@ -2108,6 +2126,32 @@ var ApiClient = class {
2108
2126
  body: JSON.stringify({ userId, response, origin })
2109
2127
  });
2110
2128
  }
2129
+ // ── Device unlock token (the "unlock with your password" credential) ──────
2130
+ /**
2131
+ * Issue this device an unlock token. Returned in the clear once and never
2132
+ * again — the backend stores only its hash — so the caller must encrypt and
2133
+ * persist it before the value is dropped.
2134
+ */
2135
+ async issueUnlockToken(jwt) {
2136
+ return this._request("/auth/unlock-token", {
2137
+ method: "POST",
2138
+ headers: { Authorization: `Bearer ${jwt}` }
2139
+ });
2140
+ }
2141
+ /** Redeem a decrypted unlock token for a session. */
2142
+ async exchangeUnlockToken(token) {
2143
+ return this._request("/auth/unlock-token/exchange", {
2144
+ method: "POST",
2145
+ body: JSON.stringify({ token })
2146
+ });
2147
+ }
2148
+ /** Turn the password path off account-wide, so a lost device stops working. */
2149
+ async revokeUnlockTokens(jwt) {
2150
+ await this._request("/auth/unlock-token", {
2151
+ method: "DELETE",
2152
+ headers: { Authorization: `Bearer ${jwt}` }
2153
+ });
2154
+ }
2111
2155
  async revoke(jwt) {
2112
2156
  await this._request("/auth/revoke", {
2113
2157
  method: "POST",
@@ -2756,16 +2800,86 @@ async function aesGcmDecrypt(key, ciphertext, iv) {
2756
2800
  return new Uint8Array(pt);
2757
2801
  }
2758
2802
 
2759
- // src/social/crypto/pin.ts
2803
+ // src/social/crypto/password.ts
2760
2804
  var PBKDF2_ITERATIONS = 6e5;
2761
2805
  function buf2(u) {
2762
2806
  return u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
2763
2807
  }
2808
+ var MIN_PASSWORD_LENGTH = 12;
2809
+ var TOO_PREDICTABLE = [
2810
+ /^(.)\1+$/,
2811
+ // one repeated character
2812
+ /^0?123456789?0?1?2?$/,
2813
+ // ascending runs
2814
+ /^(?:password|passw0rd)/i,
2815
+ /^(?:qwerty|azerty|asdfgh)/i,
2816
+ /^(?:letmein|welcome|iloveyou|admin)/i
2817
+ ];
2818
+ var WeakPasswordError = class extends Error {
2819
+ constructor(message) {
2820
+ super(message);
2821
+ this.name = "WeakPasswordError";
2822
+ }
2823
+ };
2824
+ function validateUnlockPassword(password) {
2825
+ if (typeof password !== "string" || password.length < MIN_PASSWORD_LENGTH) {
2826
+ throw new WeakPasswordError(
2827
+ `Password must be at least ${MIN_PASSWORD_LENGTH} characters`
2828
+ );
2829
+ }
2830
+ if (/^\d+$/.test(password)) {
2831
+ throw new WeakPasswordError("Password cannot be only digits \u2014 add letters or symbols");
2832
+ }
2833
+ if (/^\s|\s$/.test(password)) {
2834
+ throw new WeakPasswordError("Password cannot start or end with a space");
2835
+ }
2836
+ if (TOO_PREDICTABLE.some((re) => re.test(password))) {
2837
+ throw new WeakPasswordError("Password is too easy to guess \u2014 choose something less common");
2838
+ }
2839
+ }
2840
+ async function derivePasswordKeys(password, salt) {
2841
+ const base = await crypto.subtle.importKey(
2842
+ "raw",
2843
+ buf2(new TextEncoder().encode(password)),
2844
+ "PBKDF2",
2845
+ false,
2846
+ ["deriveBits"]
2847
+ );
2848
+ const master = await crypto.subtle.deriveBits(
2849
+ { name: "PBKDF2", salt: buf2(salt), iterations: PBKDF2_ITERATIONS, hash: "SHA-256" },
2850
+ base,
2851
+ 256
2852
+ );
2853
+ const hkdfKey = await crypto.subtle.importKey("raw", master, "HKDF", false, ["deriveKey"]);
2854
+ const expand = (label) => crypto.subtle.deriveKey(
2855
+ {
2856
+ name: "HKDF",
2857
+ hash: "SHA-256",
2858
+ salt: buf2(salt),
2859
+ info: buf2(new TextEncoder().encode(label))
2860
+ },
2861
+ hkdfKey,
2862
+ { name: "AES-GCM", length: 256 },
2863
+ false,
2864
+ ["encrypt", "decrypt"]
2865
+ );
2866
+ const [shareKey, tokenKey] = await Promise.all([
2867
+ expand("decane:unlock:share"),
2868
+ expand("decane:unlock:token")
2869
+ ]);
2870
+ return { shareKey, tokenKey };
2871
+ }
2872
+
2873
+ // src/social/crypto/pin.ts
2874
+ var PBKDF2_ITERATIONS2 = 6e5;
2875
+ function buf3(u) {
2876
+ return u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
2877
+ }
2764
2878
  async function deriveKeyFromPin(pin, salt) {
2765
2879
  const pinBytes = new TextEncoder().encode(pin);
2766
- const base = await crypto.subtle.importKey("raw", buf2(pinBytes), "PBKDF2", false, ["deriveKey"]);
2880
+ const base = await crypto.subtle.importKey("raw", buf3(pinBytes), "PBKDF2", false, ["deriveKey"]);
2767
2881
  return crypto.subtle.deriveKey(
2768
- { name: "PBKDF2", salt: buf2(salt), iterations: PBKDF2_ITERATIONS, hash: "SHA-256" },
2882
+ { name: "PBKDF2", salt: buf3(salt), iterations: PBKDF2_ITERATIONS2, hash: "SHA-256" },
2769
2883
  base,
2770
2884
  { name: "AES-GCM", length: 256 },
2771
2885
  false,
@@ -2782,7 +2896,7 @@ import { startRegistration, startAuthentication as startAuthentication2 } from "
2782
2896
  function fromB64u(s) {
2783
2897
  return Uint8Array.from(atob(s.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0));
2784
2898
  }
2785
- function buf3(u) {
2899
+ function buf4(u) {
2786
2900
  return u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
2787
2901
  }
2788
2902
  function isPasskeySupported() {
@@ -2813,7 +2927,7 @@ async function authenticateWithPasskey(opts) {
2813
2927
  ...opts.optionsJSON,
2814
2928
  extensions: {
2815
2929
  ...opts.optionsJSON.extensions,
2816
- prf: { eval: { first: buf3(opts.prfSalt) } }
2930
+ prf: { eval: { first: buf4(opts.prfSalt) } }
2817
2931
  }
2818
2932
  };
2819
2933
  let response;
@@ -2837,11 +2951,11 @@ async function getPrfOutput(opts) {
2837
2951
  try {
2838
2952
  cred = await navigator.credentials.get({
2839
2953
  publicKey: {
2840
- challenge: buf3(challenge),
2954
+ challenge: buf4(challenge),
2841
2955
  rpId: opts.rpId,
2842
- allowCredentials: [{ id: buf3(opts.credentialId), type: "public-key" }],
2956
+ allowCredentials: [{ id: buf4(opts.credentialId), type: "public-key" }],
2843
2957
  userVerification: "required",
2844
- extensions: { prf: { eval: { first: buf3(opts.prfSalt) } } }
2958
+ extensions: { prf: { eval: { first: buf4(opts.prfSalt) } } }
2845
2959
  }
2846
2960
  });
2847
2961
  } catch (e) {
@@ -3208,7 +3322,7 @@ async function fetchWrappedShareBackup(api, jwt) {
3208
3322
 
3209
3323
  // src/social/recovery/recoveryShareFile.ts
3210
3324
  init_errors();
3211
- var PBKDF2_ITERATIONS2 = 6e5;
3325
+ var PBKDF2_ITERATIONS3 = 6e5;
3212
3326
  var FILE_VERSION = 2;
3213
3327
  function b642(u) {
3214
3328
  let s = "";
@@ -3218,7 +3332,7 @@ function b642(u) {
3218
3332
  function unb642(s) {
3219
3333
  return Uint8Array.from(atob(s), (c) => c.charCodeAt(0));
3220
3334
  }
3221
- function buf4(u) {
3335
+ function buf5(u) {
3222
3336
  return u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
3223
3337
  }
3224
3338
  async function deriveFileKey(password, salt, iterations) {
@@ -3230,7 +3344,7 @@ async function deriveFileKey(password, salt, iterations) {
3230
3344
  ["deriveKey"]
3231
3345
  );
3232
3346
  return crypto.subtle.deriveKey(
3233
- { name: "PBKDF2", salt: buf4(salt), iterations, hash: "SHA-256" },
3347
+ { name: "PBKDF2", salt: buf5(salt), iterations, hash: "SHA-256" },
3234
3348
  base,
3235
3349
  { name: "AES-GCM", length: 256 },
3236
3350
  false,
@@ -3245,16 +3359,16 @@ async function buildRecoveryShareFile(share, password, addresses, shareSetVersio
3245
3359
  }
3246
3360
  const salt = crypto.getRandomValues(new Uint8Array(16));
3247
3361
  const iv = crypto.getRandomValues(new Uint8Array(12));
3248
- const key = await deriveFileKey(password, salt, PBKDF2_ITERATIONS2);
3362
+ const key = await deriveFileKey(password, salt, PBKDF2_ITERATIONS3);
3249
3363
  const ct = new Uint8Array(
3250
- await crypto.subtle.encrypt({ name: "AES-GCM", iv: buf4(iv) }, key, buf4(share))
3364
+ await crypto.subtle.encrypt({ name: "AES-GCM", iv: buf5(iv) }, key, buf5(share))
3251
3365
  );
3252
3366
  return {
3253
3367
  version: FILE_VERSION,
3254
3368
  type: "decane-recovery-share",
3255
3369
  shareSetVersion,
3256
3370
  kdf: "pbkdf2-sha256",
3257
- kdfParams: { iterations: PBKDF2_ITERATIONS2, salt: b642(salt) },
3371
+ kdfParams: { iterations: PBKDF2_ITERATIONS3, salt: b642(salt) },
3258
3372
  cipher: "aes-256-gcm",
3259
3373
  iv: b642(iv),
3260
3374
  ciphertext: b642(ct),
@@ -3285,7 +3399,7 @@ async function decryptRecoveryShareFile(file, password) {
3285
3399
  let share;
3286
3400
  try {
3287
3401
  share = new Uint8Array(
3288
- await crypto.subtle.decrypt({ name: "AES-GCM", iv: buf4(iv) }, key, buf4(ct))
3402
+ await crypto.subtle.decrypt({ name: "AES-GCM", iv: buf5(iv) }, key, buf5(ct))
3289
3403
  );
3290
3404
  } catch {
3291
3405
  throw new WrongPasswordError();
@@ -3360,14 +3474,14 @@ async function recoverDeviceShare(input) {
3360
3474
 
3361
3475
  // src/social/recovery/portableBackup.ts
3362
3476
  init_errors();
3363
- var PBKDF2_ITERATIONS3 = 6e5;
3477
+ var PBKDF2_ITERATIONS4 = 6e5;
3364
3478
  var FILE_VERSION2 = 1;
3365
3479
  function b643(u) {
3366
3480
  let s = "";
3367
3481
  for (const b of u) s += String.fromCharCode(b);
3368
3482
  return btoa(s);
3369
3483
  }
3370
- function buf5(u) {
3484
+ function buf6(u) {
3371
3485
  return u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
3372
3486
  }
3373
3487
  async function deriveFileKey2(password, salt, iterations) {
@@ -3379,7 +3493,7 @@ async function deriveFileKey2(password, salt, iterations) {
3379
3493
  ["deriveKey"]
3380
3494
  );
3381
3495
  return crypto.subtle.deriveKey(
3382
- { name: "PBKDF2", salt: buf5(salt), iterations, hash: "SHA-256" },
3496
+ { name: "PBKDF2", salt: buf6(salt), iterations, hash: "SHA-256" },
3383
3497
  base,
3384
3498
  { name: "AES-GCM", length: 256 },
3385
3499
  false,
@@ -3399,16 +3513,16 @@ async function buildPortableBackup(deviceShare, recoveryShare, password, address
3399
3513
  );
3400
3514
  const salt = crypto.getRandomValues(new Uint8Array(16));
3401
3515
  const iv = crypto.getRandomValues(new Uint8Array(12));
3402
- const key = await deriveFileKey2(password, salt, PBKDF2_ITERATIONS3);
3516
+ const key = await deriveFileKey2(password, salt, PBKDF2_ITERATIONS4);
3403
3517
  const ct = new Uint8Array(
3404
- await crypto.subtle.encrypt({ name: "AES-GCM", iv: buf5(iv) }, key, buf5(plaintext))
3518
+ await crypto.subtle.encrypt({ name: "AES-GCM", iv: buf6(iv) }, key, buf6(plaintext))
3405
3519
  );
3406
3520
  return {
3407
3521
  version: FILE_VERSION2,
3408
3522
  type: "decane-portable-backup",
3409
3523
  shareSetVersion,
3410
3524
  kdf: "pbkdf2-sha256",
3411
- kdfParams: { iterations: PBKDF2_ITERATIONS3, salt: b643(salt) },
3525
+ kdfParams: { iterations: PBKDF2_ITERATIONS4, salt: b643(salt) },
3412
3526
  cipher: "aes-256-gcm",
3413
3527
  iv: b643(iv),
3414
3528
  ciphertext: b643(ct),
@@ -3615,7 +3729,24 @@ async function createDecaneConnect(config) {
3615
3729
  const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
3616
3730
  await storage.putShare({ ...stored, wrappedDeviceShare, iv });
3617
3731
  }
3732
+ function hasPasswordUnlock(stored) {
3733
+ return !!stored && stored.unlockMethod === "password" && !!stored.passwordSalt && !!stored.wrappedUnlockToken && !!stored.unlockTokenIv;
3734
+ }
3735
+ async function passwordDeviceCandidate() {
3736
+ if (persisted?.userId) {
3737
+ const stored = await storage.getShare(persisted.userId);
3738
+ return hasPasswordUnlock(stored) ? stored : null;
3739
+ }
3740
+ const candidates = (await storage.listShares()).filter(hasPasswordUnlock);
3741
+ return candidates.length === 1 ? candidates[0] : null;
3742
+ }
3618
3743
  async function getWrappingKey(stored) {
3744
+ if (stored.unlockMethod === "password") {
3745
+ if (!config.promptUnlockPassword) throw new PromptPasswordNotConfiguredError();
3746
+ const password = await config.promptUnlockPassword({ kind: "unlock" });
3747
+ const { shareKey } = await derivePasswordKeys(password, stored.passwordSalt);
3748
+ return shareKey;
3749
+ }
3619
3750
  if (stored.unlockMethod === "passkey") {
3620
3751
  const prfOutput = await getPrfOutput({
3621
3752
  rpId,
@@ -4146,6 +4277,127 @@ async function createDecaneConnect(config) {
4146
4277
  if (!candidate) throw new NewDeviceError();
4147
4278
  return passkeyLogin(candidate);
4148
4279
  },
4280
+ /**
4281
+ * Turn on "unlock with your password" for this device.
4282
+ *
4283
+ * Re-wraps the device share under the password and stores an unlock token
4284
+ * encrypted beside it, replacing whatever wrapping was there. Requires a
4285
+ * live session, because issuing the token needs a JWT.
4286
+ *
4287
+ * The share is only overwritten once the new ciphertext is in hand, so a
4288
+ * failure part-way leaves the device exactly as it was rather than holding
4289
+ * a share nothing can open.
4290
+ */
4291
+ async setUnlockPassword(password) {
4292
+ if (!auth) throw new ReconnectRequiredError();
4293
+ validateUnlockPassword(password);
4294
+ const stored = await storage.getShare(auth.userId);
4295
+ if (!stored) throw new NewDeviceError();
4296
+ const deviceShare = await aesGcmDecrypt(
4297
+ await getWrappingKey(stored),
4298
+ stored.wrappedDeviceShare,
4299
+ stored.iv
4300
+ );
4301
+ try {
4302
+ const passwordSalt = generateRandomBytes(16);
4303
+ const { shareKey, tokenKey } = await derivePasswordKeys(password, passwordSalt);
4304
+ const { token } = await api.issueUnlockToken(auth.jwt);
4305
+ const unlockTokenIv = generateRandomBytes(12);
4306
+ const wrappedUnlockToken = await aesGcmEncrypt(
4307
+ tokenKey,
4308
+ new TextEncoder().encode(token),
4309
+ unlockTokenIv
4310
+ );
4311
+ const iv = generateRandomBytes(12);
4312
+ const wrappedDeviceShare = await aesGcmEncrypt(shareKey, deviceShare, iv);
4313
+ await storage.putShare({
4314
+ ...stored,
4315
+ unlockMethod: "password",
4316
+ wrappedDeviceShare,
4317
+ iv,
4318
+ passwordSalt,
4319
+ passwordIterations: PBKDF2_ITERATIONS,
4320
+ wrappedUnlockToken,
4321
+ unlockTokenIv,
4322
+ // The old wrapping's metadata would otherwise sit there describing a
4323
+ // key that no longer opens anything.
4324
+ credentialId: void 0,
4325
+ prfSalt: void 0,
4326
+ pinSalt: void 0,
4327
+ pinIterations: void 0
4328
+ });
4329
+ } finally {
4330
+ zeroize(deviceShare);
4331
+ }
4332
+ },
4333
+ /** True when this device can open a session from a password alone. */
4334
+ async canUnlockWithPassword() {
4335
+ return await passwordDeviceCandidate() !== null;
4336
+ },
4337
+ /**
4338
+ * Open a session from the password alone — no identity provider, no passkey.
4339
+ *
4340
+ * The password decrypts the unlock token, the token buys a JWT, and the
4341
+ * same password unwraps the device share. Both halves come from one
4342
+ * derivation, so one prompt does the whole job.
4343
+ */
4344
+ async unlockWithPassword(password) {
4345
+ const stored = await passwordDeviceCandidate();
4346
+ if (!stored) throw new NewDeviceError();
4347
+ events.emit("wallet-unlocking");
4348
+ const { shareKey, tokenKey } = await derivePasswordKeys(password, stored.passwordSalt);
4349
+ let token;
4350
+ try {
4351
+ token = new TextDecoder().decode(
4352
+ await aesGcmDecrypt(tokenKey, stored.wrappedUnlockToken, stored.unlockTokenIv)
4353
+ );
4354
+ } catch {
4355
+ throw new WrongUnlockPasswordError();
4356
+ }
4357
+ let jwt;
4358
+ try {
4359
+ ({ jwt } = await api.exchangeUnlockToken(token));
4360
+ } catch (e) {
4361
+ if (e instanceof DecaneConnectError && e.code === "NETWORK") throw e;
4362
+ throw new UnlockTokenRejectedError();
4363
+ }
4364
+ const deviceShare = await aesGcmDecrypt(shareKey, stored.wrappedDeviceShare, stored.iv);
4365
+ const rawAddresses = await api.getAddresses(jwt);
4366
+ const recordedAddresses = rawAddresses.evm && rawAddresses.solana ? {
4367
+ evm: rawAddresses.evm,
4368
+ solana: rawAddresses.solana,
4369
+ tron: rawAddresses.tron ?? evmToTronAddress2(rawAddresses.evm)
4370
+ } : null;
4371
+ const addresses = await session.start(deviceShare, jwt, recordedAddresses);
4372
+ zeroize(deviceShare);
4373
+ auth = { jwt, userId: stored.userId, addresses };
4374
+ persisted = { userId: stored.userId, addresses };
4375
+ savePersisted(PERSIST_KEY, persisted);
4376
+ events.emit("connected");
4377
+ persistLiveSession();
4378
+ return {
4379
+ addresses,
4380
+ isNewUser: false,
4381
+ ...stored.profile ? { profile: stored.profile } : {}
4382
+ };
4383
+ },
4384
+ /**
4385
+ * Turn the password path off.
4386
+ *
4387
+ * Revokes the token account-wide first — that is what makes a lost device
4388
+ * safe, and it must happen even if this device's local cleanup fails.
4389
+ */
4390
+ async removeUnlockPassword() {
4391
+ if (!auth) throw new ReconnectRequiredError();
4392
+ await api.revokeUnlockTokens(auth.jwt);
4393
+ const stored = await storage.getShare(auth.userId);
4394
+ if (!stored || stored.unlockMethod !== "password") return;
4395
+ await storage.putShare({
4396
+ ...stored,
4397
+ wrappedUnlockToken: void 0,
4398
+ unlockTokenIv: void 0
4399
+ });
4400
+ },
4149
4401
  async hasPasskey() {
4150
4402
  if (!auth) return false;
4151
4403
  const stored = await storage.getShare(auth.userId);
@@ -4489,8 +4741,8 @@ async function createDecaneConnect(config) {
4489
4741
  };
4490
4742
  return client;
4491
4743
  }
4492
- function bufferToB64u(buf6) {
4493
- return btoa(String.fromCharCode(...buf6)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
4744
+ function bufferToB64u(buf7) {
4745
+ return btoa(String.fromCharCode(...buf7)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
4494
4746
  }
4495
4747
  function b64uToBuffer(b64u) {
4496
4748
  const b644 = b64u.replace(/-/g, "+").replace(/_/g, "/");
@@ -6691,6 +6943,7 @@ function DecaneKit({ config, children }) {
6691
6943
  apiKey,
6692
6944
  authMethods,
6693
6945
  promptPin,
6946
+ promptUnlockPassword,
6694
6947
  apiBaseUrl,
6695
6948
  chains,
6696
6949
  rpcUrls,
@@ -6713,6 +6966,7 @@ function DecaneKit({ config, children }) {
6713
6966
  chains: chains ?? ["evm:1", "solana:mainnet"],
6714
6967
  rpcUrls,
6715
6968
  promptPin,
6969
+ promptUnlockPassword,
6716
6970
  requireAssertionPerSignature,
6717
6971
  offerRecoveryAtSignup,
6718
6972
  onRecoveryShareOffer,
@@ -6954,6 +7208,26 @@ function DecaneKit({ config, children }) {
6954
7208
  if (!sdk) throw new Error("Social wallet not connected");
6955
7209
  return sdk.addPasskey();
6956
7210
  }, []);
7211
+ const socialSetUnlockPassword = useCallback3(async (password) => {
7212
+ const sdk = socialRef.current;
7213
+ if (!sdk) throw new Error("Social wallet not connected");
7214
+ return sdk.setUnlockPassword(password);
7215
+ }, []);
7216
+ const socialCanUnlockWithPassword = useCallback3(async () => {
7217
+ const sdk = socialRef.current;
7218
+ if (!sdk) return false;
7219
+ return sdk.canUnlockWithPassword();
7220
+ }, []);
7221
+ const socialUnlockWithPassword = useCallback3(async (password) => {
7222
+ const sdk = socialRef.current;
7223
+ if (!sdk) throw new Error("Social wallet not connected");
7224
+ applyResult(await sdk.unlockWithPassword(password));
7225
+ }, []);
7226
+ const socialRemoveUnlockPassword = useCallback3(async () => {
7227
+ const sdk = socialRef.current;
7228
+ if (!sdk) throw new Error("Social wallet not connected");
7229
+ return sdk.removeUnlockPassword();
7230
+ }, []);
6957
7231
  const socialHasRecoveryShare = useCallback3(async () => {
6958
7232
  const sdk = socialRef.current;
6959
7233
  if (!sdk) throw new Error("Social wallet not connected");
@@ -7017,6 +7291,10 @@ function DecaneKit({ config, children }) {
7017
7291
  socialHasRecoveryShare,
7018
7292
  socialHasPasskey,
7019
7293
  socialAddPasskey,
7294
+ socialSetUnlockPassword,
7295
+ socialCanUnlockWithPassword,
7296
+ socialUnlockWithPassword,
7297
+ socialRemoveUnlockPassword,
7020
7298
  socialShouldPromptRecovery,
7021
7299
  socialDismissRecoveryPrompt,
7022
7300
  socialModalOpen,
@@ -7303,6 +7581,10 @@ function useSocialWallet() {
7303
7581
  socialHasRecoveryShare,
7304
7582
  socialHasPasskey,
7305
7583
  socialAddPasskey,
7584
+ socialSetUnlockPassword,
7585
+ socialCanUnlockWithPassword,
7586
+ socialUnlockWithPassword,
7587
+ socialRemoveUnlockPassword,
7306
7588
  socialShouldPromptRecovery,
7307
7589
  socialDismissRecoveryPrompt,
7308
7590
  openSocialModal,
@@ -7400,6 +7682,37 @@ function useSocialWallet() {
7400
7682
  throw e;
7401
7683
  }
7402
7684
  }, [socialAddPasskey]);
7685
+ const setUnlockPassword = useCallback4(async (password) => {
7686
+ setError(null);
7687
+ try {
7688
+ await socialSetUnlockPassword(password);
7689
+ } catch (e) {
7690
+ setError(e.message);
7691
+ throw e;
7692
+ }
7693
+ }, [socialSetUnlockPassword]);
7694
+ const canUnlockWithPassword = useCallback4(
7695
+ () => socialCanUnlockWithPassword(),
7696
+ [socialCanUnlockWithPassword]
7697
+ );
7698
+ const unlockWithPassword = useCallback4(async (password) => {
7699
+ setError(null);
7700
+ try {
7701
+ await socialUnlockWithPassword(password);
7702
+ } catch (e) {
7703
+ setError(e.message);
7704
+ throw e;
7705
+ }
7706
+ }, [socialUnlockWithPassword]);
7707
+ const removeUnlockPassword = useCallback4(async () => {
7708
+ setError(null);
7709
+ try {
7710
+ await socialRemoveUnlockPassword();
7711
+ } catch (e) {
7712
+ setError(e.message);
7713
+ throw e;
7714
+ }
7715
+ }, [socialRemoveUnlockPassword]);
7403
7716
  const hasRecoveryShare = useCallback4(
7404
7717
  () => run(() => socialHasRecoveryShare()),
7405
7718
  [socialHasRecoveryShare]
@@ -7456,6 +7769,13 @@ function useSocialWallet() {
7456
7769
  // Passkey management for THIS device
7457
7770
  hasPasskey,
7458
7771
  addPasskey,
7772
+ // "Unlock with your password": setUnlockPassword needs a live session;
7773
+ // canUnlockWithPassword and unlockWithPassword are the signed-out pair that
7774
+ // open one without an identity provider or a passkey.
7775
+ setUnlockPassword,
7776
+ canUnlockWithPassword,
7777
+ unlockWithPassword,
7778
+ removeUnlockPassword,
7459
7779
  // Recovery (2-of-3 Shamir share set)
7460
7780
  rotateShares,
7461
7781
  exportPortableBackup,
@@ -7764,9 +8084,11 @@ export {
7764
8084
  InvalidApiKeyError,
7765
8085
  InvalidPinError,
7766
8086
  InvalidRecoveryFileError,
8087
+ MIN_PASSWORD_LENGTH,
7767
8088
  NetworkError,
7768
8089
  NewDeviceError,
7769
8090
  OriginNotAllowedError,
8091
+ PromptPasswordNotConfiguredError,
7770
8092
  PromptPinNotConfiguredError,
7771
8093
  ProvisioningTimeoutError,
7772
8094
  ReconnectRequiredError,
@@ -7777,10 +8099,13 @@ export {
7777
8099
  SocialWalletModal,
7778
8100
  StaleDeviceShareError,
7779
8101
  UnlockRequiredError,
8102
+ UnlockTokenRejectedError,
7780
8103
  UserCancelledError,
7781
8104
  WalletSelector,
8105
+ WeakPasswordError,
7782
8106
  WebAuthnNotSupportedError,
7783
8107
  WrongPasswordError,
8108
+ WrongUnlockPasswordError,
7784
8109
  createDecane,
7785
8110
  createDecaneConnect,
7786
8111
  createEthereumProvider,
@@ -7796,6 +8121,7 @@ export {
7796
8121
  useSocialBalance,
7797
8122
  useSocialWallet,
7798
8123
  useWalletSelector,
8124
+ validateUnlockPassword,
7799
8125
  verifyEmailCode
7800
8126
  };
7801
8127
  //# sourceMappingURL=index.js.map