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

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/changelog.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # 1.3.4
2
2
 
3
+ ## 0.8.0-beta.3
4
+
5
+ ### Patch Changes
6
+
7
+ - Fix PWA/mobile logout issues: harden auth for offline resilience, prevent spurious logouts on transient server errors, proactive token refresh on app resume, and fix potential deadlock in token refresh flow
8
+
3
9
  ## 0.8.0-beta.2
4
10
 
5
11
  ### Minor Changes
package/dist/index.js CHANGED
@@ -159,14 +159,37 @@ var init_syncProtocol = __esm({
159
159
  onError("Authentication failed: " + (err.message || err), RECONNECT_DELAY);
160
160
  }
161
161
  };
162
+ function handleVisibilityResume() {
163
+ if (document.visibilityState === "visible" && ws.readyState === WebSocket.OPEN) {
164
+ log("Page became visible - refreshing token for WebSocket");
165
+ 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
+ }
170
+ }).catch(function(err) {
171
+ log("Token refresh on visibility resume failed:", err);
172
+ });
173
+ }
174
+ }
175
+ if (typeof document !== "undefined") {
176
+ document.addEventListener("visibilitychange", handleVisibilityResume);
177
+ }
178
+ function cleanupVisibilityListener() {
179
+ if (typeof document !== "undefined") {
180
+ document.removeEventListener("visibilitychange", handleVisibilityResume);
181
+ }
182
+ }
162
183
  ws.onerror = function(event) {
163
184
  clearRefreshTimer();
185
+ cleanupVisibilityListener();
164
186
  ws.close();
165
187
  log("ws.onerror", event);
166
188
  onError(event?.message, RECONNECT_DELAY);
167
189
  };
168
190
  ws.onclose = function(event) {
169
191
  clearRefreshTimer();
192
+ cleanupVisibilityListener();
170
193
  onError("Socket closed: " + event.reason, RECONNECT_DELAY);
171
194
  };
172
195
  var isFirstRound = true;
@@ -203,6 +226,7 @@ var init_syncProtocol = __esm({
203
226
  },
204
227
  disconnect: function() {
205
228
  clearRefreshTimer();
229
+ cleanupVisibilityListener();
206
230
  ws.close();
207
231
  }
208
232
  });
@@ -242,7 +266,7 @@ var init_syncProtocol = __esm({
242
266
  var version;
243
267
  var init_package = __esm({
244
268
  "package.json"() {
245
- version = "0.8.0-beta.2";
269
+ version = "0.8.0-beta.3";
246
270
  }
247
271
  });
248
272
 
@@ -326,7 +350,7 @@ function cleanOAuthParamsFromUrl() {
326
350
  const url = new URL(window.location.href);
327
351
  url.searchParams.delete("code");
328
352
  url.searchParams.delete("state");
329
- window.history.pushState({}, document.title, url.pathname + url.search);
353
+ window.history.replaceState({}, document.title, url.pathname + url.search);
330
354
  log("Cleaned OAuth parameters from URL");
331
355
  }
332
356
  }
@@ -1800,8 +1824,11 @@ var AuthManager = class {
1800
1824
  const refreshToken = await this.storage.get(STORAGE_KEYS.REFRESH_TOKEN);
1801
1825
  if (refreshToken) {
1802
1826
  log("Found refresh token in storage, attempting to refresh access token");
1803
- this.exchangeToken(refreshToken, true).catch((error) => {
1827
+ this.exchangeToken(refreshToken, true).catch(async (error) => {
1804
1828
  log("Error fetching refresh token:", error);
1829
+ if (this.isNetworkError(error)) {
1830
+ await this.restoreCachedUser();
1831
+ }
1805
1832
  });
1806
1833
  } else {
1807
1834
  const cachedUserInfo = await this.storage.get(STORAGE_KEYS.USER_INFO);
@@ -1870,7 +1897,8 @@ var AuthManager = class {
1870
1897
  log("Token refresh already in progress, waiting...");
1871
1898
  try {
1872
1899
  const newToken = await this.refreshPromise;
1873
- return newToken?.access_token || "";
1900
+ if (!newToken?.access_token) throw new Error("Token refresh returned empty access token");
1901
+ return newToken.access_token;
1874
1902
  } catch (error) {
1875
1903
  log("In-flight refresh failed:", error);
1876
1904
  if (this.isNetworkError(error)) {
@@ -1884,7 +1912,8 @@ var AuthManager = class {
1884
1912
  if (refreshToken) {
1885
1913
  try {
1886
1914
  const newToken = await this.exchangeToken(refreshToken, true);
1887
- return newToken?.access_token || "";
1915
+ if (!newToken?.access_token) throw new Error("Token refresh returned empty access token");
1916
+ return newToken.access_token;
1888
1917
  } catch (error) {
1889
1918
  log("Failed to refresh expired token:", error);
1890
1919
  if (this.isNetworkError(error)) {
@@ -1897,7 +1926,8 @@ var AuthManager = class {
1897
1926
  throw new Error("no refresh token available");
1898
1927
  }
1899
1928
  }
1900
- return this.token.access_token || "";
1929
+ if (!this.token.access_token) throw new Error("Token exists but access_token is empty");
1930
+ return this.token.access_token;
1901
1931
  }
1902
1932
  async getSignInUrl(redirectUri, endpoints) {
1903
1933
  log("getting sign in link...");
@@ -1906,7 +1936,7 @@ var AuthManager = class {
1906
1936
  }
1907
1937
  const pdsEndpoints = endpoints || this.defaultPdsEndpoints();
1908
1938
  await this.storage.set(STORAGE_KEYS.PDS_ENDPOINTS, JSON.stringify(pdsEndpoints));
1909
- const randomState = Math.random().toString(36).substring(6);
1939
+ const randomState = base64UrlEncode(crypto.getRandomValues(new Uint8Array(16)));
1910
1940
  await this.storage.set(STORAGE_KEYS.AUTH_STATE, randomState);
1911
1941
  const redirectUrl = redirectUri || window.location.href;
1912
1942
  if (!redirectUrl || !redirectUrl.startsWith("http://") && !redirectUrl.startsWith("https://")) {
@@ -2025,7 +2055,10 @@ var AuthManager = class {
2025
2055
  return requested.filter((s) => !granted.has(s));
2026
2056
  }
2027
2057
  /**
2028
- * Register online/offline handlers that retry pending refreshes.
2058
+ * Register online/offline and visibility handlers that retry pending
2059
+ * refreshes and proactively refresh tokens when the app resumes from
2060
+ * background (critical for PWAs and mobile browsers where timers are
2061
+ * frozen while backgrounded).
2029
2062
  * Returns a cleanup function for useEffect teardown.
2030
2063
  */
2031
2064
  setupNetworkListeners() {
@@ -2047,11 +2080,25 @@ var AuthManager = class {
2047
2080
  log("Network went offline");
2048
2081
  this.isOnline = false;
2049
2082
  };
2083
+ const handleVisibilityChange = () => {
2084
+ 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);
2088
+ });
2089
+ }
2090
+ };
2050
2091
  window.addEventListener("online", handleOnline);
2051
2092
  window.addEventListener("offline", handleOffline);
2093
+ if (typeof document !== "undefined") {
2094
+ document.addEventListener("visibilitychange", handleVisibilityChange);
2095
+ }
2052
2096
  return () => {
2053
2097
  window.removeEventListener("online", handleOnline);
2054
2098
  window.removeEventListener("offline", handleOffline);
2099
+ if (typeof document !== "undefined") {
2100
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
2101
+ }
2055
2102
  };
2056
2103
  }
2057
2104
  // ------------------------------------------------------------------
@@ -2114,39 +2161,26 @@ var AuthManager = class {
2114
2161
  const decoded = (0, import_jwt_decode.jwtDecode)(this.token.access_token);
2115
2162
  if (decoded.sub) this.did = decoded.sub;
2116
2163
  if (decoded.scope) this.tokenScope = decoded.scope;
2117
- const expirationBuffer = 5;
2118
- const isExpired = decoded.exp && decoded.exp < Date.now() / 1e3 + expirationBuffer;
2119
- if (isExpired) {
2120
- log("token is expired - refreshing ...");
2121
- const refreshToken = this.token.refresh_token;
2122
- if (!refreshToken) {
2123
- log("Error: No refresh token available for expired token");
2124
- this.isAuthReady = true;
2125
- this.notify();
2126
- return;
2127
- }
2128
- try {
2129
- const newToken = await this.exchangeToken(refreshToken, true);
2130
- await this.fetchUser(newToken?.access_token || "");
2131
- } catch (error) {
2132
- log("Failed to refresh token in processNewToken:", error);
2133
- if (this.isNetworkError(error)) {
2134
- log("Network issue - continuing with expired token until online");
2135
- await this.fetchUser(this.token.access_token);
2136
- } else {
2137
- this.isAuthReady = true;
2138
- this.notify();
2139
- }
2140
- }
2141
- } else {
2142
- await this.fetchUser(this.token.access_token);
2143
- }
2164
+ await this.fetchUser(this.token.access_token);
2144
2165
  } catch (error) {
2145
2166
  log("Error processing token:", error);
2146
2167
  this.isAuthReady = true;
2147
2168
  this.notify();
2148
2169
  }
2149
2170
  }
2171
+ async restoreCachedUser() {
2172
+ const cached = await this.storage.get(STORAGE_KEYS.USER_INFO);
2173
+ if (cached) {
2174
+ try {
2175
+ this.user = JSON.parse(cached);
2176
+ this.isSignedIn = true;
2177
+ log("Restored cached user info for offline mode");
2178
+ } catch {
2179
+ }
2180
+ }
2181
+ this.isAuthReady = true;
2182
+ this.notify();
2183
+ }
2150
2184
  async fetchUser(accessToken) {
2151
2185
  log("fetching user");
2152
2186
  try {
@@ -2180,8 +2214,12 @@ var AuthManager = class {
2180
2214
  this.notify();
2181
2215
  } catch (error) {
2182
2216
  log("Failed to fetch user info:", error);
2183
- this.isAuthReady = true;
2184
- this.notify();
2217
+ if (this.isNetworkError(error)) {
2218
+ await this.restoreCachedUser();
2219
+ } else {
2220
+ this.isAuthReady = true;
2221
+ this.notify();
2222
+ }
2185
2223
  }
2186
2224
  }
2187
2225
  /**
@@ -2277,9 +2315,12 @@ var AuthManager = class {
2277
2315
  this.pendingRefresh = true;
2278
2316
  throw new Error("Network issue - refresh will be retried when online");
2279
2317
  }
2280
- await this.clearStoredAuth();
2281
- this.resetAuthState();
2282
- this.notify();
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();
2323
+ }
2283
2324
  throw new Error(`Token refresh failed: ${token.error}`);
2284
2325
  } else {
2285
2326
  this.token = token;
@@ -2300,7 +2341,9 @@ var AuthManager = class {
2300
2341
  return token;
2301
2342
  } catch (error) {
2302
2343
  log("Token refresh error:", error);
2303
- if (!this.isNetworkError(error)) {
2344
+ const msg = error instanceof Error ? error.message : "";
2345
+ const alreadyHandled = msg.startsWith("Token refresh failed:");
2346
+ if (!alreadyHandled && !this.isNetworkError(error)) {
2304
2347
  await this.clearStoredAuth();
2305
2348
  this.resetAuthState();
2306
2349
  this.notify();
@@ -2344,6 +2387,7 @@ var AuthManager = class {
2344
2387
  await this.storage.remove(STORAGE_KEYS.PDS_ENDPOINTS);
2345
2388
  }
2346
2389
  isNetworkError(error) {
2390
+ if (error instanceof TypeError) return true;
2347
2391
  if (error instanceof Error) {
2348
2392
  return error.message.includes("offline") || error.message.includes("Network");
2349
2393
  }
@@ -2753,6 +2797,10 @@ function BasicProvider({
2753
2797
  debug,
2754
2798
  onAuthError: (error2) => {
2755
2799
  log("RemoteDB auth error:", error2);
2800
+ if (error2.errorType === "forbidden") {
2801
+ log("403 Forbidden - user lacks required scope, not signing out");
2802
+ return;
2803
+ }
2756
2804
  handleSignOut();
2757
2805
  }
2758
2806
  });