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.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, "/");
@@ -6954,6 +7206,26 @@ function DecaneKit({ config, children }) {
6954
7206
  if (!sdk) throw new Error("Social wallet not connected");
6955
7207
  return sdk.addPasskey();
6956
7208
  }, []);
7209
+ const socialSetUnlockPassword = useCallback3(async (password) => {
7210
+ const sdk = socialRef.current;
7211
+ if (!sdk) throw new Error("Social wallet not connected");
7212
+ return sdk.setUnlockPassword(password);
7213
+ }, []);
7214
+ const socialCanUnlockWithPassword = useCallback3(async () => {
7215
+ const sdk = socialRef.current;
7216
+ if (!sdk) return false;
7217
+ return sdk.canUnlockWithPassword();
7218
+ }, []);
7219
+ const socialUnlockWithPassword = useCallback3(async (password) => {
7220
+ const sdk = socialRef.current;
7221
+ if (!sdk) throw new Error("Social wallet not connected");
7222
+ applyResult(await sdk.unlockWithPassword(password));
7223
+ }, []);
7224
+ const socialRemoveUnlockPassword = useCallback3(async () => {
7225
+ const sdk = socialRef.current;
7226
+ if (!sdk) throw new Error("Social wallet not connected");
7227
+ return sdk.removeUnlockPassword();
7228
+ }, []);
6957
7229
  const socialHasRecoveryShare = useCallback3(async () => {
6958
7230
  const sdk = socialRef.current;
6959
7231
  if (!sdk) throw new Error("Social wallet not connected");
@@ -7017,6 +7289,10 @@ function DecaneKit({ config, children }) {
7017
7289
  socialHasRecoveryShare,
7018
7290
  socialHasPasskey,
7019
7291
  socialAddPasskey,
7292
+ socialSetUnlockPassword,
7293
+ socialCanUnlockWithPassword,
7294
+ socialUnlockWithPassword,
7295
+ socialRemoveUnlockPassword,
7020
7296
  socialShouldPromptRecovery,
7021
7297
  socialDismissRecoveryPrompt,
7022
7298
  socialModalOpen,
@@ -7303,6 +7579,10 @@ function useSocialWallet() {
7303
7579
  socialHasRecoveryShare,
7304
7580
  socialHasPasskey,
7305
7581
  socialAddPasskey,
7582
+ socialSetUnlockPassword,
7583
+ socialCanUnlockWithPassword,
7584
+ socialUnlockWithPassword,
7585
+ socialRemoveUnlockPassword,
7306
7586
  socialShouldPromptRecovery,
7307
7587
  socialDismissRecoveryPrompt,
7308
7588
  openSocialModal,
@@ -7400,6 +7680,37 @@ function useSocialWallet() {
7400
7680
  throw e;
7401
7681
  }
7402
7682
  }, [socialAddPasskey]);
7683
+ const setUnlockPassword = useCallback4(async (password) => {
7684
+ setError(null);
7685
+ try {
7686
+ await socialSetUnlockPassword(password);
7687
+ } catch (e) {
7688
+ setError(e.message);
7689
+ throw e;
7690
+ }
7691
+ }, [socialSetUnlockPassword]);
7692
+ const canUnlockWithPassword = useCallback4(
7693
+ () => socialCanUnlockWithPassword(),
7694
+ [socialCanUnlockWithPassword]
7695
+ );
7696
+ const unlockWithPassword = useCallback4(async (password) => {
7697
+ setError(null);
7698
+ try {
7699
+ await socialUnlockWithPassword(password);
7700
+ } catch (e) {
7701
+ setError(e.message);
7702
+ throw e;
7703
+ }
7704
+ }, [socialUnlockWithPassword]);
7705
+ const removeUnlockPassword = useCallback4(async () => {
7706
+ setError(null);
7707
+ try {
7708
+ await socialRemoveUnlockPassword();
7709
+ } catch (e) {
7710
+ setError(e.message);
7711
+ throw e;
7712
+ }
7713
+ }, [socialRemoveUnlockPassword]);
7403
7714
  const hasRecoveryShare = useCallback4(
7404
7715
  () => run(() => socialHasRecoveryShare()),
7405
7716
  [socialHasRecoveryShare]
@@ -7456,6 +7767,13 @@ function useSocialWallet() {
7456
7767
  // Passkey management for THIS device
7457
7768
  hasPasskey,
7458
7769
  addPasskey,
7770
+ // "Unlock with your password": setUnlockPassword needs a live session;
7771
+ // canUnlockWithPassword and unlockWithPassword are the signed-out pair that
7772
+ // open one without an identity provider or a passkey.
7773
+ setUnlockPassword,
7774
+ canUnlockWithPassword,
7775
+ unlockWithPassword,
7776
+ removeUnlockPassword,
7459
7777
  // Recovery (2-of-3 Shamir share set)
7460
7778
  rotateShares,
7461
7779
  exportPortableBackup,
@@ -7764,9 +8082,11 @@ export {
7764
8082
  InvalidApiKeyError,
7765
8083
  InvalidPinError,
7766
8084
  InvalidRecoveryFileError,
8085
+ MIN_PASSWORD_LENGTH,
7767
8086
  NetworkError,
7768
8087
  NewDeviceError,
7769
8088
  OriginNotAllowedError,
8089
+ PromptPasswordNotConfiguredError,
7770
8090
  PromptPinNotConfiguredError,
7771
8091
  ProvisioningTimeoutError,
7772
8092
  ReconnectRequiredError,
@@ -7777,10 +8097,13 @@ export {
7777
8097
  SocialWalletModal,
7778
8098
  StaleDeviceShareError,
7779
8099
  UnlockRequiredError,
8100
+ UnlockTokenRejectedError,
7780
8101
  UserCancelledError,
7781
8102
  WalletSelector,
8103
+ WeakPasswordError,
7782
8104
  WebAuthnNotSupportedError,
7783
8105
  WrongPasswordError,
8106
+ WrongUnlockPasswordError,
7784
8107
  createDecane,
7785
8108
  createDecaneConnect,
7786
8109
  createEthereumProvider,
@@ -7796,6 +8119,7 @@ export {
7796
8119
  useSocialBalance,
7797
8120
  useSocialWallet,
7798
8121
  useWalletSelector,
8122
+ validateUnlockPassword,
7799
8123
  verifyEmailCode
7800
8124
  };
7801
8125
  //# sourceMappingURL=index.js.map