@oxyhq/services 10.2.2 → 10.2.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/lib/commonjs/ui/context/OxyContext.js +106 -9
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/hooks/useAuth.js +6 -0
- package/lib/commonjs/ui/hooks/useAuth.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +106 -9
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/hooks/useAuth.js +6 -0
- package/lib/module/ui/hooks/useAuth.js.map +1 -1
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts +3 -0
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useAuth.d.ts +13 -0
- package/lib/typescript/commonjs/ui/hooks/useAuth.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/OxyContext.d.ts +3 -0
- package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useAuth.d.ts +13 -0
- package/lib/typescript/module/ui/hooks/useAuth.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/ui/context/OxyContext.tsx +120 -8
- package/src/ui/hooks/useAuth.ts +22 -0
|
@@ -60,6 +60,9 @@ export interface OxyContextState {
|
|
|
60
60
|
isAuthenticated: boolean;
|
|
61
61
|
isLoading: boolean;
|
|
62
62
|
isTokenReady: boolean;
|
|
63
|
+
hasAccessToken: boolean;
|
|
64
|
+
canUsePrivateApi: boolean;
|
|
65
|
+
isPrivateApiPending: boolean;
|
|
63
66
|
/**
|
|
64
67
|
* Whether the initial auth determination has concluded.
|
|
65
68
|
*
|
|
@@ -251,6 +254,35 @@ const COOKIE_RESTORE_TIMEOUT = 3000;
|
|
|
251
254
|
*/
|
|
252
255
|
const COLD_BOOT_OVERALL_DEADLINE = 20000;
|
|
253
256
|
|
|
257
|
+
function getHttpStatus(error: unknown): number | undefined {
|
|
258
|
+
if (!error || typeof error !== 'object') {
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if ('status' in error) {
|
|
263
|
+
const status = (error as { status?: unknown }).status;
|
|
264
|
+
if (typeof status === 'number') {
|
|
265
|
+
return status;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if ('response' in error) {
|
|
270
|
+
const response = (error as { response?: unknown }).response;
|
|
271
|
+
if (response && typeof response === 'object' && 'status' in response) {
|
|
272
|
+
const status = (response as { status?: unknown }).status;
|
|
273
|
+
if (typeof status === 'number') {
|
|
274
|
+
return status;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return undefined;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function isUnauthorizedStatus(error: unknown): boolean {
|
|
283
|
+
return getHttpStatus(error) === 401;
|
|
284
|
+
}
|
|
285
|
+
|
|
254
286
|
/**
|
|
255
287
|
* Whether `idpOrigin` is a same-site, first-party host of the current page —
|
|
256
288
|
* i.e. it shares the page's registrable apex (last two labels), so a "no
|
|
@@ -273,7 +305,10 @@ function isSameSiteIdP(idpOrigin: string): boolean {
|
|
|
273
305
|
let idpHostname: string;
|
|
274
306
|
try {
|
|
275
307
|
idpHostname = new URL(idpOrigin).hostname;
|
|
276
|
-
} catch {
|
|
308
|
+
} catch (parseError) {
|
|
309
|
+
if (__DEV__) {
|
|
310
|
+
loggerUtil.debug('Invalid IdP origin while checking same-site session status', { component: 'OxyContext' }, parseError as unknown);
|
|
311
|
+
}
|
|
277
312
|
return false;
|
|
278
313
|
}
|
|
279
314
|
const pageHostname = window.location.hostname;
|
|
@@ -384,6 +419,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
384
419
|
);
|
|
385
420
|
|
|
386
421
|
const [tokenReady, setTokenReady] = useState(true);
|
|
422
|
+
const [hasAccessToken, setHasAccessToken] = useState(() => Boolean(oxyServices.getAccessToken()));
|
|
387
423
|
// Whether the FIRST cold-boot auth restore has concluded. Starts `false`
|
|
388
424
|
// (auth undetermined) and flips to `true` exactly once — monotonic, never
|
|
389
425
|
// reverts. It now flips the MOMENT a session commits (the common reload case
|
|
@@ -393,6 +429,10 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
393
429
|
// `isAuthResolved` on the context type for the consumer contract.
|
|
394
430
|
const [authResolved, setAuthResolved] = useState(false);
|
|
395
431
|
const authResolvedRef = useRef(false);
|
|
432
|
+
const userRef = useRef<User | null>(user);
|
|
433
|
+
const isAuthenticatedRef = useRef(isAuthenticated);
|
|
434
|
+
userRef.current = user;
|
|
435
|
+
isAuthenticatedRef.current = isAuthenticated;
|
|
396
436
|
const [initialized, setInitialized] = useState(false);
|
|
397
437
|
const [ssoCallbackIntercepting, setSsoCallbackIntercepting] = useState(false);
|
|
398
438
|
const setAuthState = useAuthStore.setState;
|
|
@@ -633,6 +673,43 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
633
673
|
updateSessionsRef.current = updateSessions;
|
|
634
674
|
const clearSessionStateRef = useRef(clearSessionState);
|
|
635
675
|
clearSessionStateRef.current = clearSessionState;
|
|
676
|
+
const clearingInvalidTokenRef = useRef(false);
|
|
677
|
+
|
|
678
|
+
useEffect(() => {
|
|
679
|
+
const handleTokenChange = (accessToken: string | null) => {
|
|
680
|
+
setHasAccessToken(Boolean(accessToken));
|
|
681
|
+
if (accessToken) {
|
|
682
|
+
setTokenReady(true);
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (userRef.current || isAuthenticatedRef.current) {
|
|
687
|
+
setTokenReady(false);
|
|
688
|
+
if (clearingInvalidTokenRef.current) {
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
clearingInvalidTokenRef.current = true;
|
|
692
|
+
clearSessionStateRef.current()
|
|
693
|
+
.catch((clearError) => {
|
|
694
|
+
logger('Failed to clear invalidated auth session', clearError);
|
|
695
|
+
})
|
|
696
|
+
.finally(() => {
|
|
697
|
+
clearingInvalidTokenRef.current = false;
|
|
698
|
+
if (authResolvedRef.current) {
|
|
699
|
+
setTokenReady(true);
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
if (authResolvedRef.current) {
|
|
706
|
+
setTokenReady(true);
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
handleTokenChange(oxyServices.getAccessToken());
|
|
711
|
+
return oxyServices.onTokensChanged(handleTokenChange);
|
|
712
|
+
}, [logger, oxyServices]);
|
|
636
713
|
|
|
637
714
|
// Durable, navigation-safe session persistence.
|
|
638
715
|
//
|
|
@@ -649,12 +726,16 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
649
726
|
await readyStorage.setItem(storageKeys.activeSessionId, sessionId);
|
|
650
727
|
const existingIds = await readyStorage.getItem(storageKeys.sessionIds);
|
|
651
728
|
let sessionIds: string[] = [];
|
|
652
|
-
try {
|
|
729
|
+
try {
|
|
730
|
+
sessionIds = existingIds ? JSON.parse(existingIds) : [];
|
|
731
|
+
} catch (parseError) {
|
|
732
|
+
logger('Failed to parse persisted session ids; replacing corrupted storage value', parseError);
|
|
733
|
+
}
|
|
653
734
|
if (!sessionIds.includes(sessionId)) {
|
|
654
735
|
sessionIds.push(sessionId);
|
|
655
736
|
await readyStorage.setItem(storageKeys.sessionIds, JSON.stringify(sessionIds));
|
|
656
737
|
}
|
|
657
|
-
}, [getReadyStorage, storageKeys.activeSessionId, storageKeys.sessionIds]);
|
|
738
|
+
}, [getReadyStorage, logger, storageKeys.activeSessionId, storageKeys.sessionIds]);
|
|
658
739
|
|
|
659
740
|
// Refs so the cold-boot restore can plant session state without widening its
|
|
660
741
|
// dependency array (mirrors the existing ref pattern above).
|
|
@@ -1397,7 +1478,10 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
1397
1478
|
let fullUser: User;
|
|
1398
1479
|
try {
|
|
1399
1480
|
fullUser = await oxyServices.getCurrentUser();
|
|
1400
|
-
} catch {
|
|
1481
|
+
} catch (profileError) {
|
|
1482
|
+
if (__DEV__) {
|
|
1483
|
+
loggerUtil.debug('Failed to fetch full user after web session; using session user fallback', { component: 'OxyContext', method: 'handleWebSSOSession' }, profileError as unknown);
|
|
1484
|
+
}
|
|
1401
1485
|
// If the profile fetch fails, fall back to the minimal data from the session
|
|
1402
1486
|
// so the user is still logged in (the store accepts User, but the shapes overlap at runtime).
|
|
1403
1487
|
fullUser = session.user as unknown as User;
|
|
@@ -1615,16 +1699,25 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
1615
1699
|
|
|
1616
1700
|
// Load managed accounts when authenticated
|
|
1617
1701
|
const refreshManagedAccounts = useCallback(async (): Promise<void> => {
|
|
1618
|
-
if (!isAuthenticated)
|
|
1702
|
+
if (!isAuthenticated || !tokenReady || !oxyServices.getAccessToken()) {
|
|
1703
|
+
setManagedAccounts([]);
|
|
1704
|
+
return;
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1619
1707
|
try {
|
|
1620
1708
|
const accounts = await oxyServices.getManagedAccounts();
|
|
1621
1709
|
setManagedAccounts(accounts);
|
|
1622
1710
|
} catch (err) {
|
|
1711
|
+
if (isUnauthorizedStatus(err)) {
|
|
1712
|
+
setManagedAccounts([]);
|
|
1713
|
+
await clearSessionStateRef.current();
|
|
1714
|
+
return;
|
|
1715
|
+
}
|
|
1623
1716
|
if (__DEV__) {
|
|
1624
1717
|
loggerUtil.debug('Failed to load managed accounts', { component: 'OxyContext' }, err as unknown);
|
|
1625
1718
|
}
|
|
1626
1719
|
}
|
|
1627
|
-
}, [isAuthenticated, oxyServices]);
|
|
1720
|
+
}, [isAuthenticated, oxyServices, tokenReady]);
|
|
1628
1721
|
|
|
1629
1722
|
useEffect(() => {
|
|
1630
1723
|
if (isAuthenticated && initialized && tokenReady) {
|
|
@@ -1638,9 +1731,13 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
1638
1731
|
// Persist to storage
|
|
1639
1732
|
if (storage) {
|
|
1640
1733
|
if (userId) {
|
|
1641
|
-
storage.setItem(`${storageKeyPrefix}_acting_as`, userId).catch(() => {
|
|
1734
|
+
storage.setItem(`${storageKeyPrefix}_acting_as`, userId).catch((persistError) => {
|
|
1735
|
+
loggerUtil.debug('Failed to persist acting-as account', { component: 'OxyContext' }, persistError as unknown);
|
|
1736
|
+
});
|
|
1642
1737
|
} else {
|
|
1643
|
-
storage.removeItem(`${storageKeyPrefix}_acting_as`).catch(() => {
|
|
1738
|
+
storage.removeItem(`${storageKeyPrefix}_acting_as`).catch((persistError) => {
|
|
1739
|
+
loggerUtil.debug('Failed to clear acting-as account', { component: 'OxyContext' }, persistError as unknown);
|
|
1740
|
+
});
|
|
1644
1741
|
}
|
|
1645
1742
|
}
|
|
1646
1743
|
}, [oxyServices, storage, storageKeyPrefix]);
|
|
@@ -1651,6 +1748,9 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
1651
1748
|
return account;
|
|
1652
1749
|
}, [oxyServices, refreshManagedAccounts]);
|
|
1653
1750
|
|
|
1751
|
+
const canUsePrivateApi = authResolved && isAuthenticated && tokenReady && hasAccessToken;
|
|
1752
|
+
const isPrivateApiPending = !authResolved || (isAuthenticated && (!tokenReady || !hasAccessToken));
|
|
1753
|
+
|
|
1654
1754
|
const contextValue: OxyContextState = useMemo(() => ({
|
|
1655
1755
|
user,
|
|
1656
1756
|
sessions,
|
|
@@ -1658,6 +1758,9 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
1658
1758
|
isAuthenticated,
|
|
1659
1759
|
isLoading,
|
|
1660
1760
|
isTokenReady: tokenReady,
|
|
1761
|
+
hasAccessToken,
|
|
1762
|
+
canUsePrivateApi,
|
|
1763
|
+
isPrivateApiPending,
|
|
1661
1764
|
isAuthResolved: authResolved,
|
|
1662
1765
|
isStorageReady: storage !== null,
|
|
1663
1766
|
error,
|
|
@@ -1701,6 +1804,9 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
1701
1804
|
currentNativeLanguageName,
|
|
1702
1805
|
error,
|
|
1703
1806
|
getDeviceSessions,
|
|
1807
|
+
hasAccessToken,
|
|
1808
|
+
canUsePrivateApi,
|
|
1809
|
+
isPrivateApiPending,
|
|
1704
1810
|
getPublicKey,
|
|
1705
1811
|
hasIdentity,
|
|
1706
1812
|
isAuthenticated,
|
|
@@ -1717,6 +1823,9 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
1717
1823
|
storage,
|
|
1718
1824
|
switchSessionForContext,
|
|
1719
1825
|
tokenReady,
|
|
1826
|
+
hasAccessToken,
|
|
1827
|
+
canUsePrivateApi,
|
|
1828
|
+
isPrivateApiPending,
|
|
1720
1829
|
authResolved,
|
|
1721
1830
|
updateDeviceName,
|
|
1722
1831
|
clearAllAccountData,
|
|
@@ -1765,6 +1874,9 @@ const LOADING_STATE: OxyContextState = {
|
|
|
1765
1874
|
isAuthenticated: false,
|
|
1766
1875
|
isLoading: true,
|
|
1767
1876
|
isTokenReady: false,
|
|
1877
|
+
hasAccessToken: false,
|
|
1878
|
+
canUsePrivateApi: false,
|
|
1879
|
+
isPrivateApiPending: true,
|
|
1768
1880
|
isAuthResolved: false,
|
|
1769
1881
|
isStorageReady: false,
|
|
1770
1882
|
error: null,
|
package/src/ui/hooks/useAuth.ts
CHANGED
|
@@ -41,6 +41,22 @@ export interface AuthState {
|
|
|
41
41
|
/** Whether the auth token is ready for API calls */
|
|
42
42
|
isReady: boolean;
|
|
43
43
|
|
|
44
|
+
/** Whether the current OxyServices instance currently holds an access token */
|
|
45
|
+
hasAccessToken: boolean;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* True only when auth cold-boot is resolved, the user is authenticated, and a
|
|
49
|
+
* bearer token is available for private backend requests.
|
|
50
|
+
*/
|
|
51
|
+
canUsePrivateApi: boolean;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* True while the SDK is still resolving auth or an authenticated session is
|
|
55
|
+
* waiting for its bearer token. Use this to hold private API screens in a
|
|
56
|
+
* loading state instead of firing unauthenticated requests.
|
|
57
|
+
*/
|
|
58
|
+
isPrivateApiPending: boolean;
|
|
59
|
+
|
|
44
60
|
/**
|
|
45
61
|
* Whether the initial auth determination has concluded.
|
|
46
62
|
*
|
|
@@ -107,6 +123,9 @@ export function useAuth(): UseAuthReturn {
|
|
|
107
123
|
isAuthenticated,
|
|
108
124
|
isLoading,
|
|
109
125
|
isTokenReady,
|
|
126
|
+
hasAccessToken,
|
|
127
|
+
canUsePrivateApi,
|
|
128
|
+
isPrivateApiPending,
|
|
110
129
|
isAuthResolved,
|
|
111
130
|
error,
|
|
112
131
|
signIn: oxySignIn,
|
|
@@ -196,6 +215,9 @@ export function useAuth(): UseAuthReturn {
|
|
|
196
215
|
isAuthenticated,
|
|
197
216
|
isLoading: isLoading || !isAuthResolved,
|
|
198
217
|
isReady: isTokenReady,
|
|
218
|
+
hasAccessToken,
|
|
219
|
+
canUsePrivateApi,
|
|
220
|
+
isPrivateApiPending,
|
|
199
221
|
isAuthResolved,
|
|
200
222
|
error,
|
|
201
223
|
|