@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/changelog.md +6 -0
- package/dist/index.d.mts +31 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +537 -141
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +545 -142
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/readme.md +99 -97
- package/src/AuthContext.tsx +513 -415
- package/src/context.tsx +19 -1
- package/src/core/auth/AuthManager.ts +1235 -746
- package/src/sync/syncProtocol.js +23 -7
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 (
|
|
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
|
-
|
|
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.
|
|
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(
|
|
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
|
-
|
|
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,
|
|
1748
|
-
|
|
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
|
|
1754
|
-
this.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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(
|
|
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(
|
|
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)
|
|
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 =
|
|
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)
|
|
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)
|
|
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(
|
|
1939
|
-
|
|
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:", {
|
|
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
|
|
2166
|
+
if (this.pendingRefresh) {
|
|
2069
2167
|
log("Retrying pending token refresh");
|
|
2070
2168
|
this.pendingRefresh = false;
|
|
2071
|
-
const refreshToken =
|
|
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 -
|
|
2086
|
-
this.
|
|
2087
|
-
|
|
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(
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
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.
|
|
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
|
-
|
|
2163
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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: {
|
|
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(
|
|
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.
|
|
2207
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
2450
|
+
throw new Error(
|
|
2451
|
+
"Network issue - refresh will be retried when online"
|
|
2452
|
+
);
|
|
2317
2453
|
}
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
2345
|
-
|
|
2346
|
-
if (
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
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
|
-
|
|
2518
|
+
if (status !== "reauth_required") {
|
|
2519
|
+
this.did = null;
|
|
2520
|
+
}
|
|
2378
2521
|
this.tokenScope = null;
|
|
2379
|
-
this.
|
|
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 [
|
|
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)(
|
|
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(
|
|
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(
|
|
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(
|
|
2768
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
}, [
|
|
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
|
-
|
|
3361
|
+
isDbReady && authState.isAuthReady && children
|
|
2966
3362
|
] });
|
|
2967
3363
|
}
|
|
2968
3364
|
function ErrorDisplay({ error }) {
|