@basictech/react 0.8.0-beta.3 → 0.8.0-beta.4

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.mjs CHANGED
@@ -71,6 +71,7 @@ var init_syncProtocol = __esm({
71
71
  var requestId = 0;
72
72
  var acceptCallbacks = {};
73
73
  var refreshTimer = null;
74
+ var pendingTokenUpdate = null;
74
75
  log("Connecting to", url);
75
76
  var ws = new WebSocket(url);
76
77
  function sendChanges(changes2, baseRevision2, partial2, onChangesAccepted2) {
@@ -93,6 +94,12 @@ var init_syncProtocol = __esm({
93
94
  refreshTimer = null;
94
95
  }
95
96
  }
97
+ function sendTokenUpdate(token) {
98
+ if (ws.readyState !== WebSocket.OPEN) return false;
99
+ pendingTokenUpdate = token;
100
+ ws.send(JSON.stringify({ type: "tokenUpdate", authToken: token }));
101
+ return true;
102
+ }
96
103
  function resolveGetToken() {
97
104
  var fn = getTokenGetter(url);
98
105
  if (!fn) throw new Error("No token getter registered for " + url);
@@ -108,10 +115,8 @@ var init_syncProtocol = __esm({
108
115
  refreshTimer = setTimeout(async function() {
109
116
  try {
110
117
  var newToken = await resolveGetToken()({ forceRefresh: true });
111
- if (ws.readyState === WebSocket.OPEN) {
118
+ if (sendTokenUpdate(newToken)) {
112
119
  log("Sending tokenUpdate on existing WebSocket");
113
- ws.send(JSON.stringify({ type: "tokenUpdate", authToken: newToken }));
114
- scheduleTokenRefresh(newToken);
115
120
  }
116
121
  } catch (err) {
117
122
  log("Proactive token refresh failed (non-fatal):", err);
@@ -141,10 +146,7 @@ var init_syncProtocol = __esm({
141
146
  if (document.visibilityState === "visible" && ws.readyState === WebSocket.OPEN) {
142
147
  log("Page became visible - refreshing token for WebSocket");
143
148
  resolveGetToken()({ forceRefresh: true }).then(function(newToken) {
144
- if (ws.readyState === WebSocket.OPEN) {
145
- ws.send(JSON.stringify({ type: "tokenUpdate", authToken: newToken }));
146
- scheduleTokenRefresh(newToken);
147
- }
149
+ sendTokenUpdate(newToken);
148
150
  }).catch(function(err) {
149
151
  log("Token refresh on visibility resume failed:", err);
150
152
  });
@@ -210,6 +212,19 @@ var init_syncProtocol = __esm({
210
212
  });
211
213
  isFirstRound = false;
212
214
  }
215
+ } else if (requestFromServer.type == "tokenUpdateAck") {
216
+ if (requestFromServer.ok) {
217
+ scheduleTokenRefresh(requestFromServer.authToken || pendingTokenUpdate);
218
+ pendingTokenUpdate = null;
219
+ } else {
220
+ log("tokenUpdate rejected by server:", requestFromServer.code || requestFromServer.message);
221
+ pendingTokenUpdate = null;
222
+ ws.close(4001, requestFromServer.code || "token_update_failed");
223
+ onError(
224
+ requestFromServer.message || "Authentication refresh failed",
225
+ RECONNECT_DELAY
226
+ );
227
+ }
213
228
  } else if (requestFromServer.type == "ack") {
214
229
  var requestId2 = requestFromServer.requestId;
215
230
  var acceptCallback = acceptCallbacks[requestId2.toString()];
@@ -244,7 +259,7 @@ var init_syncProtocol = __esm({
244
259
  var version;
245
260
  var init_package = __esm({
246
261
  "package.json"() {
247
- version = "0.8.0-beta.3";
262
+ version = "0.8.0-beta.4";
248
263
  }
249
264
  });
250
265
 
@@ -387,6 +402,8 @@ var init_context = __esm({
387
402
  BasicContext = createContext({
388
403
  isReady: false,
389
404
  isSignedIn: false,
405
+ authStatus: "bootstrapping",
406
+ authErrorCode: null,
390
407
  user: null,
391
408
  did: null,
392
409
  scope: null,
@@ -1042,7 +1059,14 @@ var init_BasicDevToolbar = __esm({
1042
1059
  });
1043
1060
 
1044
1061
  // src/AuthContext.tsx
1045
- import { useCallback as useCallback2, useEffect, useRef, useState as useState2, Suspense, lazy } from "react";
1062
+ import {
1063
+ useCallback as useCallback2,
1064
+ useEffect,
1065
+ useRef,
1066
+ useState as useState2,
1067
+ Suspense,
1068
+ lazy
1069
+ } from "react";
1046
1070
 
1047
1071
  // src/sync/index.ts
1048
1072
  init_config();
@@ -1636,6 +1660,21 @@ async function resolveHandle(handle) {
1636
1660
  // src/core/auth/AuthManager.ts
1637
1661
  init_network();
1638
1662
  init_config();
1663
+ var DEFINITIVE_TOKEN_ERRORS = /* @__PURE__ */ new Set([
1664
+ "invalid_grant",
1665
+ "invalid_client",
1666
+ "unauthorized_client"
1667
+ ]);
1668
+ var USER_RECOVERY_RETRY_COOLDOWN_MS = 3e4;
1669
+ var SESSION_RECONCILE_THROTTLE_MS = 5e3;
1670
+ var DefinitiveAuthError = class extends Error {
1671
+ code;
1672
+ constructor(code) {
1673
+ super(`Definitive auth failure: ${code}`);
1674
+ this.name = "DefinitiveAuthError";
1675
+ this.code = code;
1676
+ }
1677
+ };
1639
1678
  function generateCodeVerifier() {
1640
1679
  const array = new Uint8Array(32);
1641
1680
  crypto.getRandomValues(array);
@@ -1643,7 +1682,9 @@ function generateCodeVerifier() {
1643
1682
  }
1644
1683
  async function generateCodeChallenge(verifier) {
1645
1684
  if (typeof crypto === "undefined" || !crypto.subtle) {
1646
- log("crypto.subtle unavailable (non-secure context?) -- falling back to plain PKCE challenge");
1685
+ log(
1686
+ "crypto.subtle unavailable (non-secure context?) -- falling back to plain PKCE challenge"
1687
+ );
1647
1688
  return { challenge: verifier, method: "plain" };
1648
1689
  }
1649
1690
  const encoder = new TextEncoder();
@@ -1664,6 +1705,8 @@ var AuthManager = class {
1664
1705
  user = null;
1665
1706
  isSignedIn = false;
1666
1707
  isAuthReady = false;
1708
+ authStatus = "bootstrapping";
1709
+ authErrorCode = null;
1667
1710
  did = null;
1668
1711
  /** Space-separated scopes granted in the current access token */
1669
1712
  tokenScope = null;
@@ -1680,6 +1723,9 @@ var AuthManager = class {
1680
1723
  pendingRefresh = false;
1681
1724
  isOnline = typeof navigator !== "undefined" ? navigator.onLine : true;
1682
1725
  channel = null;
1726
+ nextUserRecoveryAt = 0;
1727
+ sessionCheckPromise = null;
1728
+ lastSessionCheckAt = 0;
1683
1729
  constructor(config, storage, notify) {
1684
1730
  this.config = config;
1685
1731
  this.storage = storage;
@@ -1694,31 +1740,26 @@ var AuthManager = class {
1694
1740
  this.channel.onmessage = (event) => {
1695
1741
  if (event.data?.type === "token_refreshed") {
1696
1742
  log("Received token refresh from another tab");
1697
- if (event.data.accessToken && this.token) {
1698
- this.token = { ...this.token, access_token: event.data.accessToken };
1699
- }
1700
- if (event.data.did) this.did = event.data.did;
1701
- if (event.data.tokenScope) this.tokenScope = event.data.tokenScope;
1702
- this.notify();
1743
+ void this.handleExternalTokenRefresh(event.data);
1703
1744
  }
1704
1745
  if (event.data?.type === "signed_in") {
1705
- log("Received sign-in from another tab, reloading");
1706
- if (typeof window !== "undefined") {
1707
- window.location.reload();
1708
- }
1746
+ log("Received sign-in from another tab, restoring session");
1747
+ void this.restoreStoredSession("cross-tab sign-in");
1709
1748
  }
1710
1749
  if (event.data?.type === "signed_out") {
1711
- log("Received sign-out from another tab, reloading");
1712
- this.user = null;
1713
- this.isSignedIn = false;
1714
- this.token = null;
1715
- this.did = null;
1716
- this.tokenScope = null;
1750
+ log("Received sign-out from another tab");
1751
+ this.resetAuthState("signed_out");
1717
1752
  this.notify();
1718
1753
  if (typeof window !== "undefined") {
1719
1754
  window.location.reload();
1720
1755
  }
1721
1756
  }
1757
+ if (event.data?.type === "session_invalidated") {
1758
+ log("Received session invalidation from another tab");
1759
+ void this.markReauthRequired(event.data.code || "invalid_grant", {
1760
+ broadcast: false
1761
+ });
1762
+ }
1722
1763
  };
1723
1764
  } catch {
1724
1765
  log("BroadcastChannel not available for cross-tab sync");
@@ -1738,6 +1779,9 @@ var AuthManager = class {
1738
1779
  broadcastSignOut() {
1739
1780
  this.channel?.postMessage({ type: "signed_out" });
1740
1781
  }
1782
+ broadcastSessionInvalidated(code) {
1783
+ this.channel?.postMessage({ type: "session_invalidated", code });
1784
+ }
1741
1785
  // ------------------------------------------------------------------
1742
1786
  // Public API
1743
1787
  // ------------------------------------------------------------------
@@ -1746,7 +1790,11 @@ var AuthManager = class {
1746
1790
  * from refresh token, or load cached user for offline mode.
1747
1791
  */
1748
1792
  async initialize() {
1749
- await this.storage.set(STORAGE_KEYS.DEBUG, this.config.debug ? "true" : "false");
1793
+ this.updateAuthStatus("bootstrapping");
1794
+ await this.storage.set(
1795
+ STORAGE_KEYS.DEBUG,
1796
+ this.config.debug ? "true" : "false"
1797
+ );
1750
1798
  const storedServerUrl = await this.storage.get(STORAGE_KEYS.SERVER_URL);
1751
1799
  if (storedServerUrl && storedServerUrl !== this.config.pdsUrl) {
1752
1800
  log("PDS URL changed, clearing stored tokens");
@@ -1758,7 +1806,7 @@ var AuthManager = class {
1758
1806
  if (params.has("code")) {
1759
1807
  const code = params.get("code");
1760
1808
  if (!code) {
1761
- this.isAuthReady = true;
1809
+ this.updateAuthStatus("signed_out");
1762
1810
  this.notify();
1763
1811
  return;
1764
1812
  }
@@ -1766,7 +1814,7 @@ var AuthManager = class {
1766
1814
  const urlState = params.get("state");
1767
1815
  if (!state || state !== urlState) {
1768
1816
  log("error: auth state does not match");
1769
- this.isAuthReady = true;
1817
+ this.updateAuthStatus("signed_out");
1770
1818
  this.notify();
1771
1819
  await this.storage.remove(STORAGE_KEYS.AUTH_STATE);
1772
1820
  cleanOAuthParamsFromUrl();
@@ -1777,34 +1825,18 @@ var AuthManager = class {
1777
1825
  this.freshSignIn = true;
1778
1826
  this.exchangeToken(code, false).catch((error) => {
1779
1827
  log("Error fetching token:", error);
1828
+ this.freshSignIn = false;
1829
+ void this.restoreCachedUser({
1830
+ hasRecoverableSession: !this.isDefinitiveAuthFailure(error)
1831
+ });
1780
1832
  });
1781
1833
  } else {
1782
- const refreshToken = await this.storage.get(STORAGE_KEYS.REFRESH_TOKEN);
1783
- if (refreshToken) {
1784
- log("Found refresh token in storage, attempting to refresh access token");
1785
- this.exchangeToken(refreshToken, true).catch(async (error) => {
1786
- log("Error fetching refresh token:", error);
1787
- if (this.isNetworkError(error)) {
1788
- await this.restoreCachedUser();
1789
- }
1790
- });
1791
- } else {
1792
- const cachedUserInfo = await this.storage.get(STORAGE_KEYS.USER_INFO);
1793
- if (cachedUserInfo) {
1794
- try {
1795
- this.user = JSON.parse(cachedUserInfo);
1796
- this.isSignedIn = true;
1797
- log("Loaded cached user info for offline mode");
1798
- } catch (error) {
1799
- log("Error parsing cached user info:", error);
1800
- }
1801
- }
1802
- this.isAuthReady = true;
1803
- this.notify();
1804
- }
1834
+ await this.restoreStoredSession("initialize");
1805
1835
  }
1806
1836
  } catch (e) {
1807
1837
  log("error getting token", e);
1838
+ this.updateAuthStatus("signed_out");
1839
+ this.notify();
1808
1840
  }
1809
1841
  }
1810
1842
  /**
@@ -1814,7 +1846,7 @@ var AuthManager = class {
1814
1846
  async getToken(options) {
1815
1847
  log("getting token...");
1816
1848
  if (!this.token) {
1817
- const refreshToken = await this.storage.get(STORAGE_KEYS.REFRESH_TOKEN);
1849
+ const refreshToken = await this.getRefreshToken();
1818
1850
  if (refreshToken) {
1819
1851
  log("No token in memory, attempting to refresh from storage");
1820
1852
  if (this.refreshPromise) {
@@ -1837,7 +1869,12 @@ var AuthManager = class {
1837
1869
  } catch (error) {
1838
1870
  log("Failed to refresh token from storage:", error);
1839
1871
  if (this.isNetworkError(error)) {
1840
- throw new Error("Network offline - authentication will be retried when online");
1872
+ throw new Error(
1873
+ "Network offline - authentication will be retried when online"
1874
+ );
1875
+ }
1876
+ if (!this.isDefinitiveAuthFailure(error)) {
1877
+ throw error;
1841
1878
  }
1842
1879
  throw new Error("Authentication expired. Please sign in again.");
1843
1880
  }
@@ -1850,12 +1887,15 @@ var AuthManager = class {
1850
1887
  const isExpired = decoded.exp && decoded.exp < Date.now() / 1e3 + expirationBuffer;
1851
1888
  const shouldRefresh = isExpired || options?.forceRefresh === true;
1852
1889
  if (shouldRefresh) {
1853
- log(options?.forceRefresh ? "force refreshing token..." : "token is expired - refreshing ...");
1890
+ log(
1891
+ options?.forceRefresh ? "force refreshing token..." : "token is expired - refreshing ..."
1892
+ );
1854
1893
  if (this.refreshPromise) {
1855
1894
  log("Token refresh already in progress, waiting...");
1856
1895
  try {
1857
1896
  const newToken = await this.refreshPromise;
1858
- if (!newToken?.access_token) throw new Error("Token refresh returned empty access token");
1897
+ if (!newToken?.access_token)
1898
+ throw new Error("Token refresh returned empty access token");
1859
1899
  return newToken.access_token;
1860
1900
  } catch (error) {
1861
1901
  log("In-flight refresh failed:", error);
@@ -1866,11 +1906,12 @@ var AuthManager = class {
1866
1906
  throw error;
1867
1907
  }
1868
1908
  }
1869
- const refreshToken = this.token.refresh_token || await this.storage.get(STORAGE_KEYS.REFRESH_TOKEN);
1909
+ const refreshToken = await this.getRefreshToken();
1870
1910
  if (refreshToken) {
1871
1911
  try {
1872
1912
  const newToken = await this.exchangeToken(refreshToken, true);
1873
- if (!newToken?.access_token) throw new Error("Token refresh returned empty access token");
1913
+ if (!newToken?.access_token)
1914
+ throw new Error("Token refresh returned empty access token");
1874
1915
  return newToken.access_token;
1875
1916
  } catch (error) {
1876
1917
  log("Failed to refresh expired token:", error);
@@ -1878,13 +1919,17 @@ var AuthManager = class {
1878
1919
  log("Network issue - using expired token until network is restored");
1879
1920
  return this.token.access_token;
1880
1921
  }
1922
+ if (!this.isDefinitiveAuthFailure(error)) {
1923
+ throw error;
1924
+ }
1881
1925
  throw new Error("Authentication expired. Please sign in again.");
1882
1926
  }
1883
1927
  } else {
1884
1928
  throw new Error("no refresh token available");
1885
1929
  }
1886
1930
  }
1887
- if (!this.token.access_token) throw new Error("Token exists but access_token is empty");
1931
+ if (!this.token.access_token)
1932
+ throw new Error("Token exists but access_token is empty");
1888
1933
  return this.token.access_token;
1889
1934
  }
1890
1935
  async getSignInUrl(redirectUri, endpoints) {
@@ -1893,8 +1938,13 @@ var AuthManager = class {
1893
1938
  throw new Error("Project ID is required to generate sign-in link");
1894
1939
  }
1895
1940
  const pdsEndpoints = endpoints || this.defaultPdsEndpoints();
1896
- await this.storage.set(STORAGE_KEYS.PDS_ENDPOINTS, JSON.stringify(pdsEndpoints));
1897
- const randomState = base64UrlEncode(crypto.getRandomValues(new Uint8Array(16)));
1941
+ await this.storage.set(
1942
+ STORAGE_KEYS.PDS_ENDPOINTS,
1943
+ JSON.stringify(pdsEndpoints)
1944
+ );
1945
+ const randomState = base64UrlEncode(
1946
+ crypto.getRandomValues(new Uint8Array(16))
1947
+ );
1898
1948
  await this.storage.set(STORAGE_KEYS.AUTH_STATE, randomState);
1899
1949
  const redirectUrl = redirectUri || window.location.href;
1900
1950
  if (!redirectUrl || !redirectUrl.startsWith("http://") && !redirectUrl.startsWith("https://")) {
@@ -1963,7 +2013,10 @@ var AuthManager = class {
1963
2013
  if (state) {
1964
2014
  const storedState = await this.storage.get(STORAGE_KEYS.AUTH_STATE);
1965
2015
  if (storedState && storedState !== state) {
1966
- log("State parameter mismatch:", { provided: state, stored: storedState });
2016
+ log("State parameter mismatch:", {
2017
+ provided: state,
2018
+ stored: storedState
2019
+ });
1967
2020
  return { success: false, error: "State parameter mismatch" };
1968
2021
  }
1969
2022
  }
@@ -1979,6 +2032,7 @@ var AuthManager = class {
1979
2032
  }
1980
2033
  } catch (error) {
1981
2034
  log("signInWithCode error:", error);
2035
+ this.freshSignIn = false;
1982
2036
  return {
1983
2037
  success: false,
1984
2038
  error: error.message || "Authentication failed"
@@ -1991,13 +2045,64 @@ var AuthManager = class {
1991
2045
  */
1992
2046
  async signOut() {
1993
2047
  log("signing out!");
1994
- this.resetAuthState();
2048
+ this.resetAuthState("signed_out");
1995
2049
  await this.storage.remove(STORAGE_KEYS.AUTH_STATE);
1996
2050
  await this.storage.remove(STORAGE_KEYS.LAST_CONNECT_REPORT);
1997
2051
  await this.clearStoredAuth();
1998
2052
  this.broadcastSignOut();
1999
2053
  this.notify();
2000
2054
  }
2055
+ async reconcileSession(reason = "manual", options) {
2056
+ if (this.authStatus === "signed_out" || this.authStatus === "reauth_required") {
2057
+ return;
2058
+ }
2059
+ if (!this.isOnline) {
2060
+ this.updateAuthStatus("recovering", this.authErrorCode);
2061
+ this.notify();
2062
+ return;
2063
+ }
2064
+ const throttleMs = options?.throttleMs ?? SESSION_RECONCILE_THROTTLE_MS;
2065
+ const forceRefresh = options?.forceRefresh === true;
2066
+ const now = Date.now();
2067
+ if (this.sessionCheckPromise) {
2068
+ return this.sessionCheckPromise;
2069
+ }
2070
+ if (!forceRefresh && now - this.lastSessionCheckAt < throttleMs) {
2071
+ return;
2072
+ }
2073
+ this.lastSessionCheckAt = now;
2074
+ let sessionCheck = null;
2075
+ sessionCheck = (async () => {
2076
+ try {
2077
+ const accessToken = await this.getToken(
2078
+ forceRefresh ? { forceRefresh: true } : void 0
2079
+ );
2080
+ const currentSession = await this.fetchCurrentSession(accessToken);
2081
+ if (currentSession?.active) {
2082
+ this.updateAuthStatus("authenticated");
2083
+ this.notify();
2084
+ if (!this.user) {
2085
+ await this.recoverMissingUserProfile(reason, accessToken);
2086
+ }
2087
+ }
2088
+ } catch (error) {
2089
+ log(`Session reconciliation failed on ${reason}:`, error);
2090
+ if (this.isDefinitiveAuthFailure(error)) {
2091
+ return;
2092
+ }
2093
+ if (this.isNetworkError(error)) {
2094
+ this.updateAuthStatus("recovering", this.authErrorCode);
2095
+ this.notify();
2096
+ }
2097
+ } finally {
2098
+ if (this.sessionCheckPromise === sessionCheck) {
2099
+ this.sessionCheckPromise = null;
2100
+ }
2101
+ }
2102
+ })();
2103
+ this.sessionCheckPromise = sessionCheck;
2104
+ return sessionCheck;
2105
+ }
2001
2106
  hasScope(scope) {
2002
2107
  if (!this.tokenScope) return false;
2003
2108
  return this.tokenScope.split(/[\s,]+/).filter(Boolean).includes(scope);
@@ -2023,16 +2128,26 @@ var AuthManager = class {
2023
2128
  const handleOnline = async () => {
2024
2129
  log("Network came back online");
2025
2130
  this.isOnline = true;
2026
- if (this.pendingRefresh && this.token) {
2131
+ if (this.pendingRefresh) {
2027
2132
  log("Retrying pending token refresh");
2028
2133
  this.pendingRefresh = false;
2029
- const refreshToken = this.token.refresh_token || await this.storage.get(STORAGE_KEYS.REFRESH_TOKEN);
2134
+ const refreshToken = await this.getRefreshToken();
2030
2135
  if (refreshToken) {
2031
2136
  this.exchangeToken(refreshToken, true).catch((error) => {
2032
2137
  log("Retry refresh failed:", error);
2033
2138
  });
2034
2139
  }
2035
2140
  }
2141
+ if (this.isSignedIn) {
2142
+ this.reconcileSession("online event", {
2143
+ forceRefresh: true,
2144
+ throttleMs: 0
2145
+ }).catch((error) => {
2146
+ log("Session reconciliation on online failed:", error);
2147
+ });
2148
+ } else if (this.user) {
2149
+ await this.restoreStoredSession("online restore");
2150
+ }
2036
2151
  };
2037
2152
  const handleOffline = () => {
2038
2153
  log("Network went offline");
@@ -2040,9 +2155,11 @@ var AuthManager = class {
2040
2155
  };
2041
2156
  const handleVisibilityChange = () => {
2042
2157
  if (document.visibilityState === "visible" && this.isSignedIn) {
2043
- log("App became visible - checking token freshness");
2044
- this.getToken().catch((err) => {
2045
- log("Token refresh on visibility resume failed:", err);
2158
+ log("App became visible - reconciling auth session");
2159
+ this.reconcileSession("visibility resume", {
2160
+ forceRefresh: true
2161
+ }).catch((err) => {
2162
+ log("Session reconciliation on visibility resume failed:", err);
2046
2163
  });
2047
2164
  }
2048
2165
  };
@@ -2095,12 +2212,18 @@ var AuthManager = class {
2095
2212
  if (elapsed < 24 * 60 * 60 * 1e3) return;
2096
2213
  }
2097
2214
  try {
2098
- await fetch(`${this.config.adminUrl}/project/${this.config.projectId}/user/connect`, {
2099
- method: "POST",
2100
- headers: { "Content-Type": "application/json" },
2101
- body: JSON.stringify({ token: accessToken })
2102
- });
2103
- await this.storage.set(STORAGE_KEYS.LAST_CONNECT_REPORT, Date.now().toString());
2215
+ await fetch(
2216
+ `${this.config.adminUrl}/project/${this.config.projectId}/user/connect`,
2217
+ {
2218
+ method: "POST",
2219
+ headers: { "Content-Type": "application/json" },
2220
+ body: JSON.stringify({ token: accessToken })
2221
+ }
2222
+ );
2223
+ await this.storage.set(
2224
+ STORAGE_KEYS.LAST_CONNECT_REPORT,
2225
+ Date.now().toString()
2226
+ );
2104
2227
  log("Reported connection to admin server");
2105
2228
  } catch (err) {
2106
2229
  log("Failed to report connection (non-blocking):", err);
@@ -2111,32 +2234,37 @@ var AuthManager = class {
2111
2234
  */
2112
2235
  async processNewToken() {
2113
2236
  if (!this.token) {
2114
- this.isAuthReady = true;
2237
+ this.updateAuthStatus("signed_out");
2115
2238
  this.notify();
2116
2239
  return;
2117
2240
  }
2118
2241
  try {
2119
2242
  const decoded = jwtDecode(this.token.access_token);
2120
- if (decoded.sub) this.did = decoded.sub;
2121
- if (decoded.scope) this.tokenScope = decoded.scope;
2243
+ this.applyTokenClaims(decoded);
2244
+ this.updateAuthStatus("authenticated");
2245
+ this.notify();
2246
+ this.broadcastSessionUpdate();
2122
2247
  await this.fetchUser(this.token.access_token);
2123
2248
  } catch (error) {
2124
2249
  log("Error processing token:", error);
2125
- this.isAuthReady = true;
2250
+ this.updateAuthStatus("recovering");
2126
2251
  this.notify();
2127
2252
  }
2128
2253
  }
2129
- async restoreCachedUser() {
2254
+ async restoreCachedUser(options) {
2130
2255
  const cached = await this.storage.get(STORAGE_KEYS.USER_INFO);
2131
- if (cached) {
2256
+ if (cached && options?.hasRecoverableSession) {
2132
2257
  try {
2133
2258
  this.user = JSON.parse(cached);
2134
- this.isSignedIn = true;
2135
- log("Restored cached user info for offline mode");
2259
+ log("Restored cached user info for recoverable session");
2136
2260
  } catch {
2137
2261
  }
2262
+ } else {
2263
+ this.user = null;
2138
2264
  }
2139
- this.isAuthReady = true;
2265
+ this.updateAuthStatus(
2266
+ options?.hasRecoverableSession ? "recovering" : "signed_out"
2267
+ );
2140
2268
  this.notify();
2141
2269
  }
2142
2270
  async fetchUser(accessToken) {
@@ -2145,7 +2273,7 @@ var AuthManager = class {
2145
2273
  const endpoints = await this.getActivePdsEndpoints();
2146
2274
  const response = await fetch(endpoints.userinfo_endpoint, {
2147
2275
  method: "GET",
2148
- headers: { "Authorization": `Bearer ${accessToken}` }
2276
+ headers: { Authorization: `Bearer ${accessToken}` }
2149
2277
  });
2150
2278
  if (!response.ok) {
2151
2279
  throw new Error(`Failed to fetch user info: ${response.status}`);
@@ -2156,28 +2284,22 @@ var AuthManager = class {
2156
2284
  throw new Error(`User info error: ${user.error}`);
2157
2285
  }
2158
2286
  if (this.token?.refresh_token) {
2159
- await this.storage.set(STORAGE_KEYS.REFRESH_TOKEN, this.token.refresh_token);
2287
+ await this.storage.set(
2288
+ STORAGE_KEYS.REFRESH_TOKEN,
2289
+ this.token.refresh_token
2290
+ );
2160
2291
  }
2161
2292
  await this.storage.set(STORAGE_KEYS.USER_INFO, JSON.stringify(user));
2162
2293
  log("Cached user info in storage");
2163
2294
  this.user = user;
2164
- this.isSignedIn = true;
2165
- this.isAuthReady = true;
2166
- if (this.freshSignIn) {
2167
- this.freshSignIn = false;
2168
- this.broadcastSignIn();
2169
- } else {
2170
- this.broadcastTokenRefresh();
2295
+ if (this.authStatus !== "reauth_required") {
2296
+ this.updateAuthStatus("authenticated");
2171
2297
  }
2298
+ this.nextUserRecoveryAt = 0;
2172
2299
  this.notify();
2173
2300
  } catch (error) {
2174
2301
  log("Failed to fetch user info:", error);
2175
- if (this.isNetworkError(error)) {
2176
- await this.restoreCachedUser();
2177
- } else {
2178
- this.isAuthReady = true;
2179
- this.notify();
2180
- }
2302
+ await this.handleUserFetchFailure();
2181
2303
  }
2182
2304
  }
2183
2305
  /**
@@ -2204,7 +2326,9 @@ var AuthManager = class {
2204
2326
  if (!this.isOnline) {
2205
2327
  log("Network is offline, marking refresh as pending");
2206
2328
  this.pendingRefresh = true;
2207
- throw new Error("Network offline - refresh will be retried when online");
2329
+ throw new Error(
2330
+ "Network offline - refresh will be retried when online"
2331
+ );
2208
2332
  }
2209
2333
  const endpoints = await this.getActivePdsEndpoints();
2210
2334
  let requestBody;
@@ -2214,26 +2338,36 @@ var AuthManager = class {
2214
2338
  refresh_token: codeOrRefreshToken
2215
2339
  };
2216
2340
  if (this.config.projectId) {
2217
- requestBody.client_id = normalizeClientId(this.config.projectId, this.adminHostname);
2341
+ requestBody.client_id = normalizeClientId(
2342
+ this.config.projectId,
2343
+ this.adminHostname
2344
+ );
2218
2345
  }
2219
2346
  } else {
2220
2347
  requestBody = {
2221
2348
  grant_type: "authorization_code",
2222
2349
  code: codeOrRefreshToken
2223
2350
  };
2224
- const storedRedirectUri = await this.storage.get(STORAGE_KEYS.REDIRECT_URI);
2351
+ const storedRedirectUri = await this.storage.get(
2352
+ STORAGE_KEYS.REDIRECT_URI
2353
+ );
2225
2354
  if (storedRedirectUri) {
2226
2355
  requestBody.redirect_uri = storedRedirectUri;
2227
2356
  log("Including redirect_uri in token exchange:", storedRedirectUri);
2228
2357
  } else {
2229
2358
  log("Warning: No redirect_uri found in storage for token exchange");
2230
2359
  }
2231
- const codeVerifier = await this.storage.get(STORAGE_KEYS.CODE_VERIFIER);
2360
+ const codeVerifier = await this.storage.get(
2361
+ STORAGE_KEYS.CODE_VERIFIER
2362
+ );
2232
2363
  if (codeVerifier) {
2233
2364
  requestBody.code_verifier = codeVerifier;
2234
2365
  }
2235
2366
  if (this.config.projectId) {
2236
- requestBody.client_id = normalizeClientId(this.config.projectId, this.adminHostname);
2367
+ requestBody.client_id = normalizeClientId(
2368
+ this.config.projectId,
2369
+ this.adminHostname
2370
+ );
2237
2371
  }
2238
2372
  }
2239
2373
  log("Token exchange request body:", {
@@ -2249,7 +2383,9 @@ var AuthManager = class {
2249
2383
  log("Network error fetching token:", error);
2250
2384
  if (!this.isOnline) {
2251
2385
  this.pendingRefresh = true;
2252
- throw new Error("Network offline - refresh will be retried when online");
2386
+ throw new Error(
2387
+ "Network offline - refresh will be retried when online"
2388
+ );
2253
2389
  }
2254
2390
  throw new Error("Network error during token refresh");
2255
2391
  });
@@ -2258,39 +2394,52 @@ var AuthManager = class {
2258
2394
  const decoded = jwtDecode(token.access_token);
2259
2395
  if (decoded.typ === "refresh") {
2260
2396
  log("Error: received refresh token as access token");
2261
- throw new Error("Invalid token: received refresh token instead of access token");
2397
+ throw new Error(
2398
+ "Invalid token: received refresh token instead of access token"
2399
+ );
2262
2400
  }
2263
2401
  } catch (decodeError) {
2264
2402
  if (decodeError.message.includes("Invalid token")) {
2265
2403
  throw decodeError;
2266
2404
  }
2267
- log("Warning: could not decode access token for type check:", decodeError);
2405
+ log(
2406
+ "Warning: could not decode access token for type check:",
2407
+ decodeError
2408
+ );
2268
2409
  }
2269
2410
  }
2270
2411
  if (token.error) {
2271
2412
  log("error fetching token", token.error);
2272
2413
  if (typeof token.error === "string" && (token.error.includes("network") || token.error.includes("timeout"))) {
2273
2414
  this.pendingRefresh = true;
2274
- throw new Error("Network issue - refresh will be retried when online");
2415
+ throw new Error(
2416
+ "Network issue - refresh will be retried when online"
2417
+ );
2275
2418
  }
2276
- const definitiveErrors = ["invalid_grant", "invalid_client", "unauthorized_client"];
2277
- if (typeof token.error === "string" && definitiveErrors.includes(token.error)) {
2278
- await this.clearStoredAuth();
2279
- this.resetAuthState();
2280
- this.notify();
2419
+ if (this.isDefinitiveTokenErrorCode(token.error)) {
2420
+ await this.markReauthRequired(token.error);
2421
+ throw new DefinitiveAuthError(token.error);
2281
2422
  }
2282
2423
  throw new Error(`Token refresh failed: ${token.error}`);
2283
2424
  } else {
2425
+ if (!token.access_token) {
2426
+ throw new Error("Token response missing access token");
2427
+ }
2284
2428
  this.token = token;
2285
2429
  this.pendingRefresh = false;
2286
2430
  if (token.refresh_token) {
2287
- await this.storage.set(STORAGE_KEYS.REFRESH_TOKEN, token.refresh_token);
2431
+ await this.storage.set(
2432
+ STORAGE_KEYS.REFRESH_TOKEN,
2433
+ token.refresh_token
2434
+ );
2288
2435
  log("Updated refresh token in storage");
2289
2436
  }
2290
2437
  if (!isRefreshToken) {
2291
2438
  await this.storage.remove(STORAGE_KEYS.REDIRECT_URI);
2292
2439
  await this.storage.remove(STORAGE_KEYS.CODE_VERIFIER);
2293
- log("Cleaned up redirect_uri and code_verifier from storage after successful exchange");
2440
+ log(
2441
+ "Cleaned up redirect_uri and code_verifier from storage after successful exchange"
2442
+ );
2294
2443
  }
2295
2444
  this.reportConnection(token.access_token).catch(() => {
2296
2445
  });
@@ -2299,12 +2448,12 @@ var AuthManager = class {
2299
2448
  return token;
2300
2449
  } catch (error) {
2301
2450
  log("Token refresh error:", error);
2302
- const msg = error instanceof Error ? error.message : "";
2303
- const alreadyHandled = msg.startsWith("Token refresh failed:");
2304
- if (!alreadyHandled && !this.isNetworkError(error)) {
2305
- await this.clearStoredAuth();
2306
- this.resetAuthState();
2307
- this.notify();
2451
+ if (this.isDefinitiveAuthFailure(error)) {
2452
+ log("Preserving cleared auth state after definitive token rejection");
2453
+ } else if (this.isNetworkError(error)) {
2454
+ log("Recoverable network auth failure - preserving session state");
2455
+ } else {
2456
+ log("Recoverable auth failure - preserving session state");
2308
2457
  }
2309
2458
  throw error;
2310
2459
  }
@@ -2328,13 +2477,15 @@ var AuthManager = class {
2328
2477
  }
2329
2478
  return tokenPromise;
2330
2479
  }
2331
- resetAuthState() {
2332
- this.user = null;
2333
- this.isSignedIn = false;
2480
+ resetAuthState(status = "signed_out") {
2481
+ this.user = status === "reauth_required" ? this.user : null;
2334
2482
  this.token = null;
2335
- this.did = null;
2483
+ if (status !== "reauth_required") {
2484
+ this.did = null;
2485
+ }
2336
2486
  this.tokenScope = null;
2337
- this.isAuthReady = true;
2487
+ this.nextUserRecoveryAt = 0;
2488
+ this.updateAuthStatus(status);
2338
2489
  }
2339
2490
  async clearStoredAuth() {
2340
2491
  await this.storage.remove(STORAGE_KEYS.REFRESH_TOKEN);
@@ -2351,6 +2502,215 @@ var AuthManager = class {
2351
2502
  }
2352
2503
  return false;
2353
2504
  }
2505
+ async getRefreshToken() {
2506
+ const storedRefreshToken = await this.storage.get(
2507
+ STORAGE_KEYS.REFRESH_TOKEN
2508
+ );
2509
+ if (storedRefreshToken) {
2510
+ log("Using refresh token from storage");
2511
+ if (this.token && this.token.refresh_token !== storedRefreshToken) {
2512
+ this.token = { ...this.token, refresh_token: storedRefreshToken };
2513
+ }
2514
+ return storedRefreshToken;
2515
+ }
2516
+ const memoryRefreshToken = this.token?.refresh_token ?? null;
2517
+ if (memoryRefreshToken) {
2518
+ log("Using refresh token from memory fallback");
2519
+ } else {
2520
+ log("No refresh token available in storage or memory");
2521
+ }
2522
+ return memoryRefreshToken;
2523
+ }
2524
+ async syncRefreshTokenFromStorage() {
2525
+ const storedRefreshToken = await this.storage.get(
2526
+ STORAGE_KEYS.REFRESH_TOKEN
2527
+ );
2528
+ if (storedRefreshToken && this.token && this.token.refresh_token !== storedRefreshToken) {
2529
+ this.token = { ...this.token, refresh_token: storedRefreshToken };
2530
+ log("Synced refresh token from shared storage into memory");
2531
+ }
2532
+ }
2533
+ applyTokenClaims(decoded) {
2534
+ this.did = decoded.sub || null;
2535
+ this.tokenScope = decoded.scope || null;
2536
+ }
2537
+ broadcastSessionUpdate() {
2538
+ if (this.freshSignIn) {
2539
+ this.freshSignIn = false;
2540
+ this.broadcastSignIn();
2541
+ } else {
2542
+ this.broadcastTokenRefresh();
2543
+ }
2544
+ }
2545
+ async handleUserFetchFailure() {
2546
+ if (this.isCompatibleUser(this.user)) {
2547
+ log("Preserving existing user after userinfo failure");
2548
+ } else if (this.user) {
2549
+ log("Discarding stale in-memory user after userinfo failure");
2550
+ this.user = null;
2551
+ }
2552
+ if (!this.user) {
2553
+ const cached = await this.storage.get(STORAGE_KEYS.USER_INFO);
2554
+ if (cached) {
2555
+ try {
2556
+ const parsed = JSON.parse(cached);
2557
+ if (this.isCompatibleUser(parsed)) {
2558
+ this.user = parsed;
2559
+ log("Recovered cached user after userinfo failure");
2560
+ } else {
2561
+ log("Cached user did not match the active session");
2562
+ }
2563
+ } catch (error) {
2564
+ log("Failed to parse cached user after userinfo failure:", error);
2565
+ }
2566
+ }
2567
+ }
2568
+ if (!this.user) {
2569
+ log("No compatible cached user available after userinfo failure");
2570
+ this.nextUserRecoveryAt = Date.now() + USER_RECOVERY_RETRY_COOLDOWN_MS;
2571
+ } else {
2572
+ this.nextUserRecoveryAt = 0;
2573
+ }
2574
+ if (this.authStatus === "bootstrapping") {
2575
+ this.updateAuthStatus(this.token ? "authenticated" : "recovering");
2576
+ }
2577
+ this.notify();
2578
+ }
2579
+ isCompatibleUser(user) {
2580
+ if (!user) return false;
2581
+ if (!this.did) return true;
2582
+ return user.sub === this.did;
2583
+ }
2584
+ async recoverMissingUserProfile(reason, accessToken) {
2585
+ if (!this.isSignedIn || this.user) return;
2586
+ const now = Date.now();
2587
+ if (this.nextUserRecoveryAt > now) {
2588
+ log(
2589
+ `Skipping user profile recovery on ${reason} until ${new Date(this.nextUserRecoveryAt).toISOString()}`
2590
+ );
2591
+ return;
2592
+ }
2593
+ log(`Attempting user profile recovery on ${reason}`);
2594
+ const token = accessToken ?? await this.getToken();
2595
+ if (this.user) return;
2596
+ await this.fetchUser(token);
2597
+ }
2598
+ isDefinitiveTokenErrorCode(code) {
2599
+ return typeof code === "string" && DEFINITIVE_TOKEN_ERRORS.has(code);
2600
+ }
2601
+ isDefinitiveAuthFailure(error) {
2602
+ return error instanceof DefinitiveAuthError;
2603
+ }
2604
+ /**
2605
+ * Centralised auth status setter. Derives `isSignedIn` and `isAuthReady`
2606
+ * from the status so they stay consistent.
2607
+ *
2608
+ * `isSignedIn` is intentionally `true` during `reauth_required` so the
2609
+ * UI layer can still display user info while prompting re-authentication.
2610
+ * Consumers should check `authStatus` (or a future convenience getter)
2611
+ * when they need to distinguish "healthy session" from "needs re-auth".
2612
+ */
2613
+ updateAuthStatus(status, errorCode = null) {
2614
+ this.authStatus = status;
2615
+ this.authErrorCode = errorCode;
2616
+ this.isSignedIn = status === "authenticated" || status === "recovering" || status === "reauth_required";
2617
+ this.isAuthReady = status !== "bootstrapping";
2618
+ }
2619
+ async clearStoredSessionTokens() {
2620
+ await this.storage.remove(STORAGE_KEYS.REFRESH_TOKEN);
2621
+ await this.storage.remove(STORAGE_KEYS.REDIRECT_URI);
2622
+ await this.storage.remove(STORAGE_KEYS.CODE_VERIFIER);
2623
+ }
2624
+ async restoreStoredSession(reason) {
2625
+ const refreshToken = await this.getRefreshToken();
2626
+ if (!refreshToken) {
2627
+ log(`No stored refresh token available during ${reason}`);
2628
+ await this.restoreCachedUser({ hasRecoverableSession: false });
2629
+ return;
2630
+ }
2631
+ log(`Restoring stored session during ${reason}`);
2632
+ await this.restoreCachedUser({ hasRecoverableSession: true });
2633
+ if (!this.isOnline) {
2634
+ return;
2635
+ }
2636
+ this.exchangeToken(refreshToken, true).catch(async (error) => {
2637
+ log(`Stored session refresh failed during ${reason}:`, error);
2638
+ if (this.isDefinitiveAuthFailure(error)) {
2639
+ return;
2640
+ }
2641
+ await this.restoreCachedUser({ hasRecoverableSession: true });
2642
+ });
2643
+ }
2644
+ async handleExternalTokenRefresh(data) {
2645
+ await this.syncRefreshTokenFromStorage();
2646
+ const refreshToken = await this.getRefreshToken();
2647
+ if (data.accessToken && refreshToken) {
2648
+ try {
2649
+ const decoded = jwtDecode(data.accessToken);
2650
+ const expiresIn = decoded.exp != null ? Math.max(0, decoded.exp - Math.floor(Date.now() / 1e3)) : 0;
2651
+ this.token = {
2652
+ access_token: data.accessToken,
2653
+ token_type: "Bearer",
2654
+ expires_in: expiresIn,
2655
+ refresh_token: refreshToken
2656
+ };
2657
+ this.applyTokenClaims(decoded);
2658
+ } catch (error) {
2659
+ log("Failed to decode token refreshed by another tab:", error);
2660
+ this.token = {
2661
+ access_token: data.accessToken,
2662
+ token_type: "Bearer",
2663
+ expires_in: 0,
2664
+ refresh_token: refreshToken
2665
+ };
2666
+ }
2667
+ if (this.authStatus !== "reauth_required") {
2668
+ this.updateAuthStatus("authenticated");
2669
+ }
2670
+ } else if (refreshToken) {
2671
+ await this.restoreCachedUser({ hasRecoverableSession: true });
2672
+ }
2673
+ if (data.did) this.did = data.did;
2674
+ if (data.tokenScope) this.tokenScope = data.tokenScope;
2675
+ this.notify();
2676
+ }
2677
+ async fetchCurrentSession(accessToken) {
2678
+ const endpoints = await this.getActivePdsEndpoints();
2679
+ const response = await fetch(`${endpoints.pds_url}/auth/session`, {
2680
+ method: "GET",
2681
+ headers: { Authorization: `Bearer ${accessToken}` }
2682
+ });
2683
+ const data = await response.json().catch(() => ({}));
2684
+ if (response.status === 401 && data.reauth_required) {
2685
+ await this.markReauthRequired(data.error || "invalid_session");
2686
+ throw new DefinitiveAuthError(data.error || "invalid_session");
2687
+ }
2688
+ if (!response.ok) {
2689
+ throw new Error(`Failed to reconcile session: ${response.status}`);
2690
+ }
2691
+ return data;
2692
+ }
2693
+ async markReauthRequired(code, options) {
2694
+ log("Marking auth session as requiring reauthentication:", code);
2695
+ await this.clearStoredSessionTokens();
2696
+ if (!this.user) {
2697
+ const cached = await this.storage.get(STORAGE_KEYS.USER_INFO);
2698
+ if (cached) {
2699
+ try {
2700
+ this.user = JSON.parse(cached);
2701
+ } catch {
2702
+ }
2703
+ }
2704
+ }
2705
+ this.token = null;
2706
+ this.tokenScope = null;
2707
+ this.nextUserRecoveryAt = 0;
2708
+ this.updateAuthStatus("reauth_required", code);
2709
+ if (options?.broadcast !== false) {
2710
+ this.broadcastSessionInvalidated(code);
2711
+ }
2712
+ this.notify();
2713
+ }
2354
2714
  };
2355
2715
 
2356
2716
  // src/AuthContext.tsx
@@ -2599,6 +2959,8 @@ function snapshotAuth(mgr) {
2599
2959
  isSignedIn: mgr.isSignedIn,
2600
2960
  hasToken: !!mgr.token,
2601
2961
  isAuthReady: mgr.isAuthReady,
2962
+ authStatus: mgr.authStatus,
2963
+ authErrorCode: mgr.authErrorCode,
2602
2964
  user: mgr.user,
2603
2965
  did: mgr.did,
2604
2966
  tokenScope: mgr.tokenScope
@@ -2633,6 +2995,8 @@ function BasicProvider({
2633
2995
  isSignedIn: false,
2634
2996
  hasToken: false,
2635
2997
  isAuthReady: false,
2998
+ authStatus: "bootstrapping",
2999
+ authErrorCode: null,
2636
3000
  user: null,
2637
3001
  did: null,
2638
3002
  tokenScope: null
@@ -2655,9 +3019,11 @@ function BasicProvider({
2655
3019
  const remoteDbRef = useRef(null);
2656
3020
  const [shouldConnect, setShouldConnect] = useState2(false);
2657
3021
  const [dbStatus, setDbStatus] = useState2("OFFLINE" /* OFFLINE */);
2658
- const [isReady, setIsReady] = useState2(false);
3022
+ const [isDbReady, setIsDbReady] = useState2(false);
2659
3023
  const [error, setError] = useState2(null);
2660
- const [schemaDevInfo, setSchemaDevInfo] = useState2(null);
3024
+ const [schemaDevInfo, setSchemaDevInfo] = useState2(
3025
+ null
3026
+ );
2661
3027
  const isDevMode = () => isDevelopment(debug);
2662
3028
  const refreshSchemaStatus = useCallback2(async () => {
2663
3029
  const s = schemaRef.current;
@@ -2697,10 +3063,16 @@ function BasicProvider({
2697
3063
  useEffect(() => {
2698
3064
  const runVersionUpdater = async () => {
2699
3065
  try {
2700
- const versionUpdater = createVersionUpdater(storageAdapter, version, getMigrations());
3066
+ const versionUpdater = createVersionUpdater(
3067
+ storageAdapter,
3068
+ version,
3069
+ getMigrations()
3070
+ );
2701
3071
  const updateResult = await versionUpdater.checkAndUpdate();
2702
3072
  if (updateResult.updated) {
2703
- log(`App updated from ${updateResult.fromVersion} to ${updateResult.toVersion}`);
3073
+ log(
3074
+ `App updated from ${updateResult.fromVersion} to ${updateResult.toVersion}`
3075
+ );
2704
3076
  } else {
2705
3077
  log(`App version ${updateResult.toVersion} is current`);
2706
3078
  }
@@ -2722,8 +3094,13 @@ function BasicProvider({
2722
3094
  const newStatus = getSyncStatus(status);
2723
3095
  setDbStatus(newStatus);
2724
3096
  if (newStatus === "ERROR_WILL_RETRY" /* ERROR_WILL_RETRY */) {
2725
- log("Sync entered ERROR_WILL_RETRY - proactively refreshing token");
2726
- authRef.current.getToken({ forceRefresh: true }).catch(() => {
3097
+ log(
3098
+ "Sync entered ERROR_WILL_RETRY - reconciling auth session before retry"
3099
+ );
3100
+ authRef.current.reconcileSession("sync retry", {
3101
+ forceRefresh: true,
3102
+ throttleMs: 0
3103
+ }).catch(() => {
2727
3104
  });
2728
3105
  }
2729
3106
  });
@@ -2732,7 +3109,7 @@ function BasicProvider({
2732
3109
  } else {
2733
3110
  log("Sync is disabled");
2734
3111
  }
2735
- setIsReady(true);
3112
+ setIsDbReady(true);
2736
3113
  }
2737
3114
  }
2738
3115
  function initRemoteDb() {
@@ -2743,7 +3120,7 @@ function BasicProvider({
2743
3120
  title: "Project ID Required",
2744
3121
  message: "Remote mode requires a project_id. Provide it via schema.project_id or the project_id prop."
2745
3122
  });
2746
- setIsReady(true);
3123
+ setIsDbReady(true);
2747
3124
  return;
2748
3125
  }
2749
3126
  log("Initializing Basic Remote DB");
@@ -2759,11 +3136,16 @@ function BasicProvider({
2759
3136
  log("403 Forbidden - user lacks required scope, not signing out");
2760
3137
  return;
2761
3138
  }
2762
- handleSignOut();
3139
+ authRef.current.reconcileSession(`remote db ${error2.errorType}`, {
3140
+ forceRefresh: error2.errorType !== "network",
3141
+ throttleMs: 0
3142
+ }).catch((reconcileError) => {
3143
+ log("RemoteDB auth recovery failed:", reconcileError);
3144
+ });
2763
3145
  }
2764
3146
  });
2765
3147
  setDbStatus("ONLINE" /* ONLINE */);
2766
- setIsReady(true);
3148
+ setIsDbReady(true);
2767
3149
  }
2768
3150
  }
2769
3151
  async function checkSchema() {
@@ -2789,7 +3171,7 @@ function BasicProvider({
2789
3171
  title: "Basic Schema is invalid!",
2790
3172
  message: errorMessage
2791
3173
  });
2792
- setIsReady(true);
3174
+ setIsDbReady(true);
2793
3175
  return null;
2794
3176
  }
2795
3177
  setSchemaDevInfo({
@@ -2806,7 +3188,9 @@ function BasicProvider({
2806
3188
  await initSyncDb({ shouldConnect: true });
2807
3189
  } else {
2808
3190
  if (result.schemaStatus.status === "unpublished") {
2809
- log("Schema not published yet (version 0) - sync is disabled. Publish your schema to enable sync.");
3191
+ log(
3192
+ "Schema not published yet (version 0) - sync is disabled. Publish your schema to enable sync."
3193
+ );
2810
3194
  } else {
2811
3195
  log("Schema is invalid!", result.schemaStatus);
2812
3196
  }
@@ -2830,12 +3214,12 @@ function BasicProvider({
2830
3214
  if (dbMode === "remote" && project_id) {
2831
3215
  initRemoteDb();
2832
3216
  } else {
2833
- setIsReady(true);
3217
+ setIsDbReady(true);
2834
3218
  }
2835
3219
  }
2836
3220
  }, []);
2837
3221
  useEffect(() => {
2838
- if (authState.hasToken && syncRef.current && authState.isSignedIn && shouldConnect) {
3222
+ if (authState.hasToken && syncRef.current && authState.isSignedIn && authState.authStatus !== "reauth_required" && shouldConnect) {
2839
3223
  log("connecting to db...");
2840
3224
  syncRef.current?.connect({
2841
3225
  getToken: (opts) => authRef.current.getToken(opts),
@@ -2844,7 +3228,22 @@ function BasicProvider({
2844
3228
  log("error connecting to db", e);
2845
3229
  });
2846
3230
  }
2847
- }, [authState.isSignedIn, authState.hasToken, shouldConnect]);
3231
+ }, [
3232
+ authState.authStatus,
3233
+ authState.isSignedIn,
3234
+ authState.hasToken,
3235
+ shouldConnect
3236
+ ]);
3237
+ useEffect(() => {
3238
+ if (authState.authStatus !== "reauth_required" || !syncRef.current) {
3239
+ return;
3240
+ }
3241
+ log("Auth requires reauthentication - disconnecting sync without deleting local DB");
3242
+ setDbStatus("ERROR_TOKEN_EXPIRED" /* ERROR_TOKEN_EXPIRED */);
3243
+ syncRef.current.disconnect({ ws_url: authConfig.ws_url }).catch((disconnectError) => {
3244
+ log("Error disconnecting sync after auth invalidation:", disconnectError);
3245
+ });
3246
+ }, [authConfig.ws_url, authState.authStatus]);
2848
3247
  const handleSignOut = async () => {
2849
3248
  await authRef.current.signOut();
2850
3249
  if (syncRef.current) {
@@ -2852,11 +3251,13 @@ function BasicProvider({
2852
3251
  await syncRef.current.close();
2853
3252
  await syncRef.current.delete({ disableAutoOpen: false });
2854
3253
  syncRef.current = null;
2855
- window?.location?.reload();
2856
3254
  } catch (error2) {
2857
3255
  console.error("Error during database cleanup:", error2);
2858
3256
  }
2859
3257
  }
3258
+ if (typeof window !== "undefined") {
3259
+ window.location.reload();
3260
+ }
2860
3261
  };
2861
3262
  const handleSignIn = async () => {
2862
3263
  try {
@@ -2895,6 +3296,8 @@ function BasicProvider({
2895
3296
  const contextValue = {
2896
3297
  isReady: authState.isAuthReady,
2897
3298
  isSignedIn: authState.isSignedIn,
3299
+ authStatus: authState.authStatus,
3300
+ authErrorCode: authState.authErrorCode,
2898
3301
  user: authState.user,
2899
3302
  did: authState.did,
2900
3303
  scope: authState.tokenScope,
@@ -2920,7 +3323,7 @@ function BasicProvider({
2920
3323
  return /* @__PURE__ */ jsxs2(BasicContext.Provider, { value: contextValue, children: [
2921
3324
  error && isDevMode() && /* @__PURE__ */ jsx2(ErrorDisplay, { error }),
2922
3325
  devToolbar && isDevMode() && /* @__PURE__ */ jsx2(Suspense, { fallback: null, children: /* @__PURE__ */ jsx2(BasicDevToolbar2, { debug }) }),
2923
- isReady && children
3326
+ isDbReady && authState.isAuthReady && children
2924
3327
  ] });
2925
3328
  }
2926
3329
  function ErrorDisplay({ error }) {