@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 +6 -0
- package/dist/index.js +89 -41
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +89 -41
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/AuthContext.tsx +4 -0
- package/src/core/auth/AuthManager.ts +64 -40
- package/src/sync/syncProtocol.js +30 -0
- package/src/utils/network.ts +1 -1
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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.
|
|
2184
|
-
|
|
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
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
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
|
-
|
|
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
|
});
|