@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.js CHANGED
@@ -93,6 +93,7 @@ var init_syncProtocol = __esm({
93
93
  var requestId = 0;
94
94
  var acceptCallbacks = {};
95
95
  var refreshTimer = null;
96
+ var pendingTokenUpdate = null;
96
97
  log("Connecting to", url);
97
98
  var ws = new WebSocket(url);
98
99
  function sendChanges(changes2, baseRevision2, partial2, onChangesAccepted2) {
@@ -115,6 +116,12 @@ var init_syncProtocol = __esm({
115
116
  refreshTimer = null;
116
117
  }
117
118
  }
119
+ function sendTokenUpdate(token) {
120
+ if (ws.readyState !== WebSocket.OPEN) return false;
121
+ pendingTokenUpdate = token;
122
+ ws.send(JSON.stringify({ type: "tokenUpdate", authToken: token }));
123
+ return true;
124
+ }
118
125
  function resolveGetToken() {
119
126
  var fn = getTokenGetter(url);
120
127
  if (!fn) throw new Error("No token getter registered for " + url);
@@ -130,10 +137,8 @@ var init_syncProtocol = __esm({
130
137
  refreshTimer = setTimeout(async function() {
131
138
  try {
132
139
  var newToken = await resolveGetToken()({ forceRefresh: true });
133
- if (ws.readyState === WebSocket.OPEN) {
140
+ if (sendTokenUpdate(newToken)) {
134
141
  log("Sending tokenUpdate on existing WebSocket");
135
- ws.send(JSON.stringify({ type: "tokenUpdate", authToken: newToken }));
136
- scheduleTokenRefresh(newToken);
137
142
  }
138
143
  } catch (err) {
139
144
  log("Proactive token refresh failed (non-fatal):", err);
@@ -163,10 +168,7 @@ var init_syncProtocol = __esm({
163
168
  if (document.visibilityState === "visible" && ws.readyState === WebSocket.OPEN) {
164
169
  log("Page became visible - refreshing token for WebSocket");
165
170
  resolveGetToken()({ forceRefresh: true }).then(function(newToken) {
166
- if (ws.readyState === WebSocket.OPEN) {
167
- ws.send(JSON.stringify({ type: "tokenUpdate", authToken: newToken }));
168
- scheduleTokenRefresh(newToken);
169
- }
171
+ sendTokenUpdate(newToken);
170
172
  }).catch(function(err) {
171
173
  log("Token refresh on visibility resume failed:", err);
172
174
  });
@@ -232,6 +234,19 @@ var init_syncProtocol = __esm({
232
234
  });
233
235
  isFirstRound = false;
234
236
  }
237
+ } else if (requestFromServer.type == "tokenUpdateAck") {
238
+ if (requestFromServer.ok) {
239
+ scheduleTokenRefresh(requestFromServer.authToken || pendingTokenUpdate);
240
+ pendingTokenUpdate = null;
241
+ } else {
242
+ log("tokenUpdate rejected by server:", requestFromServer.code || requestFromServer.message);
243
+ pendingTokenUpdate = null;
244
+ ws.close(4001, requestFromServer.code || "token_update_failed");
245
+ onError(
246
+ requestFromServer.message || "Authentication refresh failed",
247
+ RECONNECT_DELAY
248
+ );
249
+ }
235
250
  } else if (requestFromServer.type == "ack") {
236
251
  var requestId2 = requestFromServer.requestId;
237
252
  var acceptCallback = acceptCallbacks[requestId2.toString()];
@@ -266,7 +281,7 @@ var init_syncProtocol = __esm({
266
281
  var version;
267
282
  var init_package = __esm({
268
283
  "package.json"() {
269
- version = "0.8.0-beta.3";
284
+ version = "0.8.0-beta.4";
270
285
  }
271
286
  });
272
287
 
@@ -410,6 +425,8 @@ var init_context = __esm({
410
425
  BasicContext = (0, import_react.createContext)({
411
426
  isReady: false,
412
427
  isSignedIn: false,
428
+ authStatus: "bootstrapping",
429
+ authErrorCode: null,
413
430
  user: null,
414
431
  did: null,
415
432
  scope: null,
@@ -1678,6 +1695,21 @@ async function resolveHandle(handle) {
1678
1695
  // src/core/auth/AuthManager.ts
1679
1696
  init_network();
1680
1697
  init_config();
1698
+ var DEFINITIVE_TOKEN_ERRORS = /* @__PURE__ */ new Set([
1699
+ "invalid_grant",
1700
+ "invalid_client",
1701
+ "unauthorized_client"
1702
+ ]);
1703
+ var USER_RECOVERY_RETRY_COOLDOWN_MS = 3e4;
1704
+ var SESSION_RECONCILE_THROTTLE_MS = 5e3;
1705
+ var DefinitiveAuthError = class extends Error {
1706
+ code;
1707
+ constructor(code) {
1708
+ super(`Definitive auth failure: ${code}`);
1709
+ this.name = "DefinitiveAuthError";
1710
+ this.code = code;
1711
+ }
1712
+ };
1681
1713
  function generateCodeVerifier() {
1682
1714
  const array = new Uint8Array(32);
1683
1715
  crypto.getRandomValues(array);
@@ -1685,7 +1717,9 @@ function generateCodeVerifier() {
1685
1717
  }
1686
1718
  async function generateCodeChallenge(verifier) {
1687
1719
  if (typeof crypto === "undefined" || !crypto.subtle) {
1688
- log("crypto.subtle unavailable (non-secure context?) -- falling back to plain PKCE challenge");
1720
+ log(
1721
+ "crypto.subtle unavailable (non-secure context?) -- falling back to plain PKCE challenge"
1722
+ );
1689
1723
  return { challenge: verifier, method: "plain" };
1690
1724
  }
1691
1725
  const encoder = new TextEncoder();
@@ -1706,6 +1740,8 @@ var AuthManager = class {
1706
1740
  user = null;
1707
1741
  isSignedIn = false;
1708
1742
  isAuthReady = false;
1743
+ authStatus = "bootstrapping";
1744
+ authErrorCode = null;
1709
1745
  did = null;
1710
1746
  /** Space-separated scopes granted in the current access token */
1711
1747
  tokenScope = null;
@@ -1722,6 +1758,9 @@ var AuthManager = class {
1722
1758
  pendingRefresh = false;
1723
1759
  isOnline = typeof navigator !== "undefined" ? navigator.onLine : true;
1724
1760
  channel = null;
1761
+ nextUserRecoveryAt = 0;
1762
+ sessionCheckPromise = null;
1763
+ lastSessionCheckAt = 0;
1725
1764
  constructor(config, storage, notify) {
1726
1765
  this.config = config;
1727
1766
  this.storage = storage;
@@ -1736,31 +1775,26 @@ var AuthManager = class {
1736
1775
  this.channel.onmessage = (event) => {
1737
1776
  if (event.data?.type === "token_refreshed") {
1738
1777
  log("Received token refresh from another tab");
1739
- if (event.data.accessToken && this.token) {
1740
- this.token = { ...this.token, access_token: event.data.accessToken };
1741
- }
1742
- if (event.data.did) this.did = event.data.did;
1743
- if (event.data.tokenScope) this.tokenScope = event.data.tokenScope;
1744
- this.notify();
1778
+ void this.handleExternalTokenRefresh(event.data);
1745
1779
  }
1746
1780
  if (event.data?.type === "signed_in") {
1747
- log("Received sign-in from another tab, reloading");
1748
- if (typeof window !== "undefined") {
1749
- window.location.reload();
1750
- }
1781
+ log("Received sign-in from another tab, restoring session");
1782
+ void this.restoreStoredSession("cross-tab sign-in");
1751
1783
  }
1752
1784
  if (event.data?.type === "signed_out") {
1753
- log("Received sign-out from another tab, reloading");
1754
- this.user = null;
1755
- this.isSignedIn = false;
1756
- this.token = null;
1757
- this.did = null;
1758
- this.tokenScope = null;
1785
+ log("Received sign-out from another tab");
1786
+ this.resetAuthState("signed_out");
1759
1787
  this.notify();
1760
1788
  if (typeof window !== "undefined") {
1761
1789
  window.location.reload();
1762
1790
  }
1763
1791
  }
1792
+ if (event.data?.type === "session_invalidated") {
1793
+ log("Received session invalidation from another tab");
1794
+ void this.markReauthRequired(event.data.code || "invalid_grant", {
1795
+ broadcast: false
1796
+ });
1797
+ }
1764
1798
  };
1765
1799
  } catch {
1766
1800
  log("BroadcastChannel not available for cross-tab sync");
@@ -1780,6 +1814,9 @@ var AuthManager = class {
1780
1814
  broadcastSignOut() {
1781
1815
  this.channel?.postMessage({ type: "signed_out" });
1782
1816
  }
1817
+ broadcastSessionInvalidated(code) {
1818
+ this.channel?.postMessage({ type: "session_invalidated", code });
1819
+ }
1783
1820
  // ------------------------------------------------------------------
1784
1821
  // Public API
1785
1822
  // ------------------------------------------------------------------
@@ -1788,7 +1825,11 @@ var AuthManager = class {
1788
1825
  * from refresh token, or load cached user for offline mode.
1789
1826
  */
1790
1827
  async initialize() {
1791
- await this.storage.set(STORAGE_KEYS.DEBUG, this.config.debug ? "true" : "false");
1828
+ this.updateAuthStatus("bootstrapping");
1829
+ await this.storage.set(
1830
+ STORAGE_KEYS.DEBUG,
1831
+ this.config.debug ? "true" : "false"
1832
+ );
1792
1833
  const storedServerUrl = await this.storage.get(STORAGE_KEYS.SERVER_URL);
1793
1834
  if (storedServerUrl && storedServerUrl !== this.config.pdsUrl) {
1794
1835
  log("PDS URL changed, clearing stored tokens");
@@ -1800,7 +1841,7 @@ var AuthManager = class {
1800
1841
  if (params.has("code")) {
1801
1842
  const code = params.get("code");
1802
1843
  if (!code) {
1803
- this.isAuthReady = true;
1844
+ this.updateAuthStatus("signed_out");
1804
1845
  this.notify();
1805
1846
  return;
1806
1847
  }
@@ -1808,7 +1849,7 @@ var AuthManager = class {
1808
1849
  const urlState = params.get("state");
1809
1850
  if (!state || state !== urlState) {
1810
1851
  log("error: auth state does not match");
1811
- this.isAuthReady = true;
1852
+ this.updateAuthStatus("signed_out");
1812
1853
  this.notify();
1813
1854
  await this.storage.remove(STORAGE_KEYS.AUTH_STATE);
1814
1855
  cleanOAuthParamsFromUrl();
@@ -1819,34 +1860,18 @@ var AuthManager = class {
1819
1860
  this.freshSignIn = true;
1820
1861
  this.exchangeToken(code, false).catch((error) => {
1821
1862
  log("Error fetching token:", error);
1863
+ this.freshSignIn = false;
1864
+ void this.restoreCachedUser({
1865
+ hasRecoverableSession: !this.isDefinitiveAuthFailure(error)
1866
+ });
1822
1867
  });
1823
1868
  } else {
1824
- const refreshToken = await this.storage.get(STORAGE_KEYS.REFRESH_TOKEN);
1825
- if (refreshToken) {
1826
- log("Found refresh token in storage, attempting to refresh access token");
1827
- this.exchangeToken(refreshToken, true).catch(async (error) => {
1828
- log("Error fetching refresh token:", error);
1829
- if (this.isNetworkError(error)) {
1830
- await this.restoreCachedUser();
1831
- }
1832
- });
1833
- } else {
1834
- const cachedUserInfo = await this.storage.get(STORAGE_KEYS.USER_INFO);
1835
- if (cachedUserInfo) {
1836
- try {
1837
- this.user = JSON.parse(cachedUserInfo);
1838
- this.isSignedIn = true;
1839
- log("Loaded cached user info for offline mode");
1840
- } catch (error) {
1841
- log("Error parsing cached user info:", error);
1842
- }
1843
- }
1844
- this.isAuthReady = true;
1845
- this.notify();
1846
- }
1869
+ await this.restoreStoredSession("initialize");
1847
1870
  }
1848
1871
  } catch (e) {
1849
1872
  log("error getting token", e);
1873
+ this.updateAuthStatus("signed_out");
1874
+ this.notify();
1850
1875
  }
1851
1876
  }
1852
1877
  /**
@@ -1856,7 +1881,7 @@ var AuthManager = class {
1856
1881
  async getToken(options) {
1857
1882
  log("getting token...");
1858
1883
  if (!this.token) {
1859
- const refreshToken = await this.storage.get(STORAGE_KEYS.REFRESH_TOKEN);
1884
+ const refreshToken = await this.getRefreshToken();
1860
1885
  if (refreshToken) {
1861
1886
  log("No token in memory, attempting to refresh from storage");
1862
1887
  if (this.refreshPromise) {
@@ -1879,7 +1904,12 @@ var AuthManager = class {
1879
1904
  } catch (error) {
1880
1905
  log("Failed to refresh token from storage:", error);
1881
1906
  if (this.isNetworkError(error)) {
1882
- throw new Error("Network offline - authentication will be retried when online");
1907
+ throw new Error(
1908
+ "Network offline - authentication will be retried when online"
1909
+ );
1910
+ }
1911
+ if (!this.isDefinitiveAuthFailure(error)) {
1912
+ throw error;
1883
1913
  }
1884
1914
  throw new Error("Authentication expired. Please sign in again.");
1885
1915
  }
@@ -1892,12 +1922,15 @@ var AuthManager = class {
1892
1922
  const isExpired = decoded.exp && decoded.exp < Date.now() / 1e3 + expirationBuffer;
1893
1923
  const shouldRefresh = isExpired || options?.forceRefresh === true;
1894
1924
  if (shouldRefresh) {
1895
- log(options?.forceRefresh ? "force refreshing token..." : "token is expired - refreshing ...");
1925
+ log(
1926
+ options?.forceRefresh ? "force refreshing token..." : "token is expired - refreshing ..."
1927
+ );
1896
1928
  if (this.refreshPromise) {
1897
1929
  log("Token refresh already in progress, waiting...");
1898
1930
  try {
1899
1931
  const newToken = await this.refreshPromise;
1900
- if (!newToken?.access_token) throw new Error("Token refresh returned empty access token");
1932
+ if (!newToken?.access_token)
1933
+ throw new Error("Token refresh returned empty access token");
1901
1934
  return newToken.access_token;
1902
1935
  } catch (error) {
1903
1936
  log("In-flight refresh failed:", error);
@@ -1908,11 +1941,12 @@ var AuthManager = class {
1908
1941
  throw error;
1909
1942
  }
1910
1943
  }
1911
- const refreshToken = this.token.refresh_token || await this.storage.get(STORAGE_KEYS.REFRESH_TOKEN);
1944
+ const refreshToken = await this.getRefreshToken();
1912
1945
  if (refreshToken) {
1913
1946
  try {
1914
1947
  const newToken = await this.exchangeToken(refreshToken, true);
1915
- if (!newToken?.access_token) throw new Error("Token refresh returned empty access token");
1948
+ if (!newToken?.access_token)
1949
+ throw new Error("Token refresh returned empty access token");
1916
1950
  return newToken.access_token;
1917
1951
  } catch (error) {
1918
1952
  log("Failed to refresh expired token:", error);
@@ -1920,13 +1954,17 @@ var AuthManager = class {
1920
1954
  log("Network issue - using expired token until network is restored");
1921
1955
  return this.token.access_token;
1922
1956
  }
1957
+ if (!this.isDefinitiveAuthFailure(error)) {
1958
+ throw error;
1959
+ }
1923
1960
  throw new Error("Authentication expired. Please sign in again.");
1924
1961
  }
1925
1962
  } else {
1926
1963
  throw new Error("no refresh token available");
1927
1964
  }
1928
1965
  }
1929
- if (!this.token.access_token) throw new Error("Token exists but access_token is empty");
1966
+ if (!this.token.access_token)
1967
+ throw new Error("Token exists but access_token is empty");
1930
1968
  return this.token.access_token;
1931
1969
  }
1932
1970
  async getSignInUrl(redirectUri, endpoints) {
@@ -1935,8 +1973,13 @@ var AuthManager = class {
1935
1973
  throw new Error("Project ID is required to generate sign-in link");
1936
1974
  }
1937
1975
  const pdsEndpoints = endpoints || this.defaultPdsEndpoints();
1938
- await this.storage.set(STORAGE_KEYS.PDS_ENDPOINTS, JSON.stringify(pdsEndpoints));
1939
- const randomState = base64UrlEncode(crypto.getRandomValues(new Uint8Array(16)));
1976
+ await this.storage.set(
1977
+ STORAGE_KEYS.PDS_ENDPOINTS,
1978
+ JSON.stringify(pdsEndpoints)
1979
+ );
1980
+ const randomState = base64UrlEncode(
1981
+ crypto.getRandomValues(new Uint8Array(16))
1982
+ );
1940
1983
  await this.storage.set(STORAGE_KEYS.AUTH_STATE, randomState);
1941
1984
  const redirectUrl = redirectUri || window.location.href;
1942
1985
  if (!redirectUrl || !redirectUrl.startsWith("http://") && !redirectUrl.startsWith("https://")) {
@@ -2005,7 +2048,10 @@ var AuthManager = class {
2005
2048
  if (state) {
2006
2049
  const storedState = await this.storage.get(STORAGE_KEYS.AUTH_STATE);
2007
2050
  if (storedState && storedState !== state) {
2008
- log("State parameter mismatch:", { provided: state, stored: storedState });
2051
+ log("State parameter mismatch:", {
2052
+ provided: state,
2053
+ stored: storedState
2054
+ });
2009
2055
  return { success: false, error: "State parameter mismatch" };
2010
2056
  }
2011
2057
  }
@@ -2021,6 +2067,7 @@ var AuthManager = class {
2021
2067
  }
2022
2068
  } catch (error) {
2023
2069
  log("signInWithCode error:", error);
2070
+ this.freshSignIn = false;
2024
2071
  return {
2025
2072
  success: false,
2026
2073
  error: error.message || "Authentication failed"
@@ -2033,13 +2080,64 @@ var AuthManager = class {
2033
2080
  */
2034
2081
  async signOut() {
2035
2082
  log("signing out!");
2036
- this.resetAuthState();
2083
+ this.resetAuthState("signed_out");
2037
2084
  await this.storage.remove(STORAGE_KEYS.AUTH_STATE);
2038
2085
  await this.storage.remove(STORAGE_KEYS.LAST_CONNECT_REPORT);
2039
2086
  await this.clearStoredAuth();
2040
2087
  this.broadcastSignOut();
2041
2088
  this.notify();
2042
2089
  }
2090
+ async reconcileSession(reason = "manual", options) {
2091
+ if (this.authStatus === "signed_out" || this.authStatus === "reauth_required") {
2092
+ return;
2093
+ }
2094
+ if (!this.isOnline) {
2095
+ this.updateAuthStatus("recovering", this.authErrorCode);
2096
+ this.notify();
2097
+ return;
2098
+ }
2099
+ const throttleMs = options?.throttleMs ?? SESSION_RECONCILE_THROTTLE_MS;
2100
+ const forceRefresh = options?.forceRefresh === true;
2101
+ const now = Date.now();
2102
+ if (this.sessionCheckPromise) {
2103
+ return this.sessionCheckPromise;
2104
+ }
2105
+ if (!forceRefresh && now - this.lastSessionCheckAt < throttleMs) {
2106
+ return;
2107
+ }
2108
+ this.lastSessionCheckAt = now;
2109
+ let sessionCheck = null;
2110
+ sessionCheck = (async () => {
2111
+ try {
2112
+ const accessToken = await this.getToken(
2113
+ forceRefresh ? { forceRefresh: true } : void 0
2114
+ );
2115
+ const currentSession = await this.fetchCurrentSession(accessToken);
2116
+ if (currentSession?.active) {
2117
+ this.updateAuthStatus("authenticated");
2118
+ this.notify();
2119
+ if (!this.user) {
2120
+ await this.recoverMissingUserProfile(reason, accessToken);
2121
+ }
2122
+ }
2123
+ } catch (error) {
2124
+ log(`Session reconciliation failed on ${reason}:`, error);
2125
+ if (this.isDefinitiveAuthFailure(error)) {
2126
+ return;
2127
+ }
2128
+ if (this.isNetworkError(error)) {
2129
+ this.updateAuthStatus("recovering", this.authErrorCode);
2130
+ this.notify();
2131
+ }
2132
+ } finally {
2133
+ if (this.sessionCheckPromise === sessionCheck) {
2134
+ this.sessionCheckPromise = null;
2135
+ }
2136
+ }
2137
+ })();
2138
+ this.sessionCheckPromise = sessionCheck;
2139
+ return sessionCheck;
2140
+ }
2043
2141
  hasScope(scope) {
2044
2142
  if (!this.tokenScope) return false;
2045
2143
  return this.tokenScope.split(/[\s,]+/).filter(Boolean).includes(scope);
@@ -2065,16 +2163,26 @@ var AuthManager = class {
2065
2163
  const handleOnline = async () => {
2066
2164
  log("Network came back online");
2067
2165
  this.isOnline = true;
2068
- if (this.pendingRefresh && this.token) {
2166
+ if (this.pendingRefresh) {
2069
2167
  log("Retrying pending token refresh");
2070
2168
  this.pendingRefresh = false;
2071
- const refreshToken = this.token.refresh_token || await this.storage.get(STORAGE_KEYS.REFRESH_TOKEN);
2169
+ const refreshToken = await this.getRefreshToken();
2072
2170
  if (refreshToken) {
2073
2171
  this.exchangeToken(refreshToken, true).catch((error) => {
2074
2172
  log("Retry refresh failed:", error);
2075
2173
  });
2076
2174
  }
2077
2175
  }
2176
+ if (this.isSignedIn) {
2177
+ this.reconcileSession("online event", {
2178
+ forceRefresh: true,
2179
+ throttleMs: 0
2180
+ }).catch((error) => {
2181
+ log("Session reconciliation on online failed:", error);
2182
+ });
2183
+ } else if (this.user) {
2184
+ await this.restoreStoredSession("online restore");
2185
+ }
2078
2186
  };
2079
2187
  const handleOffline = () => {
2080
2188
  log("Network went offline");
@@ -2082,9 +2190,11 @@ var AuthManager = class {
2082
2190
  };
2083
2191
  const handleVisibilityChange = () => {
2084
2192
  if (document.visibilityState === "visible" && this.isSignedIn) {
2085
- log("App became visible - checking token freshness");
2086
- this.getToken().catch((err) => {
2087
- log("Token refresh on visibility resume failed:", err);
2193
+ log("App became visible - reconciling auth session");
2194
+ this.reconcileSession("visibility resume", {
2195
+ forceRefresh: true
2196
+ }).catch((err) => {
2197
+ log("Session reconciliation on visibility resume failed:", err);
2088
2198
  });
2089
2199
  }
2090
2200
  };
@@ -2137,12 +2247,18 @@ var AuthManager = class {
2137
2247
  if (elapsed < 24 * 60 * 60 * 1e3) return;
2138
2248
  }
2139
2249
  try {
2140
- await fetch(`${this.config.adminUrl}/project/${this.config.projectId}/user/connect`, {
2141
- method: "POST",
2142
- headers: { "Content-Type": "application/json" },
2143
- body: JSON.stringify({ token: accessToken })
2144
- });
2145
- await this.storage.set(STORAGE_KEYS.LAST_CONNECT_REPORT, Date.now().toString());
2250
+ await fetch(
2251
+ `${this.config.adminUrl}/project/${this.config.projectId}/user/connect`,
2252
+ {
2253
+ method: "POST",
2254
+ headers: { "Content-Type": "application/json" },
2255
+ body: JSON.stringify({ token: accessToken })
2256
+ }
2257
+ );
2258
+ await this.storage.set(
2259
+ STORAGE_KEYS.LAST_CONNECT_REPORT,
2260
+ Date.now().toString()
2261
+ );
2146
2262
  log("Reported connection to admin server");
2147
2263
  } catch (err) {
2148
2264
  log("Failed to report connection (non-blocking):", err);
@@ -2153,32 +2269,37 @@ var AuthManager = class {
2153
2269
  */
2154
2270
  async processNewToken() {
2155
2271
  if (!this.token) {
2156
- this.isAuthReady = true;
2272
+ this.updateAuthStatus("signed_out");
2157
2273
  this.notify();
2158
2274
  return;
2159
2275
  }
2160
2276
  try {
2161
2277
  const decoded = (0, import_jwt_decode.jwtDecode)(this.token.access_token);
2162
- if (decoded.sub) this.did = decoded.sub;
2163
- if (decoded.scope) this.tokenScope = decoded.scope;
2278
+ this.applyTokenClaims(decoded);
2279
+ this.updateAuthStatus("authenticated");
2280
+ this.notify();
2281
+ this.broadcastSessionUpdate();
2164
2282
  await this.fetchUser(this.token.access_token);
2165
2283
  } catch (error) {
2166
2284
  log("Error processing token:", error);
2167
- this.isAuthReady = true;
2285
+ this.updateAuthStatus("recovering");
2168
2286
  this.notify();
2169
2287
  }
2170
2288
  }
2171
- async restoreCachedUser() {
2289
+ async restoreCachedUser(options) {
2172
2290
  const cached = await this.storage.get(STORAGE_KEYS.USER_INFO);
2173
- if (cached) {
2291
+ if (cached && options?.hasRecoverableSession) {
2174
2292
  try {
2175
2293
  this.user = JSON.parse(cached);
2176
- this.isSignedIn = true;
2177
- log("Restored cached user info for offline mode");
2294
+ log("Restored cached user info for recoverable session");
2178
2295
  } catch {
2179
2296
  }
2297
+ } else {
2298
+ this.user = null;
2180
2299
  }
2181
- this.isAuthReady = true;
2300
+ this.updateAuthStatus(
2301
+ options?.hasRecoverableSession ? "recovering" : "signed_out"
2302
+ );
2182
2303
  this.notify();
2183
2304
  }
2184
2305
  async fetchUser(accessToken) {
@@ -2187,7 +2308,7 @@ var AuthManager = class {
2187
2308
  const endpoints = await this.getActivePdsEndpoints();
2188
2309
  const response = await fetch(endpoints.userinfo_endpoint, {
2189
2310
  method: "GET",
2190
- headers: { "Authorization": `Bearer ${accessToken}` }
2311
+ headers: { Authorization: `Bearer ${accessToken}` }
2191
2312
  });
2192
2313
  if (!response.ok) {
2193
2314
  throw new Error(`Failed to fetch user info: ${response.status}`);
@@ -2198,28 +2319,22 @@ var AuthManager = class {
2198
2319
  throw new Error(`User info error: ${user.error}`);
2199
2320
  }
2200
2321
  if (this.token?.refresh_token) {
2201
- await this.storage.set(STORAGE_KEYS.REFRESH_TOKEN, this.token.refresh_token);
2322
+ await this.storage.set(
2323
+ STORAGE_KEYS.REFRESH_TOKEN,
2324
+ this.token.refresh_token
2325
+ );
2202
2326
  }
2203
2327
  await this.storage.set(STORAGE_KEYS.USER_INFO, JSON.stringify(user));
2204
2328
  log("Cached user info in storage");
2205
2329
  this.user = user;
2206
- this.isSignedIn = true;
2207
- this.isAuthReady = true;
2208
- if (this.freshSignIn) {
2209
- this.freshSignIn = false;
2210
- this.broadcastSignIn();
2211
- } else {
2212
- this.broadcastTokenRefresh();
2330
+ if (this.authStatus !== "reauth_required") {
2331
+ this.updateAuthStatus("authenticated");
2213
2332
  }
2333
+ this.nextUserRecoveryAt = 0;
2214
2334
  this.notify();
2215
2335
  } catch (error) {
2216
2336
  log("Failed to fetch user info:", error);
2217
- if (this.isNetworkError(error)) {
2218
- await this.restoreCachedUser();
2219
- } else {
2220
- this.isAuthReady = true;
2221
- this.notify();
2222
- }
2337
+ await this.handleUserFetchFailure();
2223
2338
  }
2224
2339
  }
2225
2340
  /**
@@ -2246,7 +2361,9 @@ var AuthManager = class {
2246
2361
  if (!this.isOnline) {
2247
2362
  log("Network is offline, marking refresh as pending");
2248
2363
  this.pendingRefresh = true;
2249
- throw new Error("Network offline - refresh will be retried when online");
2364
+ throw new Error(
2365
+ "Network offline - refresh will be retried when online"
2366
+ );
2250
2367
  }
2251
2368
  const endpoints = await this.getActivePdsEndpoints();
2252
2369
  let requestBody;
@@ -2256,26 +2373,36 @@ var AuthManager = class {
2256
2373
  refresh_token: codeOrRefreshToken
2257
2374
  };
2258
2375
  if (this.config.projectId) {
2259
- requestBody.client_id = normalizeClientId(this.config.projectId, this.adminHostname);
2376
+ requestBody.client_id = normalizeClientId(
2377
+ this.config.projectId,
2378
+ this.adminHostname
2379
+ );
2260
2380
  }
2261
2381
  } else {
2262
2382
  requestBody = {
2263
2383
  grant_type: "authorization_code",
2264
2384
  code: codeOrRefreshToken
2265
2385
  };
2266
- const storedRedirectUri = await this.storage.get(STORAGE_KEYS.REDIRECT_URI);
2386
+ const storedRedirectUri = await this.storage.get(
2387
+ STORAGE_KEYS.REDIRECT_URI
2388
+ );
2267
2389
  if (storedRedirectUri) {
2268
2390
  requestBody.redirect_uri = storedRedirectUri;
2269
2391
  log("Including redirect_uri in token exchange:", storedRedirectUri);
2270
2392
  } else {
2271
2393
  log("Warning: No redirect_uri found in storage for token exchange");
2272
2394
  }
2273
- const codeVerifier = await this.storage.get(STORAGE_KEYS.CODE_VERIFIER);
2395
+ const codeVerifier = await this.storage.get(
2396
+ STORAGE_KEYS.CODE_VERIFIER
2397
+ );
2274
2398
  if (codeVerifier) {
2275
2399
  requestBody.code_verifier = codeVerifier;
2276
2400
  }
2277
2401
  if (this.config.projectId) {
2278
- requestBody.client_id = normalizeClientId(this.config.projectId, this.adminHostname);
2402
+ requestBody.client_id = normalizeClientId(
2403
+ this.config.projectId,
2404
+ this.adminHostname
2405
+ );
2279
2406
  }
2280
2407
  }
2281
2408
  log("Token exchange request body:", {
@@ -2291,7 +2418,9 @@ var AuthManager = class {
2291
2418
  log("Network error fetching token:", error);
2292
2419
  if (!this.isOnline) {
2293
2420
  this.pendingRefresh = true;
2294
- throw new Error("Network offline - refresh will be retried when online");
2421
+ throw new Error(
2422
+ "Network offline - refresh will be retried when online"
2423
+ );
2295
2424
  }
2296
2425
  throw new Error("Network error during token refresh");
2297
2426
  });
@@ -2300,39 +2429,52 @@ var AuthManager = class {
2300
2429
  const decoded = (0, import_jwt_decode.jwtDecode)(token.access_token);
2301
2430
  if (decoded.typ === "refresh") {
2302
2431
  log("Error: received refresh token as access token");
2303
- throw new Error("Invalid token: received refresh token instead of access token");
2432
+ throw new Error(
2433
+ "Invalid token: received refresh token instead of access token"
2434
+ );
2304
2435
  }
2305
2436
  } catch (decodeError) {
2306
2437
  if (decodeError.message.includes("Invalid token")) {
2307
2438
  throw decodeError;
2308
2439
  }
2309
- log("Warning: could not decode access token for type check:", decodeError);
2440
+ log(
2441
+ "Warning: could not decode access token for type check:",
2442
+ decodeError
2443
+ );
2310
2444
  }
2311
2445
  }
2312
2446
  if (token.error) {
2313
2447
  log("error fetching token", token.error);
2314
2448
  if (typeof token.error === "string" && (token.error.includes("network") || token.error.includes("timeout"))) {
2315
2449
  this.pendingRefresh = true;
2316
- throw new Error("Network issue - refresh will be retried when online");
2450
+ throw new Error(
2451
+ "Network issue - refresh will be retried when online"
2452
+ );
2317
2453
  }
2318
- const definitiveErrors = ["invalid_grant", "invalid_client", "unauthorized_client"];
2319
- if (typeof token.error === "string" && definitiveErrors.includes(token.error)) {
2320
- await this.clearStoredAuth();
2321
- this.resetAuthState();
2322
- this.notify();
2454
+ if (this.isDefinitiveTokenErrorCode(token.error)) {
2455
+ await this.markReauthRequired(token.error);
2456
+ throw new DefinitiveAuthError(token.error);
2323
2457
  }
2324
2458
  throw new Error(`Token refresh failed: ${token.error}`);
2325
2459
  } else {
2460
+ if (!token.access_token) {
2461
+ throw new Error("Token response missing access token");
2462
+ }
2326
2463
  this.token = token;
2327
2464
  this.pendingRefresh = false;
2328
2465
  if (token.refresh_token) {
2329
- await this.storage.set(STORAGE_KEYS.REFRESH_TOKEN, token.refresh_token);
2466
+ await this.storage.set(
2467
+ STORAGE_KEYS.REFRESH_TOKEN,
2468
+ token.refresh_token
2469
+ );
2330
2470
  log("Updated refresh token in storage");
2331
2471
  }
2332
2472
  if (!isRefreshToken) {
2333
2473
  await this.storage.remove(STORAGE_KEYS.REDIRECT_URI);
2334
2474
  await this.storage.remove(STORAGE_KEYS.CODE_VERIFIER);
2335
- log("Cleaned up redirect_uri and code_verifier from storage after successful exchange");
2475
+ log(
2476
+ "Cleaned up redirect_uri and code_verifier from storage after successful exchange"
2477
+ );
2336
2478
  }
2337
2479
  this.reportConnection(token.access_token).catch(() => {
2338
2480
  });
@@ -2341,12 +2483,12 @@ var AuthManager = class {
2341
2483
  return token;
2342
2484
  } catch (error) {
2343
2485
  log("Token refresh error:", error);
2344
- const msg = error instanceof Error ? error.message : "";
2345
- const alreadyHandled = msg.startsWith("Token refresh failed:");
2346
- if (!alreadyHandled && !this.isNetworkError(error)) {
2347
- await this.clearStoredAuth();
2348
- this.resetAuthState();
2349
- this.notify();
2486
+ if (this.isDefinitiveAuthFailure(error)) {
2487
+ log("Preserving cleared auth state after definitive token rejection");
2488
+ } else if (this.isNetworkError(error)) {
2489
+ log("Recoverable network auth failure - preserving session state");
2490
+ } else {
2491
+ log("Recoverable auth failure - preserving session state");
2350
2492
  }
2351
2493
  throw error;
2352
2494
  }
@@ -2370,13 +2512,15 @@ var AuthManager = class {
2370
2512
  }
2371
2513
  return tokenPromise;
2372
2514
  }
2373
- resetAuthState() {
2374
- this.user = null;
2375
- this.isSignedIn = false;
2515
+ resetAuthState(status = "signed_out") {
2516
+ this.user = status === "reauth_required" ? this.user : null;
2376
2517
  this.token = null;
2377
- this.did = null;
2518
+ if (status !== "reauth_required") {
2519
+ this.did = null;
2520
+ }
2378
2521
  this.tokenScope = null;
2379
- this.isAuthReady = true;
2522
+ this.nextUserRecoveryAt = 0;
2523
+ this.updateAuthStatus(status);
2380
2524
  }
2381
2525
  async clearStoredAuth() {
2382
2526
  await this.storage.remove(STORAGE_KEYS.REFRESH_TOKEN);
@@ -2393,6 +2537,215 @@ var AuthManager = class {
2393
2537
  }
2394
2538
  return false;
2395
2539
  }
2540
+ async getRefreshToken() {
2541
+ const storedRefreshToken = await this.storage.get(
2542
+ STORAGE_KEYS.REFRESH_TOKEN
2543
+ );
2544
+ if (storedRefreshToken) {
2545
+ log("Using refresh token from storage");
2546
+ if (this.token && this.token.refresh_token !== storedRefreshToken) {
2547
+ this.token = { ...this.token, refresh_token: storedRefreshToken };
2548
+ }
2549
+ return storedRefreshToken;
2550
+ }
2551
+ const memoryRefreshToken = this.token?.refresh_token ?? null;
2552
+ if (memoryRefreshToken) {
2553
+ log("Using refresh token from memory fallback");
2554
+ } else {
2555
+ log("No refresh token available in storage or memory");
2556
+ }
2557
+ return memoryRefreshToken;
2558
+ }
2559
+ async syncRefreshTokenFromStorage() {
2560
+ const storedRefreshToken = await this.storage.get(
2561
+ STORAGE_KEYS.REFRESH_TOKEN
2562
+ );
2563
+ if (storedRefreshToken && this.token && this.token.refresh_token !== storedRefreshToken) {
2564
+ this.token = { ...this.token, refresh_token: storedRefreshToken };
2565
+ log("Synced refresh token from shared storage into memory");
2566
+ }
2567
+ }
2568
+ applyTokenClaims(decoded) {
2569
+ this.did = decoded.sub || null;
2570
+ this.tokenScope = decoded.scope || null;
2571
+ }
2572
+ broadcastSessionUpdate() {
2573
+ if (this.freshSignIn) {
2574
+ this.freshSignIn = false;
2575
+ this.broadcastSignIn();
2576
+ } else {
2577
+ this.broadcastTokenRefresh();
2578
+ }
2579
+ }
2580
+ async handleUserFetchFailure() {
2581
+ if (this.isCompatibleUser(this.user)) {
2582
+ log("Preserving existing user after userinfo failure");
2583
+ } else if (this.user) {
2584
+ log("Discarding stale in-memory user after userinfo failure");
2585
+ this.user = null;
2586
+ }
2587
+ if (!this.user) {
2588
+ const cached = await this.storage.get(STORAGE_KEYS.USER_INFO);
2589
+ if (cached) {
2590
+ try {
2591
+ const parsed = JSON.parse(cached);
2592
+ if (this.isCompatibleUser(parsed)) {
2593
+ this.user = parsed;
2594
+ log("Recovered cached user after userinfo failure");
2595
+ } else {
2596
+ log("Cached user did not match the active session");
2597
+ }
2598
+ } catch (error) {
2599
+ log("Failed to parse cached user after userinfo failure:", error);
2600
+ }
2601
+ }
2602
+ }
2603
+ if (!this.user) {
2604
+ log("No compatible cached user available after userinfo failure");
2605
+ this.nextUserRecoveryAt = Date.now() + USER_RECOVERY_RETRY_COOLDOWN_MS;
2606
+ } else {
2607
+ this.nextUserRecoveryAt = 0;
2608
+ }
2609
+ if (this.authStatus === "bootstrapping") {
2610
+ this.updateAuthStatus(this.token ? "authenticated" : "recovering");
2611
+ }
2612
+ this.notify();
2613
+ }
2614
+ isCompatibleUser(user) {
2615
+ if (!user) return false;
2616
+ if (!this.did) return true;
2617
+ return user.sub === this.did;
2618
+ }
2619
+ async recoverMissingUserProfile(reason, accessToken) {
2620
+ if (!this.isSignedIn || this.user) return;
2621
+ const now = Date.now();
2622
+ if (this.nextUserRecoveryAt > now) {
2623
+ log(
2624
+ `Skipping user profile recovery on ${reason} until ${new Date(this.nextUserRecoveryAt).toISOString()}`
2625
+ );
2626
+ return;
2627
+ }
2628
+ log(`Attempting user profile recovery on ${reason}`);
2629
+ const token = accessToken ?? await this.getToken();
2630
+ if (this.user) return;
2631
+ await this.fetchUser(token);
2632
+ }
2633
+ isDefinitiveTokenErrorCode(code) {
2634
+ return typeof code === "string" && DEFINITIVE_TOKEN_ERRORS.has(code);
2635
+ }
2636
+ isDefinitiveAuthFailure(error) {
2637
+ return error instanceof DefinitiveAuthError;
2638
+ }
2639
+ /**
2640
+ * Centralised auth status setter. Derives `isSignedIn` and `isAuthReady`
2641
+ * from the status so they stay consistent.
2642
+ *
2643
+ * `isSignedIn` is intentionally `true` during `reauth_required` so the
2644
+ * UI layer can still display user info while prompting re-authentication.
2645
+ * Consumers should check `authStatus` (or a future convenience getter)
2646
+ * when they need to distinguish "healthy session" from "needs re-auth".
2647
+ */
2648
+ updateAuthStatus(status, errorCode = null) {
2649
+ this.authStatus = status;
2650
+ this.authErrorCode = errorCode;
2651
+ this.isSignedIn = status === "authenticated" || status === "recovering" || status === "reauth_required";
2652
+ this.isAuthReady = status !== "bootstrapping";
2653
+ }
2654
+ async clearStoredSessionTokens() {
2655
+ await this.storage.remove(STORAGE_KEYS.REFRESH_TOKEN);
2656
+ await this.storage.remove(STORAGE_KEYS.REDIRECT_URI);
2657
+ await this.storage.remove(STORAGE_KEYS.CODE_VERIFIER);
2658
+ }
2659
+ async restoreStoredSession(reason) {
2660
+ const refreshToken = await this.getRefreshToken();
2661
+ if (!refreshToken) {
2662
+ log(`No stored refresh token available during ${reason}`);
2663
+ await this.restoreCachedUser({ hasRecoverableSession: false });
2664
+ return;
2665
+ }
2666
+ log(`Restoring stored session during ${reason}`);
2667
+ await this.restoreCachedUser({ hasRecoverableSession: true });
2668
+ if (!this.isOnline) {
2669
+ return;
2670
+ }
2671
+ this.exchangeToken(refreshToken, true).catch(async (error) => {
2672
+ log(`Stored session refresh failed during ${reason}:`, error);
2673
+ if (this.isDefinitiveAuthFailure(error)) {
2674
+ return;
2675
+ }
2676
+ await this.restoreCachedUser({ hasRecoverableSession: true });
2677
+ });
2678
+ }
2679
+ async handleExternalTokenRefresh(data) {
2680
+ await this.syncRefreshTokenFromStorage();
2681
+ const refreshToken = await this.getRefreshToken();
2682
+ if (data.accessToken && refreshToken) {
2683
+ try {
2684
+ const decoded = (0, import_jwt_decode.jwtDecode)(data.accessToken);
2685
+ const expiresIn = decoded.exp != null ? Math.max(0, decoded.exp - Math.floor(Date.now() / 1e3)) : 0;
2686
+ this.token = {
2687
+ access_token: data.accessToken,
2688
+ token_type: "Bearer",
2689
+ expires_in: expiresIn,
2690
+ refresh_token: refreshToken
2691
+ };
2692
+ this.applyTokenClaims(decoded);
2693
+ } catch (error) {
2694
+ log("Failed to decode token refreshed by another tab:", error);
2695
+ this.token = {
2696
+ access_token: data.accessToken,
2697
+ token_type: "Bearer",
2698
+ expires_in: 0,
2699
+ refresh_token: refreshToken
2700
+ };
2701
+ }
2702
+ if (this.authStatus !== "reauth_required") {
2703
+ this.updateAuthStatus("authenticated");
2704
+ }
2705
+ } else if (refreshToken) {
2706
+ await this.restoreCachedUser({ hasRecoverableSession: true });
2707
+ }
2708
+ if (data.did) this.did = data.did;
2709
+ if (data.tokenScope) this.tokenScope = data.tokenScope;
2710
+ this.notify();
2711
+ }
2712
+ async fetchCurrentSession(accessToken) {
2713
+ const endpoints = await this.getActivePdsEndpoints();
2714
+ const response = await fetch(`${endpoints.pds_url}/auth/session`, {
2715
+ method: "GET",
2716
+ headers: { Authorization: `Bearer ${accessToken}` }
2717
+ });
2718
+ const data = await response.json().catch(() => ({}));
2719
+ if (response.status === 401 && data.reauth_required) {
2720
+ await this.markReauthRequired(data.error || "invalid_session");
2721
+ throw new DefinitiveAuthError(data.error || "invalid_session");
2722
+ }
2723
+ if (!response.ok) {
2724
+ throw new Error(`Failed to reconcile session: ${response.status}`);
2725
+ }
2726
+ return data;
2727
+ }
2728
+ async markReauthRequired(code, options) {
2729
+ log("Marking auth session as requiring reauthentication:", code);
2730
+ await this.clearStoredSessionTokens();
2731
+ if (!this.user) {
2732
+ const cached = await this.storage.get(STORAGE_KEYS.USER_INFO);
2733
+ if (cached) {
2734
+ try {
2735
+ this.user = JSON.parse(cached);
2736
+ } catch {
2737
+ }
2738
+ }
2739
+ }
2740
+ this.token = null;
2741
+ this.tokenScope = null;
2742
+ this.nextUserRecoveryAt = 0;
2743
+ this.updateAuthStatus("reauth_required", code);
2744
+ if (options?.broadcast !== false) {
2745
+ this.broadcastSessionInvalidated(code);
2746
+ }
2747
+ this.notify();
2748
+ }
2396
2749
  };
2397
2750
 
2398
2751
  // src/AuthContext.tsx
@@ -2641,6 +2994,8 @@ function snapshotAuth(mgr) {
2641
2994
  isSignedIn: mgr.isSignedIn,
2642
2995
  hasToken: !!mgr.token,
2643
2996
  isAuthReady: mgr.isAuthReady,
2997
+ authStatus: mgr.authStatus,
2998
+ authErrorCode: mgr.authErrorCode,
2644
2999
  user: mgr.user,
2645
3000
  did: mgr.did,
2646
3001
  tokenScope: mgr.tokenScope
@@ -2675,6 +3030,8 @@ function BasicProvider({
2675
3030
  isSignedIn: false,
2676
3031
  hasToken: false,
2677
3032
  isAuthReady: false,
3033
+ authStatus: "bootstrapping",
3034
+ authErrorCode: null,
2678
3035
  user: null,
2679
3036
  did: null,
2680
3037
  tokenScope: null
@@ -2697,9 +3054,11 @@ function BasicProvider({
2697
3054
  const remoteDbRef = (0, import_react3.useRef)(null);
2698
3055
  const [shouldConnect, setShouldConnect] = (0, import_react3.useState)(false);
2699
3056
  const [dbStatus, setDbStatus] = (0, import_react3.useState)("OFFLINE" /* OFFLINE */);
2700
- const [isReady, setIsReady] = (0, import_react3.useState)(false);
3057
+ const [isDbReady, setIsDbReady] = (0, import_react3.useState)(false);
2701
3058
  const [error, setError] = (0, import_react3.useState)(null);
2702
- const [schemaDevInfo, setSchemaDevInfo] = (0, import_react3.useState)(null);
3059
+ const [schemaDevInfo, setSchemaDevInfo] = (0, import_react3.useState)(
3060
+ null
3061
+ );
2703
3062
  const isDevMode = () => isDevelopment(debug);
2704
3063
  const refreshSchemaStatus = (0, import_react3.useCallback)(async () => {
2705
3064
  const s = schemaRef.current;
@@ -2739,10 +3098,16 @@ function BasicProvider({
2739
3098
  (0, import_react3.useEffect)(() => {
2740
3099
  const runVersionUpdater = async () => {
2741
3100
  try {
2742
- const versionUpdater = createVersionUpdater(storageAdapter, version, getMigrations());
3101
+ const versionUpdater = createVersionUpdater(
3102
+ storageAdapter,
3103
+ version,
3104
+ getMigrations()
3105
+ );
2743
3106
  const updateResult = await versionUpdater.checkAndUpdate();
2744
3107
  if (updateResult.updated) {
2745
- log(`App updated from ${updateResult.fromVersion} to ${updateResult.toVersion}`);
3108
+ log(
3109
+ `App updated from ${updateResult.fromVersion} to ${updateResult.toVersion}`
3110
+ );
2746
3111
  } else {
2747
3112
  log(`App version ${updateResult.toVersion} is current`);
2748
3113
  }
@@ -2764,8 +3129,13 @@ function BasicProvider({
2764
3129
  const newStatus = getSyncStatus(status);
2765
3130
  setDbStatus(newStatus);
2766
3131
  if (newStatus === "ERROR_WILL_RETRY" /* ERROR_WILL_RETRY */) {
2767
- log("Sync entered ERROR_WILL_RETRY - proactively refreshing token");
2768
- authRef.current.getToken({ forceRefresh: true }).catch(() => {
3132
+ log(
3133
+ "Sync entered ERROR_WILL_RETRY - reconciling auth session before retry"
3134
+ );
3135
+ authRef.current.reconcileSession("sync retry", {
3136
+ forceRefresh: true,
3137
+ throttleMs: 0
3138
+ }).catch(() => {
2769
3139
  });
2770
3140
  }
2771
3141
  });
@@ -2774,7 +3144,7 @@ function BasicProvider({
2774
3144
  } else {
2775
3145
  log("Sync is disabled");
2776
3146
  }
2777
- setIsReady(true);
3147
+ setIsDbReady(true);
2778
3148
  }
2779
3149
  }
2780
3150
  function initRemoteDb() {
@@ -2785,7 +3155,7 @@ function BasicProvider({
2785
3155
  title: "Project ID Required",
2786
3156
  message: "Remote mode requires a project_id. Provide it via schema.project_id or the project_id prop."
2787
3157
  });
2788
- setIsReady(true);
3158
+ setIsDbReady(true);
2789
3159
  return;
2790
3160
  }
2791
3161
  log("Initializing Basic Remote DB");
@@ -2801,11 +3171,16 @@ function BasicProvider({
2801
3171
  log("403 Forbidden - user lacks required scope, not signing out");
2802
3172
  return;
2803
3173
  }
2804
- handleSignOut();
3174
+ authRef.current.reconcileSession(`remote db ${error2.errorType}`, {
3175
+ forceRefresh: error2.errorType !== "network",
3176
+ throttleMs: 0
3177
+ }).catch((reconcileError) => {
3178
+ log("RemoteDB auth recovery failed:", reconcileError);
3179
+ });
2805
3180
  }
2806
3181
  });
2807
3182
  setDbStatus("ONLINE" /* ONLINE */);
2808
- setIsReady(true);
3183
+ setIsDbReady(true);
2809
3184
  }
2810
3185
  }
2811
3186
  async function checkSchema() {
@@ -2831,7 +3206,7 @@ function BasicProvider({
2831
3206
  title: "Basic Schema is invalid!",
2832
3207
  message: errorMessage
2833
3208
  });
2834
- setIsReady(true);
3209
+ setIsDbReady(true);
2835
3210
  return null;
2836
3211
  }
2837
3212
  setSchemaDevInfo({
@@ -2848,7 +3223,9 @@ function BasicProvider({
2848
3223
  await initSyncDb({ shouldConnect: true });
2849
3224
  } else {
2850
3225
  if (result.schemaStatus.status === "unpublished") {
2851
- log("Schema not published yet (version 0) - sync is disabled. Publish your schema to enable sync.");
3226
+ log(
3227
+ "Schema not published yet (version 0) - sync is disabled. Publish your schema to enable sync."
3228
+ );
2852
3229
  } else {
2853
3230
  log("Schema is invalid!", result.schemaStatus);
2854
3231
  }
@@ -2872,12 +3249,12 @@ function BasicProvider({
2872
3249
  if (dbMode === "remote" && project_id) {
2873
3250
  initRemoteDb();
2874
3251
  } else {
2875
- setIsReady(true);
3252
+ setIsDbReady(true);
2876
3253
  }
2877
3254
  }
2878
3255
  }, []);
2879
3256
  (0, import_react3.useEffect)(() => {
2880
- if (authState.hasToken && syncRef.current && authState.isSignedIn && shouldConnect) {
3257
+ if (authState.hasToken && syncRef.current && authState.isSignedIn && authState.authStatus !== "reauth_required" && shouldConnect) {
2881
3258
  log("connecting to db...");
2882
3259
  syncRef.current?.connect({
2883
3260
  getToken: (opts) => authRef.current.getToken(opts),
@@ -2886,7 +3263,22 @@ function BasicProvider({
2886
3263
  log("error connecting to db", e);
2887
3264
  });
2888
3265
  }
2889
- }, [authState.isSignedIn, authState.hasToken, shouldConnect]);
3266
+ }, [
3267
+ authState.authStatus,
3268
+ authState.isSignedIn,
3269
+ authState.hasToken,
3270
+ shouldConnect
3271
+ ]);
3272
+ (0, import_react3.useEffect)(() => {
3273
+ if (authState.authStatus !== "reauth_required" || !syncRef.current) {
3274
+ return;
3275
+ }
3276
+ log("Auth requires reauthentication - disconnecting sync without deleting local DB");
3277
+ setDbStatus("ERROR_TOKEN_EXPIRED" /* ERROR_TOKEN_EXPIRED */);
3278
+ syncRef.current.disconnect({ ws_url: authConfig.ws_url }).catch((disconnectError) => {
3279
+ log("Error disconnecting sync after auth invalidation:", disconnectError);
3280
+ });
3281
+ }, [authConfig.ws_url, authState.authStatus]);
2890
3282
  const handleSignOut = async () => {
2891
3283
  await authRef.current.signOut();
2892
3284
  if (syncRef.current) {
@@ -2894,11 +3286,13 @@ function BasicProvider({
2894
3286
  await syncRef.current.close();
2895
3287
  await syncRef.current.delete({ disableAutoOpen: false });
2896
3288
  syncRef.current = null;
2897
- window?.location?.reload();
2898
3289
  } catch (error2) {
2899
3290
  console.error("Error during database cleanup:", error2);
2900
3291
  }
2901
3292
  }
3293
+ if (typeof window !== "undefined") {
3294
+ window.location.reload();
3295
+ }
2902
3296
  };
2903
3297
  const handleSignIn = async () => {
2904
3298
  try {
@@ -2937,6 +3331,8 @@ function BasicProvider({
2937
3331
  const contextValue = {
2938
3332
  isReady: authState.isAuthReady,
2939
3333
  isSignedIn: authState.isSignedIn,
3334
+ authStatus: authState.authStatus,
3335
+ authErrorCode: authState.authErrorCode,
2940
3336
  user: authState.user,
2941
3337
  did: authState.did,
2942
3338
  scope: authState.tokenScope,
@@ -2962,7 +3358,7 @@ function BasicProvider({
2962
3358
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(BasicContext.Provider, { value: contextValue, children: [
2963
3359
  error && isDevMode() && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ErrorDisplay, { error }),
2964
3360
  devToolbar && isDevMode() && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react3.Suspense, { fallback: null, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(BasicDevToolbar2, { debug }) }),
2965
- isReady && children
3361
+ isDbReady && authState.isAuthReady && children
2966
3362
  ] });
2967
3363
  }
2968
3364
  function ErrorDisplay({ error }) {