@yieldfabric/wallet 0.2.3 → 0.2.5

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
@@ -1521,6 +1521,26 @@ var init_tokenStore = __esm({
1521
1521
  }
1522
1522
  });
1523
1523
 
1524
+ // src/core/crossTabRefresh.ts
1525
+ function withRefreshLock(renew, locks = globalThis.navigator?.locks) {
1526
+ if (!locks?.request) return renew();
1527
+ return locks.request(REFRESH_LOCK, renew);
1528
+ }
1529
+ function renewedElsewhere(spent, stored, chainId, now = Date.now()) {
1530
+ if (!stored.access || !stored.refresh || stored.refresh === spent) return false;
1531
+ const claims = decodeJwtPayload(stored.access);
1532
+ const exp = claims?.exp;
1533
+ return typeof exp === "number" && exp * 1e3 - now > LIVE_FOR_MS && claims?.default_chain_id === chainId;
1534
+ }
1535
+ var REFRESH_LOCK, LIVE_FOR_MS;
1536
+ var init_crossTabRefresh = __esm({
1537
+ "src/core/crossTabRefresh.ts"() {
1538
+ init_jwtPayload();
1539
+ REFRESH_LOCK = "yieldfabric-auth-refresh";
1540
+ LIVE_FOR_MS = 3e4;
1541
+ }
1542
+ });
1543
+
1524
1544
  // src/core/onboardingPlanSelectionStorage.ts
1525
1545
  function readOnboardingPlanSelectionHint() {
1526
1546
  if (typeof window === "undefined") return null;
@@ -1600,6 +1620,7 @@ var init_authService = __esm({
1600
1620
  init_tokenManager();
1601
1621
  init_jwtClaims();
1602
1622
  init_tokenStore();
1623
+ init_crossTabRefresh();
1603
1624
  init_onboardingPlanSelectionStorage();
1604
1625
  isRefreshing = false;
1605
1626
  refreshPromise = null;
@@ -1786,19 +1807,23 @@ var init_authService = __esm({
1786
1807
  error.statusCode = 401;
1787
1808
  throw error;
1788
1809
  }
1810
+ const spent = this.refreshToken;
1789
1811
  isRefreshing = true;
1790
1812
  refreshOperation = { kind: "regular", chainId };
1791
- refreshPromise = this.performRefresh(chainId);
1813
+ refreshPromise = withRefreshLock(async () => this.adoptRenewedSession(chainId, spent) ?? this.performRefresh(chainId));
1792
1814
  const activePromise = refreshPromise;
1793
1815
  try {
1794
1816
  const result = await refreshPromise;
1795
1817
  return result;
1796
1818
  } catch (error) {
1797
1819
  switch (classifyAuthFailure(error)) {
1798
- case "auth":
1820
+ case "auth": {
1821
+ const adopted = this.adoptRenewedSession(chainId, spent);
1822
+ if (adopted) return adopted;
1799
1823
  console.warn("Authentication error during token refresh, clearing tokens");
1800
1824
  this.clearTokensFromStorage();
1801
1825
  break;
1826
+ }
1802
1827
  case "network":
1803
1828
  console.warn("Network error during token refresh, preserving tokens for retry");
1804
1829
  break;
@@ -1814,6 +1839,20 @@ var init_authService = __esm({
1814
1839
  }
1815
1840
  }
1816
1841
  }
1842
+ /** The session another window renewed since this one read the refresh
1843
+ * token `spent`, taken as this window's own; `null` when there is none. */
1844
+ adoptRenewedSession(chainId, spent) {
1845
+ const stored = { access: exports.defaultTokenStore.getAccessToken(), refresh: exports.defaultTokenStore.getRefreshToken() };
1846
+ if (!stored.access || !stored.refresh || !renewedElsewhere(spent, stored, chainId)) return null;
1847
+ this.saveTokensToStorage(stored.access, stored.refresh);
1848
+ const exp = decodeJwtPayload(stored.access)?.exp;
1849
+ return {
1850
+ access_token: stored.access,
1851
+ refresh_token: stored.refresh,
1852
+ token_type: "Bearer",
1853
+ expires_in: Math.max(0, Math.floor(exp - Date.now() / 1e3))
1854
+ };
1855
+ }
1817
1856
  /**
1818
1857
  * Perform the actual token refresh.
1819
1858
  *
package/dist/index.d.cts CHANGED
@@ -741,6 +741,9 @@ declare class AuthService {
741
741
  * Uses global lock to prevent concurrent refresh attempts
742
742
  */
743
743
  refreshAccessToken(targetChainId?: string): Promise<RefreshTokenResponse>;
744
+ /** The session another window renewed since this one read the refresh
745
+ * token `spent`, taken as this window's own; `null` when there is none. */
746
+ private adoptRenewedSession;
744
747
  /**
745
748
  * Perform the actual token refresh.
746
749
  *
package/dist/index.d.ts CHANGED
@@ -741,6 +741,9 @@ declare class AuthService {
741
741
  * Uses global lock to prevent concurrent refresh attempts
742
742
  */
743
743
  refreshAccessToken(targetChainId?: string): Promise<RefreshTokenResponse>;
744
+ /** The session another window renewed since this one read the refresh
745
+ * token `spent`, taken as this window's own; `null` when there is none. */
746
+ private adoptRenewedSession;
744
747
  /**
745
748
  * Perform the actual token refresh.
746
749
  *
package/dist/index.js CHANGED
@@ -1513,6 +1513,26 @@ var init_tokenStore = __esm({
1513
1513
  }
1514
1514
  });
1515
1515
 
1516
+ // src/core/crossTabRefresh.ts
1517
+ function withRefreshLock(renew, locks = globalThis.navigator?.locks) {
1518
+ if (!locks?.request) return renew();
1519
+ return locks.request(REFRESH_LOCK, renew);
1520
+ }
1521
+ function renewedElsewhere(spent, stored, chainId, now = Date.now()) {
1522
+ if (!stored.access || !stored.refresh || stored.refresh === spent) return false;
1523
+ const claims = decodeJwtPayload(stored.access);
1524
+ const exp = claims?.exp;
1525
+ return typeof exp === "number" && exp * 1e3 - now > LIVE_FOR_MS && claims?.default_chain_id === chainId;
1526
+ }
1527
+ var REFRESH_LOCK, LIVE_FOR_MS;
1528
+ var init_crossTabRefresh = __esm({
1529
+ "src/core/crossTabRefresh.ts"() {
1530
+ init_jwtPayload();
1531
+ REFRESH_LOCK = "yieldfabric-auth-refresh";
1532
+ LIVE_FOR_MS = 3e4;
1533
+ }
1534
+ });
1535
+
1516
1536
  // src/core/onboardingPlanSelectionStorage.ts
1517
1537
  function readOnboardingPlanSelectionHint() {
1518
1538
  if (typeof window === "undefined") return null;
@@ -1592,6 +1612,7 @@ var init_authService = __esm({
1592
1612
  init_tokenManager();
1593
1613
  init_jwtClaims();
1594
1614
  init_tokenStore();
1615
+ init_crossTabRefresh();
1595
1616
  init_onboardingPlanSelectionStorage();
1596
1617
  isRefreshing = false;
1597
1618
  refreshPromise = null;
@@ -1778,19 +1799,23 @@ var init_authService = __esm({
1778
1799
  error.statusCode = 401;
1779
1800
  throw error;
1780
1801
  }
1802
+ const spent = this.refreshToken;
1781
1803
  isRefreshing = true;
1782
1804
  refreshOperation = { kind: "regular", chainId };
1783
- refreshPromise = this.performRefresh(chainId);
1805
+ refreshPromise = withRefreshLock(async () => this.adoptRenewedSession(chainId, spent) ?? this.performRefresh(chainId));
1784
1806
  const activePromise = refreshPromise;
1785
1807
  try {
1786
1808
  const result = await refreshPromise;
1787
1809
  return result;
1788
1810
  } catch (error) {
1789
1811
  switch (classifyAuthFailure(error)) {
1790
- case "auth":
1812
+ case "auth": {
1813
+ const adopted = this.adoptRenewedSession(chainId, spent);
1814
+ if (adopted) return adopted;
1791
1815
  console.warn("Authentication error during token refresh, clearing tokens");
1792
1816
  this.clearTokensFromStorage();
1793
1817
  break;
1818
+ }
1794
1819
  case "network":
1795
1820
  console.warn("Network error during token refresh, preserving tokens for retry");
1796
1821
  break;
@@ -1806,6 +1831,20 @@ var init_authService = __esm({
1806
1831
  }
1807
1832
  }
1808
1833
  }
1834
+ /** The session another window renewed since this one read the refresh
1835
+ * token `spent`, taken as this window's own; `null` when there is none. */
1836
+ adoptRenewedSession(chainId, spent) {
1837
+ const stored = { access: defaultTokenStore.getAccessToken(), refresh: defaultTokenStore.getRefreshToken() };
1838
+ if (!stored.access || !stored.refresh || !renewedElsewhere(spent, stored, chainId)) return null;
1839
+ this.saveTokensToStorage(stored.access, stored.refresh);
1840
+ const exp = decodeJwtPayload(stored.access)?.exp;
1841
+ return {
1842
+ access_token: stored.access,
1843
+ refresh_token: stored.refresh,
1844
+ token_type: "Bearer",
1845
+ expires_in: Math.max(0, Math.floor(exp - Date.now() / 1e3))
1846
+ };
1847
+ }
1809
1848
  /**
1810
1849
  * Perform the actual token refresh.
1811
1850
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yieldfabric/wallet",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "Reusable YieldFabric wallet — login, signing, profile, and key management",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "author": "YieldFabric Technologies Ltd",
@@ -55,7 +55,7 @@
55
55
  "build": "tsup",
56
56
  "dev": "tsup --watch",
57
57
  "clean": "rm -rf dist",
58
- "test:owned-group": "tsup --config tests/tsup.config.ts && node --test tests/ownedGroupSession.test.mjs tests/metaTransactionDigest.test.mjs tests/ownedGroupAuthorization.test.mjs tests/groupOwnershipRelationships.test.mjs tests/ownedGroupDelegationService.test.mjs tests/ownedGroupKeySelection.test.mjs tests/websocketAuthentication.test.mjs tests/withDeadline.test.mjs tests/onboardingCache.test.mjs tests/authFailure.test.mjs tests/jsonEventStream.test.mjs",
58
+ "test:owned-group": "tsup --config tests/tsup.config.ts && node --test tests/ownedGroupSession.test.mjs tests/metaTransactionDigest.test.mjs tests/ownedGroupAuthorization.test.mjs tests/groupOwnershipRelationships.test.mjs tests/ownedGroupDelegationService.test.mjs tests/ownedGroupKeySelection.test.mjs tests/websocketAuthentication.test.mjs tests/withDeadline.test.mjs tests/onboardingCache.test.mjs tests/authFailure.test.mjs tests/jsonEventStream.test.mjs tests/crossTabRefresh.test.mjs tests/twoWindowsRefresh.test.mjs",
59
59
  "typecheck": "tsc --noEmit",
60
60
  "prepublishOnly": "npm run clean && npm run build"
61
61
  },