decane-connect-kit 2.20.0 → 2.22.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
@@ -2038,10 +2038,13 @@ function readRedirectOutcome(search) {
2038
2038
  const name = params.get("decane_name");
2039
2039
  const email = params.get("decane_email");
2040
2040
  const picture = params.get("decane_picture");
2041
+ const username = params.get("decane_username");
2042
+ const providerSubject = params.get("decane_provider_subject");
2041
2043
  const profile = {
2042
2044
  ...name ? { name } : {},
2043
2045
  ...email ? { email } : {},
2044
- ...picture ? { picture } : {}
2046
+ ...picture ? { picture } : {},
2047
+ ...username ? { username } : {}
2045
2048
  };
2046
2049
  return {
2047
2050
  kind: "result",
@@ -2054,7 +2057,8 @@ function readRedirectOutcome(search) {
2054
2057
  // test `result.profile` rather than an object that is always present but
2055
2058
  // sometimes empty. X never sends an email, so this is the normal case
2056
2059
  // there rather than an edge one.
2057
- ...Object.keys(profile).length ? { profile } : {}
2060
+ ...Object.keys(profile).length ? { profile } : {},
2061
+ ...providerSubject ? { providerSubject } : {}
2058
2062
  }
2059
2063
  };
2060
2064
  }
@@ -3685,6 +3689,29 @@ function clearPersisted(key) {
3685
3689
  } catch {
3686
3690
  }
3687
3691
  }
3692
+ function makeLastUserKey(apiKey) {
3693
+ return `${makePersistKey(apiKey)}:last-user`;
3694
+ }
3695
+ function loadLastUser(key) {
3696
+ try {
3697
+ const raw = typeof localStorage !== "undefined" ? localStorage.getItem(key) : null;
3698
+ return raw ? JSON.parse(raw).userId ?? null : null;
3699
+ } catch {
3700
+ return null;
3701
+ }
3702
+ }
3703
+ function saveLastUser(key, userId) {
3704
+ try {
3705
+ localStorage.setItem(key, JSON.stringify({ userId }));
3706
+ } catch {
3707
+ }
3708
+ }
3709
+ function clearLastUser(key) {
3710
+ try {
3711
+ localStorage.removeItem(key);
3712
+ } catch {
3713
+ }
3714
+ }
3688
3715
  function sessionPersistKey(apiKey) {
3689
3716
  return `decane:sess:${apiKey.slice(0, 20)}`;
3690
3717
  }
@@ -3723,6 +3750,7 @@ async function createDecaneConnect(config) {
3723
3750
  const rpId = config.rpId ?? (typeof window !== "undefined" ? window.location.hostname : "localhost");
3724
3751
  const rpName = config.rpName ?? "Decane Connect";
3725
3752
  const PERSIST_KEY = makePersistKey(config.apiKey);
3753
+ const LAST_USER_KEY = makeLastUserKey(config.apiKey);
3726
3754
  const teeUrl = config.teeUrl ?? DEFAULT_TEE_URL;
3727
3755
  const events = new EventEmitter();
3728
3756
  const api = new ApiClient(baseUrl, config.apiKey, config.appId);
@@ -3735,6 +3763,7 @@ async function createDecaneConnect(config) {
3735
3763
  );
3736
3764
  let auth = null;
3737
3765
  let persisted = loadPersisted(PERSIST_KEY);
3766
+ let lastUserId = loadLastUser(LAST_USER_KEY);
3738
3767
  let ethProvider = null;
3739
3768
  const persistSession = config.persistSession !== false;
3740
3769
  const serverAssistedRecovery = true;
@@ -3880,6 +3909,10 @@ async function createDecaneConnect(config) {
3880
3909
  const stored = await storage.getShare(persisted.userId);
3881
3910
  return hasPasswordUnlock(stored) ? stored : null;
3882
3911
  }
3912
+ if (lastUserId) {
3913
+ const stored = await storage.getShare(lastUserId);
3914
+ return hasPasswordUnlock(stored) ? stored : null;
3915
+ }
3883
3916
  const candidates = (await storage.listShares()).filter(hasPasswordUnlock);
3884
3917
  return candidates.length === 1 ? candidates[0] : null;
3885
3918
  }
@@ -4333,14 +4366,16 @@ async function createDecaneConnect(config) {
4333
4366
  async function passkeyDeviceCandidate() {
4334
4367
  if (!isPasskeySupported()) return null;
4335
4368
  if (persisted?.userId) return passkeyLoginCandidate();
4369
+ if (lastUserId) return usablePasskeyShare(await storage.getShare(lastUserId));
4336
4370
  const candidates = (await storage.listShares()).map(usablePasskeyShare).filter(Boolean);
4337
4371
  return candidates.length === 1 ? candidates[0] : null;
4338
4372
  }
4339
4373
  async function canOfferPasskey() {
4340
4374
  if (!isPasskeySupported()) return false;
4341
4375
  if (await passkeyDeviceCandidate()) return true;
4342
- if (!persisted?.userId) return false;
4343
- return await storage.getShare(persisted.userId) == null;
4376
+ const known = persisted?.userId ?? lastUserId;
4377
+ if (!known) return false;
4378
+ return await storage.getShare(known) == null;
4344
4379
  }
4345
4380
  async function rememberProfile(userId, profile) {
4346
4381
  const incoming = Object.fromEntries(Object.entries(profile).filter(([, v]) => v));
@@ -4450,7 +4485,8 @@ async function createDecaneConnect(config) {
4450
4485
  return {
4451
4486
  addresses,
4452
4487
  isNewUser: result.isNewUser,
4453
- ...result.profile ? { profile: result.profile } : {}
4488
+ ...result.profile ? { profile: result.profile } : {},
4489
+ ...result.providerSubject ? { providerSubject: result.providerSubject } : {}
4454
4490
  };
4455
4491
  }
4456
4492
  const client = {
@@ -4552,8 +4588,9 @@ async function createDecaneConnect(config) {
4552
4588
  async signInWithPasskey() {
4553
4589
  const candidate = await passkeyDeviceCandidate();
4554
4590
  if (candidate) return passkeyLogin(candidate);
4555
- if (isPasskeySupported() && persisted?.userId && await storage.getShare(persisted.userId) == null) {
4556
- return passkeyLoginWithoutShare(persisted.userId);
4591
+ const known = persisted?.userId ?? lastUserId;
4592
+ if (isPasskeySupported() && known && await storage.getShare(known) == null) {
4593
+ return passkeyLoginWithoutShare(known);
4557
4594
  }
4558
4595
  throw new NewDeviceError();
4559
4596
  },
@@ -4893,12 +4930,21 @@ async function createDecaneConnect(config) {
4893
4930
  await api.revoke(auth.jwt).catch(() => {
4894
4931
  });
4895
4932
  }
4933
+ const leaving = auth?.userId ?? persisted?.userId ?? null;
4934
+ if (leaving) {
4935
+ lastUserId = leaving;
4936
+ saveLastUser(LAST_USER_KEY, leaving);
4937
+ }
4896
4938
  auth = null;
4897
4939
  persisted = null;
4898
4940
  clearPersisted(PERSIST_KEY);
4899
4941
  dropLiveSession();
4900
4942
  events.emit("disconnected");
4901
4943
  },
4944
+ forgetLastUser() {
4945
+ lastUserId = null;
4946
+ clearLastUser(LAST_USER_KEY);
4947
+ },
4902
4948
  getAddresses() {
4903
4949
  return auth?.addresses ?? persisted?.addresses ?? null;
4904
4950
  },
@@ -7263,6 +7309,7 @@ var DEFAULT_SOCIAL = {
7263
7309
  addresses: null,
7264
7310
  isNewUser: false,
7265
7311
  profile: null,
7312
+ providerSubject: null,
7266
7313
  isUnlocked: false,
7267
7314
  sessionExpiresAt: null,
7268
7315
  needsReconnect: false,
@@ -7398,6 +7445,7 @@ function DecaneKit({ config, children }) {
7398
7445
  addresses: result.addresses,
7399
7446
  isNewUser: result.isNewUser,
7400
7447
  profile: result.profile ?? null,
7448
+ providerSubject: result.providerSubject ?? null,
7401
7449
  isUnlocked: sdk?.isUnlocked() ?? false,
7402
7450
  sessionExpiresAt: sdk?.sessionExpiresAt() ?? null,
7403
7451
  needsReconnect: false,
@@ -7604,6 +7652,14 @@ function DecaneKit({ config, children }) {
7604
7652
  if (!sdk) return false;
7605
7653
  return sdk.canUnlockWithPassword();
7606
7654
  }, []);
7655
+ const socialForgetLastUser = (0, import_react5.useCallback)(() => {
7656
+ socialRef.current?.forgetLastUser();
7657
+ const sdk = socialRef.current;
7658
+ if (sdk) {
7659
+ sdk.canSignInWithPasskey().then((can) => setSocialAuth((p) => ({ ...p, canUsePasskey: can }))).catch(() => {
7660
+ });
7661
+ }
7662
+ }, []);
7607
7663
  const socialUnlockWithPassword = (0, import_react5.useCallback)(async (password) => {
7608
7664
  const sdk = socialRef.current;
7609
7665
  if (!sdk) throw new Error("Social wallet not connected");
@@ -7680,6 +7736,7 @@ function DecaneKit({ config, children }) {
7680
7736
  socialAddPasskey,
7681
7737
  socialSetUnlockPassword,
7682
7738
  socialCanUnlockWithPassword,
7739
+ socialForgetLastUser,
7683
7740
  socialUnlockWithPassword,
7684
7741
  socialRemoveUnlockPassword,
7685
7742
  socialShouldPromptRecovery,
@@ -7930,6 +7987,9 @@ function useSocialAuth() {
7930
7987
  // user first signed in, so it carries the same name and email a Google
7931
7988
  // sign-in does rather than coming back anonymous.
7932
7989
  profile: socialAuth.profile,
7990
+ // The provider's stable id, where it gives one. Null otherwise. Safe to
7991
+ // match an account on, unlike the handle in `profile.username`.
7992
+ providerSubject: socialAuth.providerSubject,
7933
7993
  // Long-running work in progress, for driving your own status UI.
7934
7994
  phase: socialAuth.phase,
7935
7995
  isCreatingWallet: socialAuth.phase === "creating",
@@ -7986,6 +8046,7 @@ function useSocialWallet() {
7986
8046
  socialAddPasskey,
7987
8047
  socialSetUnlockPassword,
7988
8048
  socialCanUnlockWithPassword,
8049
+ socialForgetLastUser,
7989
8050
  socialUnlockWithPassword,
7990
8051
  socialRemoveUnlockPassword,
7991
8052
  socialShouldPromptRecovery,
@@ -8094,6 +8155,7 @@ function useSocialWallet() {
8094
8155
  throw e;
8095
8156
  }
8096
8157
  }, [socialSetUnlockPassword]);
8158
+ const forgetLastUser = (0, import_react6.useCallback)(() => socialForgetLastUser(), [socialForgetLastUser]);
8097
8159
  const canUnlockWithPassword = (0, import_react6.useCallback)(
8098
8160
  () => socialCanUnlockWithPassword(),
8099
8161
  [socialCanUnlockWithPassword]
@@ -8179,6 +8241,8 @@ function useSocialWallet() {
8179
8241
  canUnlockWithPassword,
8180
8242
  unlockWithPassword,
8181
8243
  removeUnlockPassword,
8244
+ // "Use a different account": stop offering the account that signed out.
8245
+ forgetLastUser,
8182
8246
  // Recovery (2-of-3 Shamir share set)
8183
8247
  rotateShares,
8184
8248
  exportPortableBackup,