decane-connect-kit 2.24.1 → 2.26.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
@@ -159,6 +159,27 @@ interface DecaneConnectConfig {
159
159
  * has happened. Default false: the share is wrapped and stored at creation.
160
160
  */
161
161
  deferDeviceProtection?: boolean;
162
+ /**
163
+ * Resume a live session in a NEW TAB, not just after a reload. Default false.
164
+ *
165
+ * The session — handle plus JWT — normally lives in sessionStorage, so it
166
+ * survives a reload and dies with the tab. That makes a second tab, or a
167
+ * return visit an hour later, a full sign-in and unlock again even though the
168
+ * enclave session is still valid for two hours.
169
+ *
170
+ * With this on, the session is held in localStorage under the same expiry, so
171
+ * a returning user is signing again in about a second.
172
+ *
173
+ * THE TRADE, stated plainly: the handle IS the signing credential — no other
174
+ * header is required to sign with it. Widening its storage widens the window
175
+ * in which anything able to run script on your origin could take it, from one
176
+ * tab to every tab until it expires. It is off by default for that reason.
177
+ *
178
+ * Turn it on when the return-visit cost matters more than that window, and
179
+ * pair it with `requireAssertionPerSignature` so a stolen handle still cannot
180
+ * move funds without a fresh authenticator touch.
181
+ */
182
+ resumeSessionAcrossTabs?: boolean;
162
183
  /**
163
184
  * Offer to save a recovery-share file at signup. **Default false** since
164
185
  * 2.7.4 — opt in explicitly with `true`.
@@ -287,7 +308,13 @@ interface SignTypedDataRequest {
287
308
  message: Record<string, unknown>;
288
309
  primaryType?: string;
289
310
  }
290
- type SdkEvent = 'connected' | 'disconnected' | 'session-expired' | 'session-started' | 'wallet-creating' | 'wallet-unlocking'
311
+ type SdkEvent = 'connected' | 'disconnected' | 'session-expired'
312
+ /**
313
+ * The access token was renewed while the user was active, so the session
314
+ * continued instead of ending. Informational: apps that cache the token need
315
+ * to re-read it, most do not.
316
+ */
317
+ | 'session-renewed' | 'session-started' | 'wallet-creating' | 'wallet-unlocking'
291
318
  /**
292
319
  * Emitted when this wallet could not be enrolled for server-assisted
293
320
  * recovery. Not fatal — the wallet works — but until it is enrolled the user
@@ -716,6 +743,16 @@ declare class ApiClient {
716
743
  token: string;
717
744
  expiresInDays: number;
718
745
  }>;
746
+ /**
747
+ * Trade a still-valid token for a fresh one, so an active user is never
748
+ * signed out mid-use. The backend caps the whole chain by session age, so
749
+ * this cannot renew forever; a 401 means "sign in again", not "retry".
750
+ */
751
+ refreshAccessToken(jwt: string): Promise<{
752
+ jwt: string;
753
+ expiresIn: number;
754
+ sessionExpiresIn: number;
755
+ }>;
719
756
  /** Redeem a decrypted unlock token for a session. */
720
757
  exchangeUnlockToken(token: string): Promise<{
721
758
  jwt: string;
package/dist/index.d.ts CHANGED
@@ -159,6 +159,27 @@ interface DecaneConnectConfig {
159
159
  * has happened. Default false: the share is wrapped and stored at creation.
160
160
  */
161
161
  deferDeviceProtection?: boolean;
162
+ /**
163
+ * Resume a live session in a NEW TAB, not just after a reload. Default false.
164
+ *
165
+ * The session — handle plus JWT — normally lives in sessionStorage, so it
166
+ * survives a reload and dies with the tab. That makes a second tab, or a
167
+ * return visit an hour later, a full sign-in and unlock again even though the
168
+ * enclave session is still valid for two hours.
169
+ *
170
+ * With this on, the session is held in localStorage under the same expiry, so
171
+ * a returning user is signing again in about a second.
172
+ *
173
+ * THE TRADE, stated plainly: the handle IS the signing credential — no other
174
+ * header is required to sign with it. Widening its storage widens the window
175
+ * in which anything able to run script on your origin could take it, from one
176
+ * tab to every tab until it expires. It is off by default for that reason.
177
+ *
178
+ * Turn it on when the return-visit cost matters more than that window, and
179
+ * pair it with `requireAssertionPerSignature` so a stolen handle still cannot
180
+ * move funds without a fresh authenticator touch.
181
+ */
182
+ resumeSessionAcrossTabs?: boolean;
162
183
  /**
163
184
  * Offer to save a recovery-share file at signup. **Default false** since
164
185
  * 2.7.4 — opt in explicitly with `true`.
@@ -287,7 +308,13 @@ interface SignTypedDataRequest {
287
308
  message: Record<string, unknown>;
288
309
  primaryType?: string;
289
310
  }
290
- type SdkEvent = 'connected' | 'disconnected' | 'session-expired' | 'session-started' | 'wallet-creating' | 'wallet-unlocking'
311
+ type SdkEvent = 'connected' | 'disconnected' | 'session-expired'
312
+ /**
313
+ * The access token was renewed while the user was active, so the session
314
+ * continued instead of ending. Informational: apps that cache the token need
315
+ * to re-read it, most do not.
316
+ */
317
+ | 'session-renewed' | 'session-started' | 'wallet-creating' | 'wallet-unlocking'
291
318
  /**
292
319
  * Emitted when this wallet could not be enrolled for server-assisted
293
320
  * recovery. Not fatal — the wallet works — but until it is enrolled the user
@@ -716,6 +743,16 @@ declare class ApiClient {
716
743
  token: string;
717
744
  expiresInDays: number;
718
745
  }>;
746
+ /**
747
+ * Trade a still-valid token for a fresh one, so an active user is never
748
+ * signed out mid-use. The backend caps the whole chain by session age, so
749
+ * this cannot renew forever; a 401 means "sign in again", not "retry".
750
+ */
751
+ refreshAccessToken(jwt: string): Promise<{
752
+ jwt: string;
753
+ expiresIn: number;
754
+ sessionExpiresIn: number;
755
+ }>;
719
756
  /** Redeem a decrypted unlock token for a session. */
720
757
  exchangeUnlockToken(token: string): Promise<{
721
758
  jwt: string;
package/dist/index.js CHANGED
@@ -2193,6 +2193,17 @@ var ApiClient = class {
2193
2193
  headers: { Authorization: `Bearer ${jwt}` }
2194
2194
  });
2195
2195
  }
2196
+ /**
2197
+ * Trade a still-valid token for a fresh one, so an active user is never
2198
+ * signed out mid-use. The backend caps the whole chain by session age, so
2199
+ * this cannot renew forever; a 401 means "sign in again", not "retry".
2200
+ */
2201
+ async refreshAccessToken(jwt) {
2202
+ return this._request("/auth/refresh", {
2203
+ method: "POST",
2204
+ headers: { Authorization: `Bearer ${jwt}` }
2205
+ });
2206
+ }
2196
2207
  /** Redeem a decrypted unlock token for a session. */
2197
2208
  async exchangeUnlockToken(token) {
2198
2209
  return this._request("/auth/unlock-token/exchange", {
@@ -2331,9 +2342,62 @@ import { sha256, encodeBase58 } from "ethers";
2331
2342
  // src/social/teeClient.ts
2332
2343
  import { startAuthentication } from "@simplewebauthn/browser";
2333
2344
 
2345
+ // src/social/attestation.ts
2346
+ import {
2347
+ getCollateral,
2348
+ getCollateralAndVerify,
2349
+ verify as verifyQuoteWithCollateral,
2350
+ PHALA_PCCS_URL
2351
+ } from "@phala/dcap-qvl";
2352
+
2353
+ // src/social/collateralCache.ts
2354
+ var CACHE_PREFIX = "decane:collateral:v1:";
2355
+ var COLLATERAL_TTL_MS = 12 * 60 * 60 * 1e3;
2356
+ function storage() {
2357
+ try {
2358
+ return typeof localStorage !== "undefined" ? localStorage : null;
2359
+ } catch {
2360
+ return null;
2361
+ }
2362
+ }
2363
+ function keyFor(teeUrl) {
2364
+ return `${CACHE_PREFIX}${teeUrl}`;
2365
+ }
2366
+ function readCollateral(teeUrl) {
2367
+ const store = storage();
2368
+ if (!store) return null;
2369
+ try {
2370
+ const raw = store.getItem(keyFor(teeUrl));
2371
+ if (!raw) return null;
2372
+ const entry = JSON.parse(raw);
2373
+ if (!entry?.at || Date.now() - entry.at > COLLATERAL_TTL_MS) {
2374
+ store.removeItem(keyFor(teeUrl));
2375
+ return null;
2376
+ }
2377
+ return entry.collateral ?? null;
2378
+ } catch {
2379
+ return null;
2380
+ }
2381
+ }
2382
+ function writeCollateral(teeUrl, collateral) {
2383
+ const store = storage();
2384
+ if (!store) return;
2385
+ try {
2386
+ store.setItem(keyFor(teeUrl), JSON.stringify({ at: Date.now(), collateral }));
2387
+ } catch {
2388
+ }
2389
+ }
2390
+ function dropCollateral(teeUrl) {
2391
+ const store = storage();
2392
+ if (!store) return;
2393
+ try {
2394
+ store.removeItem(keyFor(teeUrl));
2395
+ } catch {
2396
+ }
2397
+ }
2398
+
2334
2399
  // src/social/attestation.ts
2335
2400
  init_errors();
2336
- import { getCollateralAndVerify } from "@phala/dcap-qvl";
2337
2401
 
2338
2402
  // ../../infra/tee-measurement.json
2339
2403
  var tee_measurement_default = {
@@ -2360,6 +2424,9 @@ function hexToBytes(hex) {
2360
2424
  function bytesToHex(bytes) {
2361
2425
  return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
2362
2426
  }
2427
+ function nowSeconds() {
2428
+ return Math.floor(Date.now() / 1e3);
2429
+ }
2363
2430
  async function verifyTeeAttestation(teeUrl, nonce) {
2364
2431
  const res = await fetch(`${teeUrl}/attestation`, {
2365
2432
  method: "POST",
@@ -2369,9 +2436,22 @@ async function verifyTeeAttestation(teeUrl, nonce) {
2369
2436
  if (!res.ok) throw new AttestationError("quote fetch failed");
2370
2437
  const { quote, measurement } = await res.json();
2371
2438
  let verified;
2439
+ const cached = readCollateral(teeUrl);
2372
2440
  try {
2373
- verified = await getCollateralAndVerify(hexToBytes(quote));
2374
- } catch {
2441
+ if (cached) {
2442
+ try {
2443
+ verified = await verifyQuoteWithCollateral(hexToBytes(quote), cached, nowSeconds());
2444
+ } catch {
2445
+ dropCollateral(teeUrl);
2446
+ verified = await getCollateralAndVerify(hexToBytes(quote));
2447
+ }
2448
+ } else {
2449
+ const collateral = await getCollateral(PHALA_PCCS_URL, hexToBytes(quote));
2450
+ verified = await verifyQuoteWithCollateral(hexToBytes(quote), collateral, nowSeconds());
2451
+ writeCollateral(teeUrl, collateral);
2452
+ }
2453
+ } catch (e) {
2454
+ if (e instanceof AttestationError) throw e;
2375
2455
  throw new AttestationError("quote signature invalid");
2376
2456
  }
2377
2457
  if (verified.status === "Revoked") {
@@ -2413,14 +2493,51 @@ async function teeFetch(url, init) {
2413
2493
  }
2414
2494
  var ATTESTATION_TTL_MS = 10 * 6e4;
2415
2495
  var attestationCache = /* @__PURE__ */ new Map();
2496
+ var ATTESTATION_PERSIST_PREFIX = "decane:attested:v1:";
2497
+ function persistedAt(teeUrl) {
2498
+ try {
2499
+ if (typeof sessionStorage === "undefined") return null;
2500
+ const raw = sessionStorage.getItem(ATTESTATION_PERSIST_PREFIX + teeUrl);
2501
+ const at = raw ? Number(raw) : NaN;
2502
+ return Number.isFinite(at) ? at : null;
2503
+ } catch {
2504
+ return null;
2505
+ }
2506
+ }
2507
+ function rememberAttested(teeUrl, at) {
2508
+ try {
2509
+ if (typeof sessionStorage !== "undefined") {
2510
+ sessionStorage.setItem(ATTESTATION_PERSIST_PREFIX + teeUrl, String(at));
2511
+ }
2512
+ } catch {
2513
+ }
2514
+ }
2515
+ function forgetAttested(teeUrl) {
2516
+ try {
2517
+ if (typeof sessionStorage !== "undefined") {
2518
+ sessionStorage.removeItem(ATTESTATION_PERSIST_PREFIX + teeUrl);
2519
+ }
2520
+ } catch {
2521
+ }
2522
+ }
2416
2523
  function attestOnce(teeUrl, verify) {
2417
2524
  const hit = attestationCache.get(teeUrl);
2418
2525
  if (hit && Date.now() - hit.at < ATTESTATION_TTL_MS) return hit.promise;
2419
- const promise = verify().catch((e) => {
2526
+ const at = persistedAt(teeUrl);
2527
+ if (at && Date.now() - at < ATTESTATION_TTL_MS) {
2528
+ const resolved = Promise.resolve();
2529
+ attestationCache.set(teeUrl, { at, promise: resolved });
2530
+ return resolved;
2531
+ }
2532
+ const startedAt = Date.now();
2533
+ const promise = verify().then(() => {
2534
+ rememberAttested(teeUrl, startedAt);
2535
+ }).catch((e) => {
2420
2536
  attestationCache.delete(teeUrl);
2537
+ forgetAttested(teeUrl);
2421
2538
  throw e;
2422
2539
  });
2423
- attestationCache.set(teeUrl, { at: Date.now(), promise });
2540
+ attestationCache.set(teeUrl, { at: startedAt, promise });
2424
2541
  return promise;
2425
2542
  }
2426
2543
  function prewarmAttestation(teeUrl) {
@@ -3692,29 +3809,44 @@ function clearLastUser(key) {
3692
3809
  function sessionPersistKey(apiKey) {
3693
3810
  return `decane:sess:${apiKey.slice(0, 20)}`;
3694
3811
  }
3812
+ var sessionStore = () => typeof sessionStorage !== "undefined" ? sessionStorage : null;
3813
+ function useCrossTabSessionStore(enabled) {
3814
+ sessionStore = enabled ? () => typeof localStorage !== "undefined" ? localStorage : null : () => typeof sessionStorage !== "undefined" ? sessionStorage : null;
3815
+ }
3695
3816
  function loadSession(key) {
3696
3817
  try {
3697
- const raw = typeof sessionStorage !== "undefined" ? sessionStorage.getItem(key) : null;
3698
- return raw ? JSON.parse(raw) : null;
3818
+ const raw = sessionStore()?.getItem(key) ?? (typeof sessionStorage !== "undefined" ? sessionStorage.getItem(key) : null) ?? (typeof localStorage !== "undefined" ? localStorage.getItem(key) : null);
3819
+ if (!raw) return null;
3820
+ const parsed = JSON.parse(raw);
3821
+ if (parsed?.expiresAt && parsed.expiresAt <= Date.now()) {
3822
+ clearSessionPersist(key);
3823
+ return null;
3824
+ }
3825
+ return parsed;
3699
3826
  } catch {
3700
3827
  return null;
3701
3828
  }
3702
3829
  }
3703
3830
  function saveSession(key, s) {
3704
3831
  try {
3705
- sessionStorage.setItem(key, JSON.stringify(s));
3832
+ sessionStore()?.setItem(key, JSON.stringify(s));
3706
3833
  } catch {
3707
3834
  }
3708
3835
  }
3709
3836
  function clearSessionPersist(key) {
3710
3837
  try {
3711
- sessionStorage.removeItem(key);
3838
+ if (typeof sessionStorage !== "undefined") sessionStorage.removeItem(key);
3839
+ } catch {
3840
+ }
3841
+ try {
3842
+ if (typeof localStorage !== "undefined") localStorage.removeItem(key);
3712
3843
  } catch {
3713
3844
  }
3714
3845
  }
3715
3846
  async function createDecaneConnect(config) {
3716
3847
  if (!config.appId) throw new Error("decane-connect-kit: appId is required");
3717
3848
  if (!config.apiKey) throw new Error("decane-connect-kit: apiKey is required");
3849
+ useCrossTabSessionStore(config.resumeSessionAcrossTabs === true);
3718
3850
  if (!config.chains?.length)
3719
3851
  throw new Error("decane-connect-kit: at least one chain is required");
3720
3852
  if (config.rotateOnNewDeviceRecovery && !config.onRecoveryRotated) {
@@ -3733,7 +3865,7 @@ async function createDecaneConnect(config) {
3733
3865
  const teeUrl = config.teeUrl ?? DEFAULT_TEE_URL;
3734
3866
  const events = new EventEmitter();
3735
3867
  const api = new ApiClient(baseUrl, config.apiKey, config.appId);
3736
- const storage = new Storage();
3868
+ const storage2 = new Storage();
3737
3869
  const session = new SessionManager(
3738
3870
  events,
3739
3871
  durationMs,
@@ -3831,6 +3963,7 @@ async function createDecaneConnect(config) {
3831
3963
  clearSessionPersist(SESSION_KEY);
3832
3964
  }
3833
3965
  let accessTokenExpiry = null;
3966
+ let tokenRefresh = null;
3834
3967
  function scheduleAccessTokenExpiry(jwt, expiresAt) {
3835
3968
  if (accessTokenExpiry) clearTimeout(accessTokenExpiry);
3836
3969
  accessTokenExpiry = setTimeout(() => {
@@ -3840,6 +3973,40 @@ async function createDecaneConnect(config) {
3840
3973
  dropLiveSession();
3841
3974
  events.emit("session-expired");
3842
3975
  }, Math.max(0, expiresAt - Date.now()));
3976
+ scheduleTokenRefresh(jwt, expiresAt);
3977
+ }
3978
+ function scheduleTokenRefresh(jwt, expiresAt) {
3979
+ if (tokenRefresh) clearTimeout(tokenRefresh);
3980
+ const lifetime = expiresAt - Date.now();
3981
+ if (lifetime <= 0) return;
3982
+ const at = Math.max(5e3, lifetime * 0.75);
3983
+ tokenRefresh = setTimeout(() => {
3984
+ tokenRefresh = null;
3985
+ void renewAccessToken(jwt);
3986
+ }, at);
3987
+ }
3988
+ async function renewAccessToken(expected) {
3989
+ if (auth?.jwt !== expected) return;
3990
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") {
3991
+ const onVisible = () => {
3992
+ document.removeEventListener("visibilitychange", onVisible);
3993
+ if (auth?.jwt === expected) void renewAccessToken(expected);
3994
+ };
3995
+ document.addEventListener("visibilitychange", onVisible);
3996
+ return;
3997
+ }
3998
+ try {
3999
+ const { jwt: fresh } = await api.refreshAccessToken(expected);
4000
+ if (auth?.jwt !== expected) return;
4001
+ auth = { ...auth, jwt: fresh };
4002
+ const freshExpiry = decodeAccessToken(fresh)?.expiresAt;
4003
+ if (freshExpiry) {
4004
+ scheduleAccessTokenExpiry(fresh, freshExpiry);
4005
+ persistLiveSession();
4006
+ }
4007
+ events.emit("session-renewed");
4008
+ } catch {
4009
+ }
3843
4010
  }
3844
4011
  if (persistSession) {
3845
4012
  const saved = loadSession(SESSION_KEY) ?? (LEGACY_SESSION_KEY !== SESSION_KEY ? loadSession(LEGACY_SESSION_KEY) : null);
@@ -3876,7 +4043,7 @@ async function createDecaneConnect(config) {
3876
4043
  }
3877
4044
  async function attachUnlockToken(userId, jwt, tokenKey) {
3878
4045
  try {
3879
- const stored = await storage.getShare(userId);
4046
+ const stored = await storage2.getShare(userId);
3880
4047
  if (!stored) return;
3881
4048
  const { token } = await api.issueUnlockToken(jwt);
3882
4049
  const unlockTokenIv = generateRandomBytes(12);
@@ -3885,13 +4052,13 @@ async function createDecaneConnect(config) {
3885
4052
  new TextEncoder().encode(token),
3886
4053
  unlockTokenIv
3887
4054
  );
3888
- await storage.putShare({ ...stored, wrappedUnlockToken, unlockTokenIv });
4055
+ await storage2.putShare({ ...stored, wrappedUnlockToken, unlockTokenIv });
3889
4056
  } catch (e) {
3890
4057
  console.warn("decane-connect-kit: could not store an unlock token for this device.", e);
3891
4058
  }
3892
4059
  }
3893
4060
  async function persistRotatedDeviceShare(userId, deviceShare) {
3894
- const stored = await storage.getShare(userId);
4061
+ const stored = await storage2.getShare(userId);
3895
4062
  if (!stored) {
3896
4063
  throw new CallSequenceError(
3897
4064
  "Share set was rotated but this device has no stored share to update. Recover the wallet on this device before rotating."
@@ -3900,21 +4067,21 @@ async function createDecaneConnect(config) {
3900
4067
  const wrappingKey = await getWrappingKey(stored);
3901
4068
  const iv = generateRandomBytes(12);
3902
4069
  const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
3903
- await storage.putShare({ ...stored, wrappedDeviceShare, iv });
4070
+ await storage2.putShare({ ...stored, wrappedDeviceShare, iv });
3904
4071
  }
3905
4072
  function hasPasswordUnlock(stored) {
3906
4073
  return !!stored && stored.unlockMethod === "password" && !!stored.passwordSalt && !!stored.wrappedUnlockToken && !!stored.unlockTokenIv;
3907
4074
  }
3908
4075
  async function passwordDeviceCandidate() {
3909
4076
  if (persisted?.userId) {
3910
- const stored = await storage.getShare(persisted.userId);
4077
+ const stored = await storage2.getShare(persisted.userId);
3911
4078
  return hasPasswordUnlock(stored) ? stored : null;
3912
4079
  }
3913
4080
  if (lastUserId) {
3914
- const stored = await storage.getShare(lastUserId);
4081
+ const stored = await storage2.getShare(lastUserId);
3915
4082
  return hasPasswordUnlock(stored) ? stored : null;
3916
4083
  }
3917
- const candidates = (await storage.listShares()).filter(hasPasswordUnlock);
4084
+ const candidates = (await storage2.listShares()).filter(hasPasswordUnlock);
3918
4085
  return candidates.length === 1 ? candidates[0] : null;
3919
4086
  }
3920
4087
  async function getWrappingKey(stored) {
@@ -3942,7 +4109,7 @@ async function createDecaneConnect(config) {
3942
4109
  let credentialId;
3943
4110
  let prfOutput;
3944
4111
  try {
3945
- const label = (await storage.getShare(userId))?.profile?.email;
4112
+ const label = (await storage2.getShare(userId))?.profile?.email;
3946
4113
  ({ credentialId, prfOutput } = await registerWebAuthnCredential({
3947
4114
  api,
3948
4115
  jwt,
@@ -3974,8 +4141,8 @@ async function createDecaneConnect(config) {
3974
4141
  const wrappingKey = await importAesKey(prfOutput);
3975
4142
  const iv = generateRandomBytes(12);
3976
4143
  const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
3977
- const existing = await storage.getShare(userId);
3978
- await storage.putShare({
4144
+ const existing = await storage2.getShare(userId);
4145
+ await storage2.putShare({
3979
4146
  ...existing,
3980
4147
  userId,
3981
4148
  wrappedDeviceShare,
@@ -4059,7 +4226,7 @@ async function createDecaneConnect(config) {
4059
4226
  const { wrappingKey, tokenKey, meta } = await passwordSetup();
4060
4227
  const iv = generateRandomBytes(12);
4061
4228
  const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
4062
- await storage.putShare({
4229
+ await storage2.putShare({
4063
4230
  userId,
4064
4231
  wrappedDeviceShare,
4065
4232
  iv,
@@ -4133,7 +4300,7 @@ async function createDecaneConnect(config) {
4133
4300
  shareSet.device,
4134
4301
  iv
4135
4302
  );
4136
- await storage.putShare({
4303
+ await storage2.putShare({
4137
4304
  userId,
4138
4305
  wrappedDeviceShare,
4139
4306
  iv,
@@ -4237,7 +4404,7 @@ async function createDecaneConnect(config) {
4237
4404
  async function returningUserUnlock(userId, jwt) {
4238
4405
  const rawAddresses = await api.getAddresses(jwt);
4239
4406
  const recordedAddresses = rawAddresses.evm && rawAddresses.solana ? { evm: rawAddresses.evm, solana: rawAddresses.solana, tron: rawAddresses.tron ?? evmToTronAddress2(rawAddresses.evm) } : null;
4240
- const stored = await storage.getShare(userId);
4407
+ const stored = await storage2.getShare(userId);
4241
4408
  let deviceShare;
4242
4409
  let joinedViaPasskeySync = false;
4243
4410
  if (!stored) {
@@ -4276,7 +4443,7 @@ async function createDecaneConnect(config) {
4276
4443
  rpId,
4277
4444
  teeUrl,
4278
4445
  api,
4279
- storage,
4446
+ storage: storage2,
4280
4447
  onProgress: config.onRecoveryProgress,
4281
4448
  promptForRecoveryFile: config.promptForRecoveryFile,
4282
4449
  onNewRecoveryShare: async (recoveryShare, shareSetVersion) => {
@@ -4290,7 +4457,7 @@ async function createDecaneConnect(config) {
4290
4457
  });
4291
4458
  deviceShare = recovered.deviceShare;
4292
4459
  joinedViaPasskeySync = recovered.flow === "passkey-sync";
4293
- if (!deferDeviceProtection && !await storage.getShare(userId) && isPasskeySupported()) {
4460
+ if (!deferDeviceProtection && !await storage2.getShare(userId) && isPasskeySupported()) {
4294
4461
  await registerPasskeyAndBackup(userId, jwt, deviceShare);
4295
4462
  }
4296
4463
  } else {
@@ -4311,7 +4478,7 @@ async function createDecaneConnect(config) {
4311
4478
  } catch (e) {
4312
4479
  if (e instanceof StaleDeviceShareError) {
4313
4480
  zeroize(deviceShare);
4314
- await storage.deleteShare(userId);
4481
+ await storage2.deleteShare(userId);
4315
4482
  if (stored) return returningUserUnlock(userId, jwt);
4316
4483
  } else {
4317
4484
  zeroize(deviceShare);
@@ -4335,7 +4502,7 @@ async function createDecaneConnect(config) {
4335
4502
  const result = await tee.rotateShares(jwt);
4336
4503
  try {
4337
4504
  await persistRotatedDeviceShare(userId, result.deviceShare);
4338
- const refreshed = await storage.getShare(userId);
4505
+ const refreshed = await storage2.getShare(userId);
4339
4506
  if (refreshed?.unlockMethod === "passkey" && refreshed.credentialId && refreshed.prfSalt) {
4340
4507
  await api.putRecoveryBackup(jwt, {
4341
4508
  wrappedDeviceShare: refreshed.wrappedDeviceShare,
@@ -4368,13 +4535,13 @@ async function createDecaneConnect(config) {
4368
4535
  }
4369
4536
  async function passkeyLoginCandidate() {
4370
4537
  if (!persisted?.userId || !isPasskeySupported()) return null;
4371
- return usablePasskeyShare(await storage.getShare(persisted.userId));
4538
+ return usablePasskeyShare(await storage2.getShare(persisted.userId));
4372
4539
  }
4373
4540
  async function passkeyDeviceCandidate() {
4374
4541
  if (!isPasskeySupported()) return null;
4375
4542
  if (persisted?.userId) return passkeyLoginCandidate();
4376
- if (lastUserId) return usablePasskeyShare(await storage.getShare(lastUserId));
4377
- const candidates = (await storage.listShares()).map(usablePasskeyShare).filter(Boolean);
4543
+ if (lastUserId) return usablePasskeyShare(await storage2.getShare(lastUserId));
4544
+ const candidates = (await storage2.listShares()).map(usablePasskeyShare).filter(Boolean);
4378
4545
  return candidates.length === 1 ? candidates[0] : null;
4379
4546
  }
4380
4547
  async function canOfferPasskey() {
@@ -4382,15 +4549,15 @@ async function createDecaneConnect(config) {
4382
4549
  if (await passkeyDeviceCandidate()) return true;
4383
4550
  const known = persisted?.userId ?? lastUserId;
4384
4551
  if (!known) return false;
4385
- return await storage.getShare(known) == null;
4552
+ return await storage2.getShare(known) == null;
4386
4553
  }
4387
4554
  async function rememberProfile(userId, profile) {
4388
4555
  const incoming = Object.fromEntries(Object.entries(profile).filter(([, v]) => v));
4389
4556
  if (!Object.keys(incoming).length) return;
4390
4557
  try {
4391
- const stored = await storage.getShare(userId);
4558
+ const stored = await storage2.getShare(userId);
4392
4559
  if (!stored) return;
4393
- await storage.putShare({ ...stored, profile: { ...stored.profile, ...incoming } });
4560
+ await storage2.putShare({ ...stored, profile: { ...stored.profile, ...incoming } });
4394
4561
  } catch {
4395
4562
  }
4396
4563
  }
@@ -4414,7 +4581,7 @@ async function createDecaneConnect(config) {
4414
4581
  savePersisted(PERSIST_KEY, persisted);
4415
4582
  events.emit("connected");
4416
4583
  persistLiveSession();
4417
- const stored = await storage.getShare(userId);
4584
+ const stored = await storage2.getShare(userId);
4418
4585
  return {
4419
4586
  addresses,
4420
4587
  isNewUser: false,
@@ -4587,7 +4754,7 @@ async function createDecaneConnect(config) {
4587
4754
  events.emit("connected");
4588
4755
  persistLiveSession();
4589
4756
  scheduleAccessTokenExpiry(token, claims.expiresAt);
4590
- const stored = await storage.getShare(claims.userId);
4757
+ const stored = await storage2.getShare(claims.userId);
4591
4758
  return {
4592
4759
  addresses,
4593
4760
  isNewUser: false,
@@ -4617,7 +4784,7 @@ async function createDecaneConnect(config) {
4617
4784
  const candidate = await passkeyDeviceCandidate();
4618
4785
  if (candidate) return passkeyLogin(candidate);
4619
4786
  const known = persisted?.userId ?? lastUserId;
4620
- if (isPasskeySupported() && known && await storage.getShare(known) == null) {
4787
+ if (isPasskeySupported() && known && await storage2.getShare(known) == null) {
4621
4788
  return passkeyLoginWithoutShare(known);
4622
4789
  }
4623
4790
  throw new NewDeviceError();
@@ -4636,7 +4803,7 @@ async function createDecaneConnect(config) {
4636
4803
  async setUnlockPassword(password) {
4637
4804
  if (!auth) throw new ReconnectRequiredError();
4638
4805
  validateUnlockPassword(password);
4639
- const stored = await storage.getShare(auth.userId);
4806
+ const stored = await storage2.getShare(auth.userId);
4640
4807
  if (!stored) throw new NewDeviceError();
4641
4808
  const deviceShare = await aesGcmDecrypt(
4642
4809
  await getWrappingKey(stored),
@@ -4655,7 +4822,7 @@ async function createDecaneConnect(config) {
4655
4822
  );
4656
4823
  const iv = generateRandomBytes(12);
4657
4824
  const wrappedDeviceShare = await aesGcmEncrypt(shareKey, deviceShare, iv);
4658
- await storage.putShare({
4825
+ await storage2.putShare({
4659
4826
  ...stored,
4660
4827
  unlockMethod: "password",
4661
4828
  wrappedDeviceShare,
@@ -4735,9 +4902,9 @@ async function createDecaneConnect(config) {
4735
4902
  async removeUnlockPassword() {
4736
4903
  if (!auth) throw new ReconnectRequiredError();
4737
4904
  await api.revokeUnlockTokens(auth.jwt);
4738
- const stored = await storage.getShare(auth.userId);
4905
+ const stored = await storage2.getShare(auth.userId);
4739
4906
  if (!stored || stored.unlockMethod !== "password") return;
4740
- await storage.putShare({
4907
+ await storage2.putShare({
4741
4908
  ...stored,
4742
4909
  wrappedUnlockToken: void 0,
4743
4910
  unlockTokenIv: void 0
@@ -4745,13 +4912,13 @@ async function createDecaneConnect(config) {
4745
4912
  },
4746
4913
  async hasPasskey() {
4747
4914
  if (!auth) return false;
4748
- const stored = await storage.getShare(auth.userId);
4915
+ const stored = await storage2.getShare(auth.userId);
4749
4916
  return stored?.unlockMethod === "passkey";
4750
4917
  },
4751
4918
  async addPasskey() {
4752
4919
  if (!auth) throw new ReconnectRequiredError();
4753
4920
  if (!isPasskeySupported()) throw new WebAuthnNotSupportedError();
4754
- const stored = await storage.getShare(auth.userId);
4921
+ const stored = await storage2.getShare(auth.userId);
4755
4922
  if (stored?.unlockMethod === "passkey") return;
4756
4923
  if (!stored) {
4757
4924
  if (!serverAssistedRecovery) throw new NewDeviceError();
@@ -4784,11 +4951,11 @@ async function createDecaneConnect(config) {
4784
4951
  },
4785
4952
  async deviceProtected() {
4786
4953
  if (!auth) return false;
4787
- return await storage.getShare(auth.userId) !== null;
4954
+ return await storage2.getShare(auth.userId) !== null;
4788
4955
  },
4789
4956
  async protectDevice() {
4790
4957
  if (!auth) throw new ReconnectRequiredError();
4791
- if (await storage.getShare(auth.userId)) return true;
4958
+ if (await storage2.getShare(auth.userId)) return true;
4792
4959
  if (!serverAssistedRecovery) throw new NewDeviceError();
4793
4960
  const tee = new TeeClient(teeUrl);
4794
4961
  const provisioned = await tee.provisionDeviceShare(auth.jwt);
@@ -4798,7 +4965,7 @@ async function createDecaneConnect(config) {
4798
4965
  } finally {
4799
4966
  zeroize(provisioned);
4800
4967
  }
4801
- return await storage.getShare(auth.userId) !== null;
4968
+ return await storage2.getShare(auth.userId) !== null;
4802
4969
  },
4803
4970
  async unlock() {
4804
4971
  if (!auth) {