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.d.cts CHANGED
@@ -229,6 +229,8 @@ interface ConnectResult {
229
229
  isNewUser: boolean;
230
230
  /** Present when the provider released a profile — see {@link AuthProfile}. */
231
231
  profile?: AuthProfile;
232
+ /** See {@link AuthResult.providerSubject}. */
233
+ providerSubject?: string;
232
234
  }
233
235
  /** A recorded on-chain transaction in the user's history. */
234
236
  interface TransactionRecord {
@@ -396,6 +398,14 @@ interface DecaneConnect {
396
398
  setUnlockPassword(password: string): Promise<void>;
397
399
  /** True when this device can open a session from a password alone. */
398
400
  canUnlockWithPassword(): Promise<boolean>;
401
+ /**
402
+ * "Use a different account." disconnect() keeps one fact about the account
403
+ * that signed out — its userId — so canSignInWithPasskey() and
404
+ * canUnlockWithPassword() can offer that account's own way back in even
405
+ * when several accounts are enrolled on this browser. This drops it. The
406
+ * device shares themselves are untouched.
407
+ */
408
+ forgetLastUser(): void;
399
409
  /** Open a session from the password alone — no provider, no passkey. */
400
410
  unlockWithPassword(password: string): Promise<ConnectResult>;
401
411
  /** Turn the password path off, revoking the token account-wide. */
@@ -547,6 +557,16 @@ interface AuthProfile {
547
557
  name?: string;
548
558
  email?: string;
549
559
  picture?: string;
560
+ /**
561
+ * The handle the user signs in under, where the provider has one. X sets it
562
+ * and releases no email at all, so for those accounts this is the only
563
+ * human-readable thing there is.
564
+ *
565
+ * Display only. A handle can be released by its owner and registered by
566
+ * somebody else, so never key an account, a lookup or access on it — use
567
+ * {@link AuthResult.providerSubject}, which cannot move.
568
+ */
569
+ username?: string;
550
570
  }
551
571
  interface AuthResult {
552
572
  jwt: string;
@@ -554,6 +574,15 @@ interface AuthResult {
554
574
  hasShare: boolean;
555
575
  userId: string;
556
576
  profile?: AuthProfile;
577
+ /**
578
+ * The provider's own stable id for this user — X's numeric user id, for
579
+ * instance. Present only where the provider gives one.
580
+ *
581
+ * Unlike a handle or an email this cannot be reassigned, which is what makes
582
+ * it safe to match an account against: the same value identifies the same
583
+ * person on both sides of a migration.
584
+ */
585
+ providerSubject?: string;
557
586
  }
558
587
 
559
588
  declare class ApiClient {
@@ -1229,6 +1258,15 @@ interface SocialAuthState {
1229
1258
  * you need it in that state.
1230
1259
  */
1231
1260
  profile: AuthProfile | null;
1261
+ /**
1262
+ * The provider's own stable id for this user, where it gives one — X's
1263
+ * numeric user id, for instance. Null for providers that do not.
1264
+ *
1265
+ * Unlike a handle or an email this cannot be reassigned, so it is the value
1266
+ * to match an account against. Like `profile` it comes from the sign-in and
1267
+ * cannot be re-fetched, so copy it if you need it after a reload.
1268
+ */
1269
+ providerSubject: string | null;
1232
1270
  isUnlocked: boolean;
1233
1271
  sessionExpiresAt: number | null;
1234
1272
  /**
@@ -1322,6 +1360,7 @@ declare function useSocialAuth(): {
1322
1360
  sessionExpiresAt: number | null;
1323
1361
  canUsePasskey: boolean;
1324
1362
  profile: AuthProfile | null;
1363
+ providerSubject: string | null;
1325
1364
  phase: "creating" | "unlocking" | null;
1326
1365
  isCreatingWallet: boolean;
1327
1366
  needsReconnect: boolean;
@@ -1413,6 +1452,7 @@ declare function useSocialWallet(): {
1413
1452
  canUnlockWithPassword: () => Promise<boolean>;
1414
1453
  unlockWithPassword: (password: string) => Promise<void>;
1415
1454
  removeUnlockPassword: () => Promise<void>;
1455
+ forgetLastUser: () => void;
1416
1456
  rotateShares: (opts: {
1417
1457
  password: string;
1418
1458
  passwordHint?: string;
package/dist/index.d.ts CHANGED
@@ -229,6 +229,8 @@ interface ConnectResult {
229
229
  isNewUser: boolean;
230
230
  /** Present when the provider released a profile — see {@link AuthProfile}. */
231
231
  profile?: AuthProfile;
232
+ /** See {@link AuthResult.providerSubject}. */
233
+ providerSubject?: string;
232
234
  }
233
235
  /** A recorded on-chain transaction in the user's history. */
234
236
  interface TransactionRecord {
@@ -396,6 +398,14 @@ interface DecaneConnect {
396
398
  setUnlockPassword(password: string): Promise<void>;
397
399
  /** True when this device can open a session from a password alone. */
398
400
  canUnlockWithPassword(): Promise<boolean>;
401
+ /**
402
+ * "Use a different account." disconnect() keeps one fact about the account
403
+ * that signed out — its userId — so canSignInWithPasskey() and
404
+ * canUnlockWithPassword() can offer that account's own way back in even
405
+ * when several accounts are enrolled on this browser. This drops it. The
406
+ * device shares themselves are untouched.
407
+ */
408
+ forgetLastUser(): void;
399
409
  /** Open a session from the password alone — no provider, no passkey. */
400
410
  unlockWithPassword(password: string): Promise<ConnectResult>;
401
411
  /** Turn the password path off, revoking the token account-wide. */
@@ -547,6 +557,16 @@ interface AuthProfile {
547
557
  name?: string;
548
558
  email?: string;
549
559
  picture?: string;
560
+ /**
561
+ * The handle the user signs in under, where the provider has one. X sets it
562
+ * and releases no email at all, so for those accounts this is the only
563
+ * human-readable thing there is.
564
+ *
565
+ * Display only. A handle can be released by its owner and registered by
566
+ * somebody else, so never key an account, a lookup or access on it — use
567
+ * {@link AuthResult.providerSubject}, which cannot move.
568
+ */
569
+ username?: string;
550
570
  }
551
571
  interface AuthResult {
552
572
  jwt: string;
@@ -554,6 +574,15 @@ interface AuthResult {
554
574
  hasShare: boolean;
555
575
  userId: string;
556
576
  profile?: AuthProfile;
577
+ /**
578
+ * The provider's own stable id for this user — X's numeric user id, for
579
+ * instance. Present only where the provider gives one.
580
+ *
581
+ * Unlike a handle or an email this cannot be reassigned, which is what makes
582
+ * it safe to match an account against: the same value identifies the same
583
+ * person on both sides of a migration.
584
+ */
585
+ providerSubject?: string;
557
586
  }
558
587
 
559
588
  declare class ApiClient {
@@ -1229,6 +1258,15 @@ interface SocialAuthState {
1229
1258
  * you need it in that state.
1230
1259
  */
1231
1260
  profile: AuthProfile | null;
1261
+ /**
1262
+ * The provider's own stable id for this user, where it gives one — X's
1263
+ * numeric user id, for instance. Null for providers that do not.
1264
+ *
1265
+ * Unlike a handle or an email this cannot be reassigned, so it is the value
1266
+ * to match an account against. Like `profile` it comes from the sign-in and
1267
+ * cannot be re-fetched, so copy it if you need it after a reload.
1268
+ */
1269
+ providerSubject: string | null;
1232
1270
  isUnlocked: boolean;
1233
1271
  sessionExpiresAt: number | null;
1234
1272
  /**
@@ -1322,6 +1360,7 @@ declare function useSocialAuth(): {
1322
1360
  sessionExpiresAt: number | null;
1323
1361
  canUsePasskey: boolean;
1324
1362
  profile: AuthProfile | null;
1363
+ providerSubject: string | null;
1325
1364
  phase: "creating" | "unlocking" | null;
1326
1365
  isCreatingWallet: boolean;
1327
1366
  needsReconnect: boolean;
@@ -1413,6 +1452,7 @@ declare function useSocialWallet(): {
1413
1452
  canUnlockWithPassword: () => Promise<boolean>;
1414
1453
  unlockWithPassword: (password: string) => Promise<void>;
1415
1454
  removeUnlockPassword: () => Promise<void>;
1455
+ forgetLastUser: () => void;
1416
1456
  rotateShares: (opts: {
1417
1457
  password: string;
1418
1458
  passwordHint?: string;
package/dist/index.js CHANGED
@@ -1956,10 +1956,13 @@ function readRedirectOutcome(search) {
1956
1956
  const name = params.get("decane_name");
1957
1957
  const email = params.get("decane_email");
1958
1958
  const picture = params.get("decane_picture");
1959
+ const username = params.get("decane_username");
1960
+ const providerSubject = params.get("decane_provider_subject");
1959
1961
  const profile = {
1960
1962
  ...name ? { name } : {},
1961
1963
  ...email ? { email } : {},
1962
- ...picture ? { picture } : {}
1964
+ ...picture ? { picture } : {},
1965
+ ...username ? { username } : {}
1963
1966
  };
1964
1967
  return {
1965
1968
  kind: "result",
@@ -1972,7 +1975,8 @@ function readRedirectOutcome(search) {
1972
1975
  // test `result.profile` rather than an object that is always present but
1973
1976
  // sometimes empty. X never sends an email, so this is the normal case
1974
1977
  // there rather than an edge one.
1975
- ...Object.keys(profile).length ? { profile } : {}
1978
+ ...Object.keys(profile).length ? { profile } : {},
1979
+ ...providerSubject ? { providerSubject } : {}
1976
1980
  }
1977
1981
  };
1978
1982
  }
@@ -3603,6 +3607,29 @@ function clearPersisted(key) {
3603
3607
  } catch {
3604
3608
  }
3605
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
+ }
3606
3633
  function sessionPersistKey(apiKey) {
3607
3634
  return `decane:sess:${apiKey.slice(0, 20)}`;
3608
3635
  }
@@ -3641,6 +3668,7 @@ async function createDecaneConnect(config) {
3641
3668
  const rpId = config.rpId ?? (typeof window !== "undefined" ? window.location.hostname : "localhost");
3642
3669
  const rpName = config.rpName ?? "Decane Connect";
3643
3670
  const PERSIST_KEY = makePersistKey(config.apiKey);
3671
+ const LAST_USER_KEY = makeLastUserKey(config.apiKey);
3644
3672
  const teeUrl = config.teeUrl ?? DEFAULT_TEE_URL;
3645
3673
  const events = new EventEmitter();
3646
3674
  const api = new ApiClient(baseUrl, config.apiKey, config.appId);
@@ -3653,6 +3681,7 @@ async function createDecaneConnect(config) {
3653
3681
  );
3654
3682
  let auth = null;
3655
3683
  let persisted = loadPersisted(PERSIST_KEY);
3684
+ let lastUserId = loadLastUser(LAST_USER_KEY);
3656
3685
  let ethProvider = null;
3657
3686
  const persistSession = config.persistSession !== false;
3658
3687
  const serverAssistedRecovery = true;
@@ -3798,6 +3827,10 @@ async function createDecaneConnect(config) {
3798
3827
  const stored = await storage.getShare(persisted.userId);
3799
3828
  return hasPasswordUnlock(stored) ? stored : null;
3800
3829
  }
3830
+ if (lastUserId) {
3831
+ const stored = await storage.getShare(lastUserId);
3832
+ return hasPasswordUnlock(stored) ? stored : null;
3833
+ }
3801
3834
  const candidates = (await storage.listShares()).filter(hasPasswordUnlock);
3802
3835
  return candidates.length === 1 ? candidates[0] : null;
3803
3836
  }
@@ -4251,14 +4284,16 @@ async function createDecaneConnect(config) {
4251
4284
  async function passkeyDeviceCandidate() {
4252
4285
  if (!isPasskeySupported()) return null;
4253
4286
  if (persisted?.userId) return passkeyLoginCandidate();
4287
+ if (lastUserId) return usablePasskeyShare(await storage.getShare(lastUserId));
4254
4288
  const candidates = (await storage.listShares()).map(usablePasskeyShare).filter(Boolean);
4255
4289
  return candidates.length === 1 ? candidates[0] : null;
4256
4290
  }
4257
4291
  async function canOfferPasskey() {
4258
4292
  if (!isPasskeySupported()) return false;
4259
4293
  if (await passkeyDeviceCandidate()) return true;
4260
- if (!persisted?.userId) return false;
4261
- return await storage.getShare(persisted.userId) == null;
4294
+ const known = persisted?.userId ?? lastUserId;
4295
+ if (!known) return false;
4296
+ return await storage.getShare(known) == null;
4262
4297
  }
4263
4298
  async function rememberProfile(userId, profile) {
4264
4299
  const incoming = Object.fromEntries(Object.entries(profile).filter(([, v]) => v));
@@ -4368,7 +4403,8 @@ async function createDecaneConnect(config) {
4368
4403
  return {
4369
4404
  addresses,
4370
4405
  isNewUser: result.isNewUser,
4371
- ...result.profile ? { profile: result.profile } : {}
4406
+ ...result.profile ? { profile: result.profile } : {},
4407
+ ...result.providerSubject ? { providerSubject: result.providerSubject } : {}
4372
4408
  };
4373
4409
  }
4374
4410
  const client = {
@@ -4470,8 +4506,9 @@ async function createDecaneConnect(config) {
4470
4506
  async signInWithPasskey() {
4471
4507
  const candidate = await passkeyDeviceCandidate();
4472
4508
  if (candidate) return passkeyLogin(candidate);
4473
- if (isPasskeySupported() && persisted?.userId && await storage.getShare(persisted.userId) == null) {
4474
- return passkeyLoginWithoutShare(persisted.userId);
4509
+ const known = persisted?.userId ?? lastUserId;
4510
+ if (isPasskeySupported() && known && await storage.getShare(known) == null) {
4511
+ return passkeyLoginWithoutShare(known);
4475
4512
  }
4476
4513
  throw new NewDeviceError();
4477
4514
  },
@@ -4811,12 +4848,21 @@ async function createDecaneConnect(config) {
4811
4848
  await api.revoke(auth.jwt).catch(() => {
4812
4849
  });
4813
4850
  }
4851
+ const leaving = auth?.userId ?? persisted?.userId ?? null;
4852
+ if (leaving) {
4853
+ lastUserId = leaving;
4854
+ saveLastUser(LAST_USER_KEY, leaving);
4855
+ }
4814
4856
  auth = null;
4815
4857
  persisted = null;
4816
4858
  clearPersisted(PERSIST_KEY);
4817
4859
  dropLiveSession();
4818
4860
  events.emit("disconnected");
4819
4861
  },
4862
+ forgetLastUser() {
4863
+ lastUserId = null;
4864
+ clearLastUser(LAST_USER_KEY);
4865
+ },
4820
4866
  getAddresses() {
4821
4867
  return auth?.addresses ?? persisted?.addresses ?? null;
4822
4868
  },
@@ -7188,6 +7234,7 @@ var DEFAULT_SOCIAL = {
7188
7234
  addresses: null,
7189
7235
  isNewUser: false,
7190
7236
  profile: null,
7237
+ providerSubject: null,
7191
7238
  isUnlocked: false,
7192
7239
  sessionExpiresAt: null,
7193
7240
  needsReconnect: false,
@@ -7323,6 +7370,7 @@ function DecaneKit({ config, children }) {
7323
7370
  addresses: result.addresses,
7324
7371
  isNewUser: result.isNewUser,
7325
7372
  profile: result.profile ?? null,
7373
+ providerSubject: result.providerSubject ?? null,
7326
7374
  isUnlocked: sdk?.isUnlocked() ?? false,
7327
7375
  sessionExpiresAt: sdk?.sessionExpiresAt() ?? null,
7328
7376
  needsReconnect: false,
@@ -7529,6 +7577,14 @@ function DecaneKit({ config, children }) {
7529
7577
  if (!sdk) return false;
7530
7578
  return sdk.canUnlockWithPassword();
7531
7579
  }, []);
7580
+ const socialForgetLastUser = useCallback3(() => {
7581
+ socialRef.current?.forgetLastUser();
7582
+ const sdk = socialRef.current;
7583
+ if (sdk) {
7584
+ sdk.canSignInWithPasskey().then((can) => setSocialAuth((p) => ({ ...p, canUsePasskey: can }))).catch(() => {
7585
+ });
7586
+ }
7587
+ }, []);
7532
7588
  const socialUnlockWithPassword = useCallback3(async (password) => {
7533
7589
  const sdk = socialRef.current;
7534
7590
  if (!sdk) throw new Error("Social wallet not connected");
@@ -7605,6 +7661,7 @@ function DecaneKit({ config, children }) {
7605
7661
  socialAddPasskey,
7606
7662
  socialSetUnlockPassword,
7607
7663
  socialCanUnlockWithPassword,
7664
+ socialForgetLastUser,
7608
7665
  socialUnlockWithPassword,
7609
7666
  socialRemoveUnlockPassword,
7610
7667
  socialShouldPromptRecovery,
@@ -7855,6 +7912,9 @@ function useSocialAuth() {
7855
7912
  // user first signed in, so it carries the same name and email a Google
7856
7913
  // sign-in does rather than coming back anonymous.
7857
7914
  profile: socialAuth.profile,
7915
+ // The provider's stable id, where it gives one. Null otherwise. Safe to
7916
+ // match an account on, unlike the handle in `profile.username`.
7917
+ providerSubject: socialAuth.providerSubject,
7858
7918
  // Long-running work in progress, for driving your own status UI.
7859
7919
  phase: socialAuth.phase,
7860
7920
  isCreatingWallet: socialAuth.phase === "creating",
@@ -7911,6 +7971,7 @@ function useSocialWallet() {
7911
7971
  socialAddPasskey,
7912
7972
  socialSetUnlockPassword,
7913
7973
  socialCanUnlockWithPassword,
7974
+ socialForgetLastUser,
7914
7975
  socialUnlockWithPassword,
7915
7976
  socialRemoveUnlockPassword,
7916
7977
  socialShouldPromptRecovery,
@@ -8019,6 +8080,7 @@ function useSocialWallet() {
8019
8080
  throw e;
8020
8081
  }
8021
8082
  }, [socialSetUnlockPassword]);
8083
+ const forgetLastUser = useCallback4(() => socialForgetLastUser(), [socialForgetLastUser]);
8022
8084
  const canUnlockWithPassword = useCallback4(
8023
8085
  () => socialCanUnlockWithPassword(),
8024
8086
  [socialCanUnlockWithPassword]
@@ -8104,6 +8166,8 @@ function useSocialWallet() {
8104
8166
  canUnlockWithPassword,
8105
8167
  unlockWithPassword,
8106
8168
  removeUnlockPassword,
8169
+ // "Use a different account": stop offering the account that signed out.
8170
+ forgetLastUser,
8107
8171
  // Recovery (2-of-3 Shamir share set)
8108
8172
  rotateShares,
8109
8173
  exportPortableBackup,