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.cjs CHANGED
@@ -3689,6 +3689,29 @@ function clearPersisted(key) {
3689
3689
  } catch {
3690
3690
  }
3691
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
+ }
3692
3715
  function sessionPersistKey(apiKey) {
3693
3716
  return `decane:sess:${apiKey.slice(0, 20)}`;
3694
3717
  }
@@ -3727,6 +3750,7 @@ async function createDecaneConnect(config) {
3727
3750
  const rpId = config.rpId ?? (typeof window !== "undefined" ? window.location.hostname : "localhost");
3728
3751
  const rpName = config.rpName ?? "Decane Connect";
3729
3752
  const PERSIST_KEY = makePersistKey(config.apiKey);
3753
+ const LAST_USER_KEY = makeLastUserKey(config.apiKey);
3730
3754
  const teeUrl = config.teeUrl ?? DEFAULT_TEE_URL;
3731
3755
  const events = new EventEmitter();
3732
3756
  const api = new ApiClient(baseUrl, config.apiKey, config.appId);
@@ -3739,8 +3763,10 @@ async function createDecaneConnect(config) {
3739
3763
  );
3740
3764
  let auth = null;
3741
3765
  let persisted = loadPersisted(PERSIST_KEY);
3766
+ let lastUserId = loadLastUser(LAST_USER_KEY);
3742
3767
  let ethProvider = null;
3743
3768
  const persistSession = config.persistSession !== false;
3769
+ const deferDeviceProtection = config.deferDeviceProtection === true;
3744
3770
  const serverAssistedRecovery = true;
3745
3771
  const RECOVERY_OFFER_TIMEOUT_MS = 5 * 6e4;
3746
3772
  const selfHealAttempted = /* @__PURE__ */ new Set();
@@ -3884,6 +3910,10 @@ async function createDecaneConnect(config) {
3884
3910
  const stored = await storage.getShare(persisted.userId);
3885
3911
  return hasPasswordUnlock(stored) ? stored : null;
3886
3912
  }
3913
+ if (lastUserId) {
3914
+ const stored = await storage.getShare(lastUserId);
3915
+ return hasPasswordUnlock(stored) ? stored : null;
3916
+ }
3887
3917
  const candidates = (await storage.listShares()).filter(hasPasswordUnlock);
3888
3918
  return candidates.length === 1 ? candidates[0] : null;
3889
3919
  }
@@ -4048,11 +4078,12 @@ async function createDecaneConnect(config) {
4048
4078
  }
4049
4079
  async function firstTimeSetup(userId, jwt) {
4050
4080
  prewarmAttestation(teeUrl);
4051
- let unlockMethod;
4052
- let wrappingKey;
4081
+ let unlockMethod = "password";
4082
+ let wrappingKey = null;
4053
4083
  let meta = {};
4054
4084
  let unlockTokenKey = null;
4055
- if (isPasskeySupported()) {
4085
+ if (deferDeviceProtection) {
4086
+ } else if (isPasskeySupported()) {
4056
4087
  const prfSalt = generateRandomBytes(32);
4057
4088
  try {
4058
4089
  const { credentialId, prfOutput } = await registerWebAuthnCredential({ api, jwt, rpId, rpName, prfSalt });
@@ -4093,32 +4124,37 @@ async function createDecaneConnect(config) {
4093
4124
  } finally {
4094
4125
  zeroize(master);
4095
4126
  }
4096
- const iv = generateRandomBytes(12);
4097
- const wrappedDeviceShare = await aesGcmEncrypt(
4098
- wrappingKey,
4099
- shareSet.device,
4100
- iv
4101
- );
4102
- await storage.putShare({
4103
- userId,
4104
- wrappedDeviceShare,
4105
- iv,
4106
- unlockMethod,
4107
- credentialId: meta.credentialId ? b64uToBuffer(meta.credentialId) : void 0,
4108
- prfSalt: meta.prfSalt ? b64uToBuffer(meta.prfSalt) : void 0,
4109
- pinSalt: meta.pinSalt ? b64uToBuffer(meta.pinSalt) : void 0,
4110
- pinIterations: meta.pinIterations,
4111
- passwordSalt: meta.passwordSalt ? b64uToBuffer(meta.passwordSalt) : void 0,
4112
- passwordIterations: meta.passwordIterations,
4113
- version: 1
4114
- });
4115
- if (unlockTokenKey) await attachUnlockToken(userId, jwt, unlockTokenKey);
4127
+ let wrappedDeviceShare = null;
4128
+ let iv = null;
4129
+ if (wrappingKey) {
4130
+ iv = generateRandomBytes(12);
4131
+ wrappedDeviceShare = await aesGcmEncrypt(
4132
+ wrappingKey,
4133
+ shareSet.device,
4134
+ iv
4135
+ );
4136
+ await storage.putShare({
4137
+ userId,
4138
+ wrappedDeviceShare,
4139
+ iv,
4140
+ unlockMethod,
4141
+ credentialId: meta.credentialId ? b64uToBuffer(meta.credentialId) : void 0,
4142
+ prfSalt: meta.prfSalt ? b64uToBuffer(meta.prfSalt) : void 0,
4143
+ pinSalt: meta.pinSalt ? b64uToBuffer(meta.pinSalt) : void 0,
4144
+ pinIterations: meta.pinIterations,
4145
+ passwordSalt: meta.passwordSalt ? b64uToBuffer(meta.passwordSalt) : void 0,
4146
+ passwordIterations: meta.passwordIterations,
4147
+ version: 1
4148
+ });
4149
+ if (unlockTokenKey) await attachUnlockToken(userId, jwt, unlockTokenKey);
4150
+ }
4116
4151
  const serverIv = generateRandomBytes(12);
4117
4152
  await withDeadline(api.putShare(jwt, {
4118
4153
  ciphertext: shareSet.server,
4119
4154
  iv: serverIv,
4120
4155
  wrappedShareMeta: {
4121
- unlockMethod
4156
+ // The server records this on the wallet_created event and nothing else.
4157
+ unlockMethod: deferDeviceProtection ? "deferred" : unlockMethod
4122
4158
  // credential IDs, prfSalt, pinSalt stay on-device — never sent to server
4123
4159
  },
4124
4160
  evmAddress,
@@ -4127,7 +4163,7 @@ async function createDecaneConnect(config) {
4127
4163
  }), remainingBudget(), "storing your wallet");
4128
4164
  zeroize(shareSet.server);
4129
4165
  const addresses = { evm: evmAddress, solana: solanaAddress, tron: tronAddress };
4130
- if (unlockMethod === "passkey" && meta.credentialId && meta.prfSalt) {
4166
+ if (unlockMethod === "passkey" && meta.credentialId && meta.prfSalt && wrappedDeviceShare && iv) {
4131
4167
  try {
4132
4168
  await api.putRecoveryBackup(jwt, {
4133
4169
  wrappedDeviceShare,
@@ -4210,7 +4246,7 @@ async function createDecaneConnect(config) {
4210
4246
  const tee = new TeeClient(teeUrl);
4211
4247
  const provisioned = await tee.provisionDeviceShare(jwt);
4212
4248
  if (provisioned) {
4213
- await persistProvisionedShare(userId, jwt, provisioned);
4249
+ if (!deferDeviceProtection) await persistProvisionedShare(userId, jwt, provisioned);
4214
4250
  let addrs;
4215
4251
  try {
4216
4252
  addrs = await session.start(provisioned, jwt, recordedAddresses);
@@ -4254,7 +4290,7 @@ async function createDecaneConnect(config) {
4254
4290
  });
4255
4291
  deviceShare = recovered.deviceShare;
4256
4292
  joinedViaPasskeySync = recovered.flow === "passkey-sync";
4257
- if (!await storage.getShare(userId) && isPasskeySupported()) {
4293
+ if (!deferDeviceProtection && !await storage.getShare(userId) && isPasskeySupported()) {
4258
4294
  await registerPasskeyAndBackup(userId, jwt, deviceShare);
4259
4295
  }
4260
4296
  } else {
@@ -4337,14 +4373,16 @@ async function createDecaneConnect(config) {
4337
4373
  async function passkeyDeviceCandidate() {
4338
4374
  if (!isPasskeySupported()) return null;
4339
4375
  if (persisted?.userId) return passkeyLoginCandidate();
4376
+ if (lastUserId) return usablePasskeyShare(await storage.getShare(lastUserId));
4340
4377
  const candidates = (await storage.listShares()).map(usablePasskeyShare).filter(Boolean);
4341
4378
  return candidates.length === 1 ? candidates[0] : null;
4342
4379
  }
4343
4380
  async function canOfferPasskey() {
4344
4381
  if (!isPasskeySupported()) return false;
4345
4382
  if (await passkeyDeviceCandidate()) return true;
4346
- if (!persisted?.userId) return false;
4347
- return await storage.getShare(persisted.userId) == null;
4383
+ const known = persisted?.userId ?? lastUserId;
4384
+ if (!known) return false;
4385
+ return await storage.getShare(known) == null;
4348
4386
  }
4349
4387
  async function rememberProfile(userId, profile) {
4350
4388
  const incoming = Object.fromEntries(Object.entries(profile).filter(([, v]) => v));
@@ -4557,8 +4595,9 @@ async function createDecaneConnect(config) {
4557
4595
  async signInWithPasskey() {
4558
4596
  const candidate = await passkeyDeviceCandidate();
4559
4597
  if (candidate) return passkeyLogin(candidate);
4560
- if (isPasskeySupported() && persisted?.userId && await storage.getShare(persisted.userId) == null) {
4561
- return passkeyLoginWithoutShare(persisted.userId);
4598
+ const known = persisted?.userId ?? lastUserId;
4599
+ if (isPasskeySupported() && known && await storage.getShare(known) == null) {
4600
+ return passkeyLoginWithoutShare(known);
4562
4601
  }
4563
4602
  throw new NewDeviceError();
4564
4603
  },
@@ -4722,6 +4761,24 @@ async function createDecaneConnect(config) {
4722
4761
  zeroize(deviceShare);
4723
4762
  }
4724
4763
  },
4764
+ async deviceProtected() {
4765
+ if (!auth) return false;
4766
+ return await storage.getShare(auth.userId) !== null;
4767
+ },
4768
+ async protectDevice() {
4769
+ if (!auth) throw new ReconnectRequiredError();
4770
+ if (await storage.getShare(auth.userId)) return true;
4771
+ if (!serverAssistedRecovery) throw new NewDeviceError();
4772
+ const tee = new TeeClient(teeUrl);
4773
+ const provisioned = await tee.provisionDeviceShare(auth.jwt);
4774
+ if (!provisioned) throw new NewDeviceError();
4775
+ try {
4776
+ await persistProvisionedShare(auth.userId, auth.jwt, provisioned);
4777
+ } finally {
4778
+ zeroize(provisioned);
4779
+ }
4780
+ return await storage.getShare(auth.userId) !== null;
4781
+ },
4725
4782
  async unlock() {
4726
4783
  if (!auth) {
4727
4784
  const candidate = await passkeyLoginCandidate();
@@ -4898,12 +4955,21 @@ async function createDecaneConnect(config) {
4898
4955
  await api.revoke(auth.jwt).catch(() => {
4899
4956
  });
4900
4957
  }
4958
+ const leaving = auth?.userId ?? persisted?.userId ?? null;
4959
+ if (leaving) {
4960
+ lastUserId = leaving;
4961
+ saveLastUser(LAST_USER_KEY, leaving);
4962
+ }
4901
4963
  auth = null;
4902
4964
  persisted = null;
4903
4965
  clearPersisted(PERSIST_KEY);
4904
4966
  dropLiveSession();
4905
4967
  events.emit("disconnected");
4906
4968
  },
4969
+ forgetLastUser() {
4970
+ lastUserId = null;
4971
+ clearLastUser(LAST_USER_KEY);
4972
+ },
4907
4973
  getAddresses() {
4908
4974
  return auth?.addresses ?? persisted?.addresses ?? null;
4909
4975
  },
@@ -7335,6 +7401,7 @@ function DecaneKit({ config, children }) {
7335
7401
  chains,
7336
7402
  rpcUrls,
7337
7403
  requireAssertionPerSignature,
7404
+ deferDeviceProtection,
7338
7405
  // Recovery callbacks. Forwarding these is what makes recovery
7339
7406
  // configurable at all from <DecaneKit>; before, they were silently
7340
7407
  // dropped and the headless client was constructed without them.
@@ -7355,6 +7422,7 @@ function DecaneKit({ config, children }) {
7355
7422
  promptPin,
7356
7423
  promptUnlockPassword,
7357
7424
  requireAssertionPerSignature,
7425
+ deferDeviceProtection,
7358
7426
  offerRecoveryAtSignup,
7359
7427
  onRecoveryShareOffer,
7360
7428
  onRecoveryFileReady,
@@ -7601,6 +7669,16 @@ function DecaneKit({ config, children }) {
7601
7669
  if (!sdk) throw new Error("Social wallet not connected");
7602
7670
  return sdk.addPasskey();
7603
7671
  }, []);
7672
+ const socialDeviceProtected = (0, import_react5.useCallback)(async () => {
7673
+ const sdk = socialRef.current;
7674
+ if (!sdk) return false;
7675
+ return sdk.deviceProtected();
7676
+ }, []);
7677
+ const socialProtectDevice = (0, import_react5.useCallback)(async () => {
7678
+ const sdk = socialRef.current;
7679
+ if (!sdk) throw new Error("Social wallet not connected");
7680
+ return sdk.protectDevice();
7681
+ }, []);
7604
7682
  const socialSetUnlockPassword = (0, import_react5.useCallback)(async (password) => {
7605
7683
  const sdk = socialRef.current;
7606
7684
  if (!sdk) throw new Error("Social wallet not connected");
@@ -7611,6 +7689,14 @@ function DecaneKit({ config, children }) {
7611
7689
  if (!sdk) return false;
7612
7690
  return sdk.canUnlockWithPassword();
7613
7691
  }, []);
7692
+ const socialForgetLastUser = (0, import_react5.useCallback)(() => {
7693
+ socialRef.current?.forgetLastUser();
7694
+ const sdk = socialRef.current;
7695
+ if (sdk) {
7696
+ sdk.canSignInWithPasskey().then((can) => setSocialAuth((p) => ({ ...p, canUsePasskey: can }))).catch(() => {
7697
+ });
7698
+ }
7699
+ }, []);
7614
7700
  const socialUnlockWithPassword = (0, import_react5.useCallback)(async (password) => {
7615
7701
  const sdk = socialRef.current;
7616
7702
  if (!sdk) throw new Error("Social wallet not connected");
@@ -7685,8 +7771,11 @@ function DecaneKit({ config, children }) {
7685
7771
  socialHasRecoveryShare,
7686
7772
  socialHasPasskey,
7687
7773
  socialAddPasskey,
7774
+ socialDeviceProtected,
7775
+ socialProtectDevice,
7688
7776
  socialSetUnlockPassword,
7689
7777
  socialCanUnlockWithPassword,
7778
+ socialForgetLastUser,
7690
7779
  socialUnlockWithPassword,
7691
7780
  socialRemoveUnlockPassword,
7692
7781
  socialShouldPromptRecovery,
@@ -7994,8 +8083,11 @@ function useSocialWallet() {
7994
8083
  socialHasRecoveryShare,
7995
8084
  socialHasPasskey,
7996
8085
  socialAddPasskey,
8086
+ socialDeviceProtected,
8087
+ socialProtectDevice,
7997
8088
  socialSetUnlockPassword,
7998
8089
  socialCanUnlockWithPassword,
8090
+ socialForgetLastUser,
7999
8091
  socialUnlockWithPassword,
8000
8092
  socialRemoveUnlockPassword,
8001
8093
  socialShouldPromptRecovery,
@@ -8095,6 +8187,16 @@ function useSocialWallet() {
8095
8187
  throw e;
8096
8188
  }
8097
8189
  }, [socialAddPasskey]);
8190
+ const deviceProtected = (0, import_react6.useCallback)(() => run(() => socialDeviceProtected()), [socialDeviceProtected]);
8191
+ const protectDevice = (0, import_react6.useCallback)(async () => {
8192
+ setError(null);
8193
+ try {
8194
+ return await socialProtectDevice();
8195
+ } catch (e) {
8196
+ setError(e.message);
8197
+ throw e;
8198
+ }
8199
+ }, [socialProtectDevice]);
8098
8200
  const setUnlockPassword = (0, import_react6.useCallback)(async (password) => {
8099
8201
  setError(null);
8100
8202
  try {
@@ -8104,6 +8206,7 @@ function useSocialWallet() {
8104
8206
  throw e;
8105
8207
  }
8106
8208
  }, [socialSetUnlockPassword]);
8209
+ const forgetLastUser = (0, import_react6.useCallback)(() => socialForgetLastUser(), [socialForgetLastUser]);
8107
8210
  const canUnlockWithPassword = (0, import_react6.useCallback)(
8108
8211
  () => socialCanUnlockWithPassword(),
8109
8212
  [socialCanUnlockWithPassword]
@@ -8182,6 +8285,10 @@ function useSocialWallet() {
8182
8285
  // Passkey management for THIS device
8183
8286
  hasPasskey,
8184
8287
  addPasskey,
8288
+ // Deferred protection (deferDeviceProtection): whether this device holds
8289
+ // a wrapped share, and the call that stores one — passkey or password.
8290
+ deviceProtected,
8291
+ protectDevice,
8185
8292
  // "Unlock with your password": setUnlockPassword needs a live session;
8186
8293
  // canUnlockWithPassword and unlockWithPassword are the signed-out pair that
8187
8294
  // open one without an identity provider or a passkey.
@@ -8189,6 +8296,8 @@ function useSocialWallet() {
8189
8296
  canUnlockWithPassword,
8190
8297
  unlockWithPassword,
8191
8298
  removeUnlockPassword,
8299
+ // "Use a different account": stop offering the account that signed out.
8300
+ forgetLastUser,
8192
8301
  // Recovery (2-of-3 Shamir share set)
8193
8302
  rotateShares,
8194
8303
  exportPortableBackup,