@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/dist/index.mjs
CHANGED
|
@@ -137,14 +137,37 @@ var init_syncProtocol = __esm({
|
|
|
137
137
|
onError("Authentication failed: " + (err.message || err), RECONNECT_DELAY);
|
|
138
138
|
}
|
|
139
139
|
};
|
|
140
|
+
function handleVisibilityResume() {
|
|
141
|
+
if (document.visibilityState === "visible" && ws.readyState === WebSocket.OPEN) {
|
|
142
|
+
log("Page became visible - refreshing token for WebSocket");
|
|
143
|
+
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
|
+
}
|
|
148
|
+
}).catch(function(err) {
|
|
149
|
+
log("Token refresh on visibility resume failed:", err);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (typeof document !== "undefined") {
|
|
154
|
+
document.addEventListener("visibilitychange", handleVisibilityResume);
|
|
155
|
+
}
|
|
156
|
+
function cleanupVisibilityListener() {
|
|
157
|
+
if (typeof document !== "undefined") {
|
|
158
|
+
document.removeEventListener("visibilitychange", handleVisibilityResume);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
140
161
|
ws.onerror = function(event) {
|
|
141
162
|
clearRefreshTimer();
|
|
163
|
+
cleanupVisibilityListener();
|
|
142
164
|
ws.close();
|
|
143
165
|
log("ws.onerror", event);
|
|
144
166
|
onError(event?.message, RECONNECT_DELAY);
|
|
145
167
|
};
|
|
146
168
|
ws.onclose = function(event) {
|
|
147
169
|
clearRefreshTimer();
|
|
170
|
+
cleanupVisibilityListener();
|
|
148
171
|
onError("Socket closed: " + event.reason, RECONNECT_DELAY);
|
|
149
172
|
};
|
|
150
173
|
var isFirstRound = true;
|
|
@@ -181,6 +204,7 @@ var init_syncProtocol = __esm({
|
|
|
181
204
|
},
|
|
182
205
|
disconnect: function() {
|
|
183
206
|
clearRefreshTimer();
|
|
207
|
+
cleanupVisibilityListener();
|
|
184
208
|
ws.close();
|
|
185
209
|
}
|
|
186
210
|
});
|
|
@@ -220,7 +244,7 @@ var init_syncProtocol = __esm({
|
|
|
220
244
|
var version;
|
|
221
245
|
var init_package = __esm({
|
|
222
246
|
"package.json"() {
|
|
223
|
-
version = "0.8.0-beta.
|
|
247
|
+
version = "0.8.0-beta.3";
|
|
224
248
|
}
|
|
225
249
|
});
|
|
226
250
|
|
|
@@ -305,7 +329,7 @@ function cleanOAuthParamsFromUrl() {
|
|
|
305
329
|
const url = new URL(window.location.href);
|
|
306
330
|
url.searchParams.delete("code");
|
|
307
331
|
url.searchParams.delete("state");
|
|
308
|
-
window.history.
|
|
332
|
+
window.history.replaceState({}, document.title, url.pathname + url.search);
|
|
309
333
|
log("Cleaned OAuth parameters from URL");
|
|
310
334
|
}
|
|
311
335
|
}
|
|
@@ -1758,8 +1782,11 @@ var AuthManager = class {
|
|
|
1758
1782
|
const refreshToken = await this.storage.get(STORAGE_KEYS.REFRESH_TOKEN);
|
|
1759
1783
|
if (refreshToken) {
|
|
1760
1784
|
log("Found refresh token in storage, attempting to refresh access token");
|
|
1761
|
-
this.exchangeToken(refreshToken, true).catch((error) => {
|
|
1785
|
+
this.exchangeToken(refreshToken, true).catch(async (error) => {
|
|
1762
1786
|
log("Error fetching refresh token:", error);
|
|
1787
|
+
if (this.isNetworkError(error)) {
|
|
1788
|
+
await this.restoreCachedUser();
|
|
1789
|
+
}
|
|
1763
1790
|
});
|
|
1764
1791
|
} else {
|
|
1765
1792
|
const cachedUserInfo = await this.storage.get(STORAGE_KEYS.USER_INFO);
|
|
@@ -1828,7 +1855,8 @@ var AuthManager = class {
|
|
|
1828
1855
|
log("Token refresh already in progress, waiting...");
|
|
1829
1856
|
try {
|
|
1830
1857
|
const newToken = await this.refreshPromise;
|
|
1831
|
-
|
|
1858
|
+
if (!newToken?.access_token) throw new Error("Token refresh returned empty access token");
|
|
1859
|
+
return newToken.access_token;
|
|
1832
1860
|
} catch (error) {
|
|
1833
1861
|
log("In-flight refresh failed:", error);
|
|
1834
1862
|
if (this.isNetworkError(error)) {
|
|
@@ -1842,7 +1870,8 @@ var AuthManager = class {
|
|
|
1842
1870
|
if (refreshToken) {
|
|
1843
1871
|
try {
|
|
1844
1872
|
const newToken = await this.exchangeToken(refreshToken, true);
|
|
1845
|
-
|
|
1873
|
+
if (!newToken?.access_token) throw new Error("Token refresh returned empty access token");
|
|
1874
|
+
return newToken.access_token;
|
|
1846
1875
|
} catch (error) {
|
|
1847
1876
|
log("Failed to refresh expired token:", error);
|
|
1848
1877
|
if (this.isNetworkError(error)) {
|
|
@@ -1855,7 +1884,8 @@ var AuthManager = class {
|
|
|
1855
1884
|
throw new Error("no refresh token available");
|
|
1856
1885
|
}
|
|
1857
1886
|
}
|
|
1858
|
-
|
|
1887
|
+
if (!this.token.access_token) throw new Error("Token exists but access_token is empty");
|
|
1888
|
+
return this.token.access_token;
|
|
1859
1889
|
}
|
|
1860
1890
|
async getSignInUrl(redirectUri, endpoints) {
|
|
1861
1891
|
log("getting sign in link...");
|
|
@@ -1864,7 +1894,7 @@ var AuthManager = class {
|
|
|
1864
1894
|
}
|
|
1865
1895
|
const pdsEndpoints = endpoints || this.defaultPdsEndpoints();
|
|
1866
1896
|
await this.storage.set(STORAGE_KEYS.PDS_ENDPOINTS, JSON.stringify(pdsEndpoints));
|
|
1867
|
-
const randomState =
|
|
1897
|
+
const randomState = base64UrlEncode(crypto.getRandomValues(new Uint8Array(16)));
|
|
1868
1898
|
await this.storage.set(STORAGE_KEYS.AUTH_STATE, randomState);
|
|
1869
1899
|
const redirectUrl = redirectUri || window.location.href;
|
|
1870
1900
|
if (!redirectUrl || !redirectUrl.startsWith("http://") && !redirectUrl.startsWith("https://")) {
|
|
@@ -1983,7 +2013,10 @@ var AuthManager = class {
|
|
|
1983
2013
|
return requested.filter((s) => !granted.has(s));
|
|
1984
2014
|
}
|
|
1985
2015
|
/**
|
|
1986
|
-
* Register online/offline handlers that retry pending
|
|
2016
|
+
* Register online/offline and visibility handlers that retry pending
|
|
2017
|
+
* refreshes and proactively refresh tokens when the app resumes from
|
|
2018
|
+
* background (critical for PWAs and mobile browsers where timers are
|
|
2019
|
+
* frozen while backgrounded).
|
|
1987
2020
|
* Returns a cleanup function for useEffect teardown.
|
|
1988
2021
|
*/
|
|
1989
2022
|
setupNetworkListeners() {
|
|
@@ -2005,11 +2038,25 @@ var AuthManager = class {
|
|
|
2005
2038
|
log("Network went offline");
|
|
2006
2039
|
this.isOnline = false;
|
|
2007
2040
|
};
|
|
2041
|
+
const handleVisibilityChange = () => {
|
|
2042
|
+
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);
|
|
2046
|
+
});
|
|
2047
|
+
}
|
|
2048
|
+
};
|
|
2008
2049
|
window.addEventListener("online", handleOnline);
|
|
2009
2050
|
window.addEventListener("offline", handleOffline);
|
|
2051
|
+
if (typeof document !== "undefined") {
|
|
2052
|
+
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
2053
|
+
}
|
|
2010
2054
|
return () => {
|
|
2011
2055
|
window.removeEventListener("online", handleOnline);
|
|
2012
2056
|
window.removeEventListener("offline", handleOffline);
|
|
2057
|
+
if (typeof document !== "undefined") {
|
|
2058
|
+
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
2059
|
+
}
|
|
2013
2060
|
};
|
|
2014
2061
|
}
|
|
2015
2062
|
// ------------------------------------------------------------------
|
|
@@ -2072,39 +2119,26 @@ var AuthManager = class {
|
|
|
2072
2119
|
const decoded = jwtDecode(this.token.access_token);
|
|
2073
2120
|
if (decoded.sub) this.did = decoded.sub;
|
|
2074
2121
|
if (decoded.scope) this.tokenScope = decoded.scope;
|
|
2075
|
-
|
|
2076
|
-
const isExpired = decoded.exp && decoded.exp < Date.now() / 1e3 + expirationBuffer;
|
|
2077
|
-
if (isExpired) {
|
|
2078
|
-
log("token is expired - refreshing ...");
|
|
2079
|
-
const refreshToken = this.token.refresh_token;
|
|
2080
|
-
if (!refreshToken) {
|
|
2081
|
-
log("Error: No refresh token available for expired token");
|
|
2082
|
-
this.isAuthReady = true;
|
|
2083
|
-
this.notify();
|
|
2084
|
-
return;
|
|
2085
|
-
}
|
|
2086
|
-
try {
|
|
2087
|
-
const newToken = await this.exchangeToken(refreshToken, true);
|
|
2088
|
-
await this.fetchUser(newToken?.access_token || "");
|
|
2089
|
-
} catch (error) {
|
|
2090
|
-
log("Failed to refresh token in processNewToken:", error);
|
|
2091
|
-
if (this.isNetworkError(error)) {
|
|
2092
|
-
log("Network issue - continuing with expired token until online");
|
|
2093
|
-
await this.fetchUser(this.token.access_token);
|
|
2094
|
-
} else {
|
|
2095
|
-
this.isAuthReady = true;
|
|
2096
|
-
this.notify();
|
|
2097
|
-
}
|
|
2098
|
-
}
|
|
2099
|
-
} else {
|
|
2100
|
-
await this.fetchUser(this.token.access_token);
|
|
2101
|
-
}
|
|
2122
|
+
await this.fetchUser(this.token.access_token);
|
|
2102
2123
|
} catch (error) {
|
|
2103
2124
|
log("Error processing token:", error);
|
|
2104
2125
|
this.isAuthReady = true;
|
|
2105
2126
|
this.notify();
|
|
2106
2127
|
}
|
|
2107
2128
|
}
|
|
2129
|
+
async restoreCachedUser() {
|
|
2130
|
+
const cached = await this.storage.get(STORAGE_KEYS.USER_INFO);
|
|
2131
|
+
if (cached) {
|
|
2132
|
+
try {
|
|
2133
|
+
this.user = JSON.parse(cached);
|
|
2134
|
+
this.isSignedIn = true;
|
|
2135
|
+
log("Restored cached user info for offline mode");
|
|
2136
|
+
} catch {
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
this.isAuthReady = true;
|
|
2140
|
+
this.notify();
|
|
2141
|
+
}
|
|
2108
2142
|
async fetchUser(accessToken) {
|
|
2109
2143
|
log("fetching user");
|
|
2110
2144
|
try {
|
|
@@ -2138,8 +2172,12 @@ var AuthManager = class {
|
|
|
2138
2172
|
this.notify();
|
|
2139
2173
|
} catch (error) {
|
|
2140
2174
|
log("Failed to fetch user info:", error);
|
|
2141
|
-
this.
|
|
2142
|
-
|
|
2175
|
+
if (this.isNetworkError(error)) {
|
|
2176
|
+
await this.restoreCachedUser();
|
|
2177
|
+
} else {
|
|
2178
|
+
this.isAuthReady = true;
|
|
2179
|
+
this.notify();
|
|
2180
|
+
}
|
|
2143
2181
|
}
|
|
2144
2182
|
}
|
|
2145
2183
|
/**
|
|
@@ -2235,9 +2273,12 @@ var AuthManager = class {
|
|
|
2235
2273
|
this.pendingRefresh = true;
|
|
2236
2274
|
throw new Error("Network issue - refresh will be retried when online");
|
|
2237
2275
|
}
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
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();
|
|
2281
|
+
}
|
|
2241
2282
|
throw new Error(`Token refresh failed: ${token.error}`);
|
|
2242
2283
|
} else {
|
|
2243
2284
|
this.token = token;
|
|
@@ -2258,7 +2299,9 @@ var AuthManager = class {
|
|
|
2258
2299
|
return token;
|
|
2259
2300
|
} catch (error) {
|
|
2260
2301
|
log("Token refresh error:", error);
|
|
2261
|
-
|
|
2302
|
+
const msg = error instanceof Error ? error.message : "";
|
|
2303
|
+
const alreadyHandled = msg.startsWith("Token refresh failed:");
|
|
2304
|
+
if (!alreadyHandled && !this.isNetworkError(error)) {
|
|
2262
2305
|
await this.clearStoredAuth();
|
|
2263
2306
|
this.resetAuthState();
|
|
2264
2307
|
this.notify();
|
|
@@ -2302,6 +2345,7 @@ var AuthManager = class {
|
|
|
2302
2345
|
await this.storage.remove(STORAGE_KEYS.PDS_ENDPOINTS);
|
|
2303
2346
|
}
|
|
2304
2347
|
isNetworkError(error) {
|
|
2348
|
+
if (error instanceof TypeError) return true;
|
|
2305
2349
|
if (error instanceof Error) {
|
|
2306
2350
|
return error.message.includes("offline") || error.message.includes("Network");
|
|
2307
2351
|
}
|
|
@@ -2711,6 +2755,10 @@ function BasicProvider({
|
|
|
2711
2755
|
debug,
|
|
2712
2756
|
onAuthError: (error2) => {
|
|
2713
2757
|
log("RemoteDB auth error:", error2);
|
|
2758
|
+
if (error2.errorType === "forbidden") {
|
|
2759
|
+
log("403 Forbidden - user lacks required scope, not signing out");
|
|
2760
|
+
return;
|
|
2761
|
+
}
|
|
2714
2762
|
handleSignOut();
|
|
2715
2763
|
}
|
|
2716
2764
|
});
|