decane-connect-kit 2.21.0 → 2.23.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.d.cts CHANGED
@@ -146,6 +146,19 @@ interface DecaneConnectConfig {
146
146
  * page load (stricter, but the reload friction that motivated this returns).
147
147
  */
148
148
  persistSession?: boolean;
149
+ /**
150
+ * Do not wrap and store this device's share at wallet creation or when a
151
+ * device is first provisioned — so sign-in asks for no passkey and no
152
+ * password. The session works without a stored share: signing runs on the
153
+ * open session, and a later visit provisions a fresh share against a live
154
+ * sign-in. What the device loses until it is protected is the short way
155
+ * back in (passkey / password unlock) — every visit is a full sign-in.
156
+ *
157
+ * Call `protectDevice()` at the moment the app wants that step, typically
158
+ * before the user's first transaction. `deviceProtected()` says whether it
159
+ * has happened. Default false: the share is wrapped and stored at creation.
160
+ */
161
+ deferDeviceProtection?: boolean;
149
162
  /**
150
163
  * Offer to save a recovery-share file at signup. **Default false** since
151
164
  * 2.7.4 — opt in explicitly with `true`.
@@ -398,6 +411,14 @@ interface DecaneConnect {
398
411
  setUnlockPassword(password: string): Promise<void>;
399
412
  /** True when this device can open a session from a password alone. */
400
413
  canUnlockWithPassword(): Promise<boolean>;
414
+ /**
415
+ * "Use a different account." disconnect() keeps one fact about the account
416
+ * that signed out — its userId — so canSignInWithPasskey() and
417
+ * canUnlockWithPassword() can offer that account's own way back in even
418
+ * when several accounts are enrolled on this browser. This drops it. The
419
+ * device shares themselves are untouched.
420
+ */
421
+ forgetLastUser(): void;
401
422
  /** Open a session from the password alone — no provider, no passkey. */
402
423
  unlockWithPassword(password: string): Promise<ConnectResult>;
403
424
  /** Turn the password path off, revoking the token account-wide. */
@@ -420,6 +441,21 @@ interface DecaneConnect {
420
441
  * on the way).
421
442
  */
422
443
  addPasskey(): Promise<void>;
444
+ /**
445
+ * Whether this device holds a wrapped share for the signed-in account, i.e.
446
+ * whether a passkey or password protects the wallet here. False on a device
447
+ * created with `deferDeviceProtection`, or whose share was never written.
448
+ */
449
+ deviceProtected(): Promise<boolean>;
450
+ /**
451
+ * Wrap and store this device's share now: a passkey where the device can
452
+ * hold one, otherwise the unlock password (`promptUnlockPassword`). Needs a
453
+ * live session; provisions a fresh share from the enclave when the device
454
+ * has none. Idempotent — a protected device returns true at once. Resolves
455
+ * false when the user dismissed the prompt, so the app can decide whether
456
+ * to go on without protection; throws for anything else.
457
+ */
458
+ protectDevice(): Promise<boolean>;
423
459
  sessionExpiresAt(): number | null;
424
460
  signMessage(req: SignMessageRequest): Promise<string>;
425
461
  sendTransaction(req: SendTransactionRequest): Promise<string>;
@@ -525,7 +561,7 @@ interface DecaneConnect {
525
561
  off(event: SdkEvent, handler: (...args: unknown[]) => void): void;
526
562
  }
527
563
  interface WrappedShareMeta {
528
- unlockMethod: 'passkey' | 'pin' | 'password';
564
+ unlockMethod: 'passkey' | 'pin' | 'password' | 'deferred';
529
565
  credentialId?: string;
530
566
  prfSalt?: string;
531
567
  pinSalt?: string;
@@ -839,6 +875,13 @@ interface SocialConfig {
839
875
  * a silent multi-second pause reads as a broken page.
840
876
  */
841
877
  showStatusOverlay?: boolean;
878
+ /**
879
+ * Ask for no passkey or password at sign-in; protect the wallet on this
880
+ * device later, when the app calls `protectDevice()` (typically before the
881
+ * first transaction). Mirrors the headless client's option of the same name;
882
+ * see it for what a device gives up until then. Default false.
883
+ */
884
+ deferDeviceProtection?: boolean;
842
885
  /**
843
886
  * Offer to save a recovery-share file at signup. **Default false** since
844
887
  * 2.7.4 — opt in explicitly with `true`.
@@ -1440,10 +1483,13 @@ declare function useSocialWallet(): {
1440
1483
  getAccessToken: () => string | null;
1441
1484
  hasPasskey: () => Promise<boolean>;
1442
1485
  addPasskey: () => Promise<void>;
1486
+ deviceProtected: () => Promise<boolean>;
1487
+ protectDevice: () => Promise<boolean>;
1443
1488
  setUnlockPassword: (password: string) => Promise<void>;
1444
1489
  canUnlockWithPassword: () => Promise<boolean>;
1445
1490
  unlockWithPassword: (password: string) => Promise<void>;
1446
1491
  removeUnlockPassword: () => Promise<void>;
1492
+ forgetLastUser: () => void;
1447
1493
  rotateShares: (opts: {
1448
1494
  password: string;
1449
1495
  passwordHint?: string;
package/dist/index.d.ts CHANGED
@@ -146,6 +146,19 @@ interface DecaneConnectConfig {
146
146
  * page load (stricter, but the reload friction that motivated this returns).
147
147
  */
148
148
  persistSession?: boolean;
149
+ /**
150
+ * Do not wrap and store this device's share at wallet creation or when a
151
+ * device is first provisioned — so sign-in asks for no passkey and no
152
+ * password. The session works without a stored share: signing runs on the
153
+ * open session, and a later visit provisions a fresh share against a live
154
+ * sign-in. What the device loses until it is protected is the short way
155
+ * back in (passkey / password unlock) — every visit is a full sign-in.
156
+ *
157
+ * Call `protectDevice()` at the moment the app wants that step, typically
158
+ * before the user's first transaction. `deviceProtected()` says whether it
159
+ * has happened. Default false: the share is wrapped and stored at creation.
160
+ */
161
+ deferDeviceProtection?: boolean;
149
162
  /**
150
163
  * Offer to save a recovery-share file at signup. **Default false** since
151
164
  * 2.7.4 — opt in explicitly with `true`.
@@ -398,6 +411,14 @@ interface DecaneConnect {
398
411
  setUnlockPassword(password: string): Promise<void>;
399
412
  /** True when this device can open a session from a password alone. */
400
413
  canUnlockWithPassword(): Promise<boolean>;
414
+ /**
415
+ * "Use a different account." disconnect() keeps one fact about the account
416
+ * that signed out — its userId — so canSignInWithPasskey() and
417
+ * canUnlockWithPassword() can offer that account's own way back in even
418
+ * when several accounts are enrolled on this browser. This drops it. The
419
+ * device shares themselves are untouched.
420
+ */
421
+ forgetLastUser(): void;
401
422
  /** Open a session from the password alone — no provider, no passkey. */
402
423
  unlockWithPassword(password: string): Promise<ConnectResult>;
403
424
  /** Turn the password path off, revoking the token account-wide. */
@@ -420,6 +441,21 @@ interface DecaneConnect {
420
441
  * on the way).
421
442
  */
422
443
  addPasskey(): Promise<void>;
444
+ /**
445
+ * Whether this device holds a wrapped share for the signed-in account, i.e.
446
+ * whether a passkey or password protects the wallet here. False on a device
447
+ * created with `deferDeviceProtection`, or whose share was never written.
448
+ */
449
+ deviceProtected(): Promise<boolean>;
450
+ /**
451
+ * Wrap and store this device's share now: a passkey where the device can
452
+ * hold one, otherwise the unlock password (`promptUnlockPassword`). Needs a
453
+ * live session; provisions a fresh share from the enclave when the device
454
+ * has none. Idempotent — a protected device returns true at once. Resolves
455
+ * false when the user dismissed the prompt, so the app can decide whether
456
+ * to go on without protection; throws for anything else.
457
+ */
458
+ protectDevice(): Promise<boolean>;
423
459
  sessionExpiresAt(): number | null;
424
460
  signMessage(req: SignMessageRequest): Promise<string>;
425
461
  sendTransaction(req: SendTransactionRequest): Promise<string>;
@@ -525,7 +561,7 @@ interface DecaneConnect {
525
561
  off(event: SdkEvent, handler: (...args: unknown[]) => void): void;
526
562
  }
527
563
  interface WrappedShareMeta {
528
- unlockMethod: 'passkey' | 'pin' | 'password';
564
+ unlockMethod: 'passkey' | 'pin' | 'password' | 'deferred';
529
565
  credentialId?: string;
530
566
  prfSalt?: string;
531
567
  pinSalt?: string;
@@ -839,6 +875,13 @@ interface SocialConfig {
839
875
  * a silent multi-second pause reads as a broken page.
840
876
  */
841
877
  showStatusOverlay?: boolean;
878
+ /**
879
+ * Ask for no passkey or password at sign-in; protect the wallet on this
880
+ * device later, when the app calls `protectDevice()` (typically before the
881
+ * first transaction). Mirrors the headless client's option of the same name;
882
+ * see it for what a device gives up until then. Default false.
883
+ */
884
+ deferDeviceProtection?: boolean;
842
885
  /**
843
886
  * Offer to save a recovery-share file at signup. **Default false** since
844
887
  * 2.7.4 — opt in explicitly with `true`.
@@ -1440,10 +1483,13 @@ declare function useSocialWallet(): {
1440
1483
  getAccessToken: () => string | null;
1441
1484
  hasPasskey: () => Promise<boolean>;
1442
1485
  addPasskey: () => Promise<void>;
1486
+ deviceProtected: () => Promise<boolean>;
1487
+ protectDevice: () => Promise<boolean>;
1443
1488
  setUnlockPassword: (password: string) => Promise<void>;
1444
1489
  canUnlockWithPassword: () => Promise<boolean>;
1445
1490
  unlockWithPassword: (password: string) => Promise<void>;
1446
1491
  removeUnlockPassword: () => Promise<void>;
1492
+ forgetLastUser: () => void;
1447
1493
  rotateShares: (opts: {
1448
1494
  password: string;
1449
1495
  passwordHint?: string;
package/dist/index.js CHANGED
@@ -3607,6 +3607,29 @@ function clearPersisted(key) {
3607
3607
  } catch {
3608
3608
  }
3609
3609
  }
3610
+ function makeLastUserKey(apiKey) {
3611
+ return `${makePersistKey(apiKey)}:last-user`;
3612
+ }
3613
+ function loadLastUser(key) {
3614
+ try {
3615
+ const raw = typeof localStorage !== "undefined" ? localStorage.getItem(key) : null;
3616
+ return raw ? JSON.parse(raw).userId ?? null : null;
3617
+ } catch {
3618
+ return null;
3619
+ }
3620
+ }
3621
+ function saveLastUser(key, userId) {
3622
+ try {
3623
+ localStorage.setItem(key, JSON.stringify({ userId }));
3624
+ } catch {
3625
+ }
3626
+ }
3627
+ function clearLastUser(key) {
3628
+ try {
3629
+ localStorage.removeItem(key);
3630
+ } catch {
3631
+ }
3632
+ }
3610
3633
  function sessionPersistKey(apiKey) {
3611
3634
  return `decane:sess:${apiKey.slice(0, 20)}`;
3612
3635
  }
@@ -3645,6 +3668,7 @@ async function createDecaneConnect(config) {
3645
3668
  const rpId = config.rpId ?? (typeof window !== "undefined" ? window.location.hostname : "localhost");
3646
3669
  const rpName = config.rpName ?? "Decane Connect";
3647
3670
  const PERSIST_KEY = makePersistKey(config.apiKey);
3671
+ const LAST_USER_KEY = makeLastUserKey(config.apiKey);
3648
3672
  const teeUrl = config.teeUrl ?? DEFAULT_TEE_URL;
3649
3673
  const events = new EventEmitter();
3650
3674
  const api = new ApiClient(baseUrl, config.apiKey, config.appId);
@@ -3657,8 +3681,10 @@ async function createDecaneConnect(config) {
3657
3681
  );
3658
3682
  let auth = null;
3659
3683
  let persisted = loadPersisted(PERSIST_KEY);
3684
+ let lastUserId = loadLastUser(LAST_USER_KEY);
3660
3685
  let ethProvider = null;
3661
3686
  const persistSession = config.persistSession !== false;
3687
+ const deferDeviceProtection = config.deferDeviceProtection === true;
3662
3688
  const serverAssistedRecovery = true;
3663
3689
  const RECOVERY_OFFER_TIMEOUT_MS = 5 * 6e4;
3664
3690
  const selfHealAttempted = /* @__PURE__ */ new Set();
@@ -3802,6 +3828,10 @@ async function createDecaneConnect(config) {
3802
3828
  const stored = await storage.getShare(persisted.userId);
3803
3829
  return hasPasswordUnlock(stored) ? stored : null;
3804
3830
  }
3831
+ if (lastUserId) {
3832
+ const stored = await storage.getShare(lastUserId);
3833
+ return hasPasswordUnlock(stored) ? stored : null;
3834
+ }
3805
3835
  const candidates = (await storage.listShares()).filter(hasPasswordUnlock);
3806
3836
  return candidates.length === 1 ? candidates[0] : null;
3807
3837
  }
@@ -3966,11 +3996,12 @@ async function createDecaneConnect(config) {
3966
3996
  }
3967
3997
  async function firstTimeSetup(userId, jwt) {
3968
3998
  prewarmAttestation(teeUrl);
3969
- let unlockMethod;
3970
- let wrappingKey;
3999
+ let unlockMethod = "password";
4000
+ let wrappingKey = null;
3971
4001
  let meta = {};
3972
4002
  let unlockTokenKey = null;
3973
- if (isPasskeySupported()) {
4003
+ if (deferDeviceProtection) {
4004
+ } else if (isPasskeySupported()) {
3974
4005
  const prfSalt = generateRandomBytes(32);
3975
4006
  try {
3976
4007
  const { credentialId, prfOutput } = await registerWebAuthnCredential({ api, jwt, rpId, rpName, prfSalt });
@@ -4011,32 +4042,37 @@ async function createDecaneConnect(config) {
4011
4042
  } finally {
4012
4043
  zeroize(master);
4013
4044
  }
4014
- const iv = generateRandomBytes(12);
4015
- const wrappedDeviceShare = await aesGcmEncrypt(
4016
- wrappingKey,
4017
- shareSet.device,
4018
- iv
4019
- );
4020
- await storage.putShare({
4021
- userId,
4022
- wrappedDeviceShare,
4023
- iv,
4024
- unlockMethod,
4025
- credentialId: meta.credentialId ? b64uToBuffer(meta.credentialId) : void 0,
4026
- prfSalt: meta.prfSalt ? b64uToBuffer(meta.prfSalt) : void 0,
4027
- pinSalt: meta.pinSalt ? b64uToBuffer(meta.pinSalt) : void 0,
4028
- pinIterations: meta.pinIterations,
4029
- passwordSalt: meta.passwordSalt ? b64uToBuffer(meta.passwordSalt) : void 0,
4030
- passwordIterations: meta.passwordIterations,
4031
- version: 1
4032
- });
4033
- if (unlockTokenKey) await attachUnlockToken(userId, jwt, unlockTokenKey);
4045
+ let wrappedDeviceShare = null;
4046
+ let iv = null;
4047
+ if (wrappingKey) {
4048
+ iv = generateRandomBytes(12);
4049
+ wrappedDeviceShare = await aesGcmEncrypt(
4050
+ wrappingKey,
4051
+ shareSet.device,
4052
+ iv
4053
+ );
4054
+ await storage.putShare({
4055
+ userId,
4056
+ wrappedDeviceShare,
4057
+ iv,
4058
+ unlockMethod,
4059
+ credentialId: meta.credentialId ? b64uToBuffer(meta.credentialId) : void 0,
4060
+ prfSalt: meta.prfSalt ? b64uToBuffer(meta.prfSalt) : void 0,
4061
+ pinSalt: meta.pinSalt ? b64uToBuffer(meta.pinSalt) : void 0,
4062
+ pinIterations: meta.pinIterations,
4063
+ passwordSalt: meta.passwordSalt ? b64uToBuffer(meta.passwordSalt) : void 0,
4064
+ passwordIterations: meta.passwordIterations,
4065
+ version: 1
4066
+ });
4067
+ if (unlockTokenKey) await attachUnlockToken(userId, jwt, unlockTokenKey);
4068
+ }
4034
4069
  const serverIv = generateRandomBytes(12);
4035
4070
  await withDeadline(api.putShare(jwt, {
4036
4071
  ciphertext: shareSet.server,
4037
4072
  iv: serverIv,
4038
4073
  wrappedShareMeta: {
4039
- unlockMethod
4074
+ // The server records this on the wallet_created event and nothing else.
4075
+ unlockMethod: deferDeviceProtection ? "deferred" : unlockMethod
4040
4076
  // credential IDs, prfSalt, pinSalt stay on-device — never sent to server
4041
4077
  },
4042
4078
  evmAddress,
@@ -4045,7 +4081,7 @@ async function createDecaneConnect(config) {
4045
4081
  }), remainingBudget(), "storing your wallet");
4046
4082
  zeroize(shareSet.server);
4047
4083
  const addresses = { evm: evmAddress, solana: solanaAddress, tron: tronAddress };
4048
- if (unlockMethod === "passkey" && meta.credentialId && meta.prfSalt) {
4084
+ if (unlockMethod === "passkey" && meta.credentialId && meta.prfSalt && wrappedDeviceShare && iv) {
4049
4085
  try {
4050
4086
  await api.putRecoveryBackup(jwt, {
4051
4087
  wrappedDeviceShare,
@@ -4128,7 +4164,7 @@ async function createDecaneConnect(config) {
4128
4164
  const tee = new TeeClient(teeUrl);
4129
4165
  const provisioned = await tee.provisionDeviceShare(jwt);
4130
4166
  if (provisioned) {
4131
- await persistProvisionedShare(userId, jwt, provisioned);
4167
+ if (!deferDeviceProtection) await persistProvisionedShare(userId, jwt, provisioned);
4132
4168
  let addrs;
4133
4169
  try {
4134
4170
  addrs = await session.start(provisioned, jwt, recordedAddresses);
@@ -4172,7 +4208,7 @@ async function createDecaneConnect(config) {
4172
4208
  });
4173
4209
  deviceShare = recovered.deviceShare;
4174
4210
  joinedViaPasskeySync = recovered.flow === "passkey-sync";
4175
- if (!await storage.getShare(userId) && isPasskeySupported()) {
4211
+ if (!deferDeviceProtection && !await storage.getShare(userId) && isPasskeySupported()) {
4176
4212
  await registerPasskeyAndBackup(userId, jwt, deviceShare);
4177
4213
  }
4178
4214
  } else {
@@ -4255,14 +4291,16 @@ async function createDecaneConnect(config) {
4255
4291
  async function passkeyDeviceCandidate() {
4256
4292
  if (!isPasskeySupported()) return null;
4257
4293
  if (persisted?.userId) return passkeyLoginCandidate();
4294
+ if (lastUserId) return usablePasskeyShare(await storage.getShare(lastUserId));
4258
4295
  const candidates = (await storage.listShares()).map(usablePasskeyShare).filter(Boolean);
4259
4296
  return candidates.length === 1 ? candidates[0] : null;
4260
4297
  }
4261
4298
  async function canOfferPasskey() {
4262
4299
  if (!isPasskeySupported()) return false;
4263
4300
  if (await passkeyDeviceCandidate()) return true;
4264
- if (!persisted?.userId) return false;
4265
- return await storage.getShare(persisted.userId) == null;
4301
+ const known = persisted?.userId ?? lastUserId;
4302
+ if (!known) return false;
4303
+ return await storage.getShare(known) == null;
4266
4304
  }
4267
4305
  async function rememberProfile(userId, profile) {
4268
4306
  const incoming = Object.fromEntries(Object.entries(profile).filter(([, v]) => v));
@@ -4475,8 +4513,9 @@ async function createDecaneConnect(config) {
4475
4513
  async signInWithPasskey() {
4476
4514
  const candidate = await passkeyDeviceCandidate();
4477
4515
  if (candidate) return passkeyLogin(candidate);
4478
- if (isPasskeySupported() && persisted?.userId && await storage.getShare(persisted.userId) == null) {
4479
- return passkeyLoginWithoutShare(persisted.userId);
4516
+ const known = persisted?.userId ?? lastUserId;
4517
+ if (isPasskeySupported() && known && await storage.getShare(known) == null) {
4518
+ return passkeyLoginWithoutShare(known);
4480
4519
  }
4481
4520
  throw new NewDeviceError();
4482
4521
  },
@@ -4640,6 +4679,24 @@ async function createDecaneConnect(config) {
4640
4679
  zeroize(deviceShare);
4641
4680
  }
4642
4681
  },
4682
+ async deviceProtected() {
4683
+ if (!auth) return false;
4684
+ return await storage.getShare(auth.userId) !== null;
4685
+ },
4686
+ async protectDevice() {
4687
+ if (!auth) throw new ReconnectRequiredError();
4688
+ if (await storage.getShare(auth.userId)) return true;
4689
+ if (!serverAssistedRecovery) throw new NewDeviceError();
4690
+ const tee = new TeeClient(teeUrl);
4691
+ const provisioned = await tee.provisionDeviceShare(auth.jwt);
4692
+ if (!provisioned) throw new NewDeviceError();
4693
+ try {
4694
+ await persistProvisionedShare(auth.userId, auth.jwt, provisioned);
4695
+ } finally {
4696
+ zeroize(provisioned);
4697
+ }
4698
+ return await storage.getShare(auth.userId) !== null;
4699
+ },
4643
4700
  async unlock() {
4644
4701
  if (!auth) {
4645
4702
  const candidate = await passkeyLoginCandidate();
@@ -4816,12 +4873,21 @@ async function createDecaneConnect(config) {
4816
4873
  await api.revoke(auth.jwt).catch(() => {
4817
4874
  });
4818
4875
  }
4876
+ const leaving = auth?.userId ?? persisted?.userId ?? null;
4877
+ if (leaving) {
4878
+ lastUserId = leaving;
4879
+ saveLastUser(LAST_USER_KEY, leaving);
4880
+ }
4819
4881
  auth = null;
4820
4882
  persisted = null;
4821
4883
  clearPersisted(PERSIST_KEY);
4822
4884
  dropLiveSession();
4823
4885
  events.emit("disconnected");
4824
4886
  },
4887
+ forgetLastUser() {
4888
+ lastUserId = null;
4889
+ clearLastUser(LAST_USER_KEY);
4890
+ },
4825
4891
  getAddresses() {
4826
4892
  return auth?.addresses ?? persisted?.addresses ?? null;
4827
4893
  },
@@ -7260,6 +7326,7 @@ function DecaneKit({ config, children }) {
7260
7326
  chains,
7261
7327
  rpcUrls,
7262
7328
  requireAssertionPerSignature,
7329
+ deferDeviceProtection,
7263
7330
  // Recovery callbacks. Forwarding these is what makes recovery
7264
7331
  // configurable at all from <DecaneKit>; before, they were silently
7265
7332
  // dropped and the headless client was constructed without them.
@@ -7280,6 +7347,7 @@ function DecaneKit({ config, children }) {
7280
7347
  promptPin,
7281
7348
  promptUnlockPassword,
7282
7349
  requireAssertionPerSignature,
7350
+ deferDeviceProtection,
7283
7351
  offerRecoveryAtSignup,
7284
7352
  onRecoveryShareOffer,
7285
7353
  onRecoveryFileReady,
@@ -7526,6 +7594,16 @@ function DecaneKit({ config, children }) {
7526
7594
  if (!sdk) throw new Error("Social wallet not connected");
7527
7595
  return sdk.addPasskey();
7528
7596
  }, []);
7597
+ const socialDeviceProtected = useCallback3(async () => {
7598
+ const sdk = socialRef.current;
7599
+ if (!sdk) return false;
7600
+ return sdk.deviceProtected();
7601
+ }, []);
7602
+ const socialProtectDevice = useCallback3(async () => {
7603
+ const sdk = socialRef.current;
7604
+ if (!sdk) throw new Error("Social wallet not connected");
7605
+ return sdk.protectDevice();
7606
+ }, []);
7529
7607
  const socialSetUnlockPassword = useCallback3(async (password) => {
7530
7608
  const sdk = socialRef.current;
7531
7609
  if (!sdk) throw new Error("Social wallet not connected");
@@ -7536,6 +7614,14 @@ function DecaneKit({ config, children }) {
7536
7614
  if (!sdk) return false;
7537
7615
  return sdk.canUnlockWithPassword();
7538
7616
  }, []);
7617
+ const socialForgetLastUser = useCallback3(() => {
7618
+ socialRef.current?.forgetLastUser();
7619
+ const sdk = socialRef.current;
7620
+ if (sdk) {
7621
+ sdk.canSignInWithPasskey().then((can) => setSocialAuth((p) => ({ ...p, canUsePasskey: can }))).catch(() => {
7622
+ });
7623
+ }
7624
+ }, []);
7539
7625
  const socialUnlockWithPassword = useCallback3(async (password) => {
7540
7626
  const sdk = socialRef.current;
7541
7627
  if (!sdk) throw new Error("Social wallet not connected");
@@ -7610,8 +7696,11 @@ function DecaneKit({ config, children }) {
7610
7696
  socialHasRecoveryShare,
7611
7697
  socialHasPasskey,
7612
7698
  socialAddPasskey,
7699
+ socialDeviceProtected,
7700
+ socialProtectDevice,
7613
7701
  socialSetUnlockPassword,
7614
7702
  socialCanUnlockWithPassword,
7703
+ socialForgetLastUser,
7615
7704
  socialUnlockWithPassword,
7616
7705
  socialRemoveUnlockPassword,
7617
7706
  socialShouldPromptRecovery,
@@ -7919,8 +8008,11 @@ function useSocialWallet() {
7919
8008
  socialHasRecoveryShare,
7920
8009
  socialHasPasskey,
7921
8010
  socialAddPasskey,
8011
+ socialDeviceProtected,
8012
+ socialProtectDevice,
7922
8013
  socialSetUnlockPassword,
7923
8014
  socialCanUnlockWithPassword,
8015
+ socialForgetLastUser,
7924
8016
  socialUnlockWithPassword,
7925
8017
  socialRemoveUnlockPassword,
7926
8018
  socialShouldPromptRecovery,
@@ -8020,6 +8112,16 @@ function useSocialWallet() {
8020
8112
  throw e;
8021
8113
  }
8022
8114
  }, [socialAddPasskey]);
8115
+ const deviceProtected = useCallback4(() => run(() => socialDeviceProtected()), [socialDeviceProtected]);
8116
+ const protectDevice = useCallback4(async () => {
8117
+ setError(null);
8118
+ try {
8119
+ return await socialProtectDevice();
8120
+ } catch (e) {
8121
+ setError(e.message);
8122
+ throw e;
8123
+ }
8124
+ }, [socialProtectDevice]);
8023
8125
  const setUnlockPassword = useCallback4(async (password) => {
8024
8126
  setError(null);
8025
8127
  try {
@@ -8029,6 +8131,7 @@ function useSocialWallet() {
8029
8131
  throw e;
8030
8132
  }
8031
8133
  }, [socialSetUnlockPassword]);
8134
+ const forgetLastUser = useCallback4(() => socialForgetLastUser(), [socialForgetLastUser]);
8032
8135
  const canUnlockWithPassword = useCallback4(
8033
8136
  () => socialCanUnlockWithPassword(),
8034
8137
  [socialCanUnlockWithPassword]
@@ -8107,6 +8210,10 @@ function useSocialWallet() {
8107
8210
  // Passkey management for THIS device
8108
8211
  hasPasskey,
8109
8212
  addPasskey,
8213
+ // Deferred protection (deferDeviceProtection): whether this device holds
8214
+ // a wrapped share, and the call that stores one — passkey or password.
8215
+ deviceProtected,
8216
+ protectDevice,
8110
8217
  // "Unlock with your password": setUnlockPassword needs a live session;
8111
8218
  // canUnlockWithPassword and unlockWithPassword are the signed-out pair that
8112
8219
  // open one without an identity provider or a passkey.
@@ -8114,6 +8221,8 @@ function useSocialWallet() {
8114
8221
  canUnlockWithPassword,
8115
8222
  unlockWithPassword,
8116
8223
  removeUnlockPassword,
8224
+ // "Use a different account": stop offering the account that signed out.
8225
+ forgetLastUser,
8117
8226
  // Recovery (2-of-3 Shamir share set)
8118
8227
  rotateShares,
8119
8228
  exportPortableBackup,