@oxyhq/services 14.1.0 → 16.0.0
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/index.js +4 -21
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/components/SignInAccountChooser.js +189 -0
- package/lib/commonjs/ui/components/SignInAccountChooser.js.map +1 -0
- package/lib/commonjs/ui/components/SignInModal.js +350 -131
- package/lib/commonjs/ui/components/SignInModal.js.map +1 -1
- package/lib/commonjs/ui/context/OxyContext.js +340 -1547
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js +61 -64
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/commonjs/ui/hooks/mutations/mutationKeys.js +1 -1
- package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js +9 -9
- package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js.map +1 -1
- package/lib/commonjs/ui/hooks/queries/queryKeys.js +1 -1
- package/lib/commonjs/ui/hooks/queries/useAccountQueries.js +7 -8
- package/lib/commonjs/ui/hooks/queries/useAccountQueries.js.map +1 -1
- package/lib/commonjs/ui/hooks/useAuth.js +14 -33
- package/lib/commonjs/ui/hooks/useAuth.js.map +1 -1
- package/lib/commonjs/ui/hooks/usePasswordSignIn.js +170 -0
- package/lib/commonjs/ui/hooks/usePasswordSignIn.js.map +1 -0
- package/lib/commonjs/ui/hooks/useProfileEditing.js +2 -5
- package/lib/commonjs/ui/hooks/useProfileEditing.js.map +1 -1
- package/lib/commonjs/ui/hooks/useSessionManagement.js +2 -2
- package/lib/commonjs/ui/hooks/useSessionManagement.js.map +1 -1
- package/lib/commonjs/ui/navigation/routes.js +1 -0
- package/lib/commonjs/ui/navigation/routes.js.map +1 -1
- package/lib/commonjs/ui/screens/ConnectedAppsScreen.js +17 -18
- package/lib/commonjs/ui/screens/ConnectedAppsScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/EditProfileFieldScreen.js +93 -68
- package/lib/commonjs/ui/screens/EditProfileFieldScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/EditProfileScreen.js +187 -0
- package/lib/commonjs/ui/screens/EditProfileScreen.js.map +1 -0
- package/lib/commonjs/ui/screens/ManageAccountScreen.js +3 -7
- package/lib/commonjs/ui/screens/ManageAccountScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/OxyAuthScreen.js +231 -95
- package/lib/commonjs/ui/screens/OxyAuthScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/ProfileScreen.js +15 -2
- package/lib/commonjs/ui/screens/ProfileScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/linkFormat.js +38 -0
- package/lib/commonjs/ui/screens/linkFormat.js.map +1 -0
- package/lib/commonjs/ui/session/authStore.js +146 -0
- package/lib/commonjs/ui/session/authStore.js.map +1 -0
- package/lib/commonjs/ui/session/createSessionClient.js +23 -17
- package/lib/commonjs/ui/session/createSessionClient.js.map +1 -1
- package/lib/commonjs/ui/session/index.js +13 -7
- package/lib/commonjs/ui/session/index.js.map +1 -1
- package/lib/commonjs/ui/session/tokenTransport.js +19 -31
- package/lib/commonjs/ui/session/tokenTransport.js.map +1 -1
- package/lib/commonjs/ui/utils/isWebBrowser.js +13 -0
- package/lib/commonjs/ui/utils/isWebBrowser.js.map +1 -0
- package/lib/commonjs/utils/deviceFlowSignIn.js +13 -16
- package/lib/commonjs/utils/deviceFlowSignIn.js.map +1 -1
- package/lib/module/index.js +2 -7
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/components/SignInAccountChooser.js +183 -0
- package/lib/module/ui/components/SignInAccountChooser.js.map +1 -0
- package/lib/module/ui/components/SignInModal.js +352 -134
- package/lib/module/ui/components/SignInModal.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +342 -1551
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/context/hooks/useAuthOperations.js +60 -63
- package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/module/ui/hooks/mutations/mutationKeys.js +1 -1
- package/lib/module/ui/hooks/mutations/useAccountMutations.js +7 -7
- package/lib/module/ui/hooks/mutations/useAccountMutations.js.map +1 -1
- package/lib/module/ui/hooks/queries/queryKeys.js +1 -1
- package/lib/module/ui/hooks/queries/useAccountQueries.js +5 -6
- package/lib/module/ui/hooks/queries/useAccountQueries.js.map +1 -1
- package/lib/module/ui/hooks/useAuth.js +14 -33
- package/lib/module/ui/hooks/useAuth.js.map +1 -1
- package/lib/module/ui/hooks/usePasswordSignIn.js +165 -0
- package/lib/module/ui/hooks/usePasswordSignIn.js.map +1 -0
- package/lib/module/ui/hooks/useProfileEditing.js +2 -5
- package/lib/module/ui/hooks/useProfileEditing.js.map +1 -1
- package/lib/module/ui/hooks/useSessionManagement.js +1 -1
- package/lib/module/ui/hooks/useSessionManagement.js.map +1 -1
- package/lib/module/ui/navigation/routes.js +1 -0
- package/lib/module/ui/navigation/routes.js.map +1 -1
- package/lib/module/ui/screens/ConnectedAppsScreen.js +19 -20
- package/lib/module/ui/screens/ConnectedAppsScreen.js.map +1 -1
- package/lib/module/ui/screens/EditProfileFieldScreen.js +91 -66
- package/lib/module/ui/screens/EditProfileFieldScreen.js.map +1 -1
- package/lib/module/ui/screens/EditProfileScreen.js +182 -0
- package/lib/module/ui/screens/EditProfileScreen.js.map +1 -0
- package/lib/module/ui/screens/ManageAccountScreen.js +3 -7
- package/lib/module/ui/screens/ManageAccountScreen.js.map +1 -1
- package/lib/module/ui/screens/OxyAuthScreen.js +233 -97
- package/lib/module/ui/screens/OxyAuthScreen.js.map +1 -1
- package/lib/module/ui/screens/ProfileScreen.js +15 -2
- package/lib/module/ui/screens/ProfileScreen.js.map +1 -1
- package/lib/module/ui/screens/linkFormat.js +31 -0
- package/lib/module/ui/screens/linkFormat.js.map +1 -0
- package/lib/module/ui/session/authStore.js +143 -0
- package/lib/module/ui/session/authStore.js.map +1 -0
- package/lib/module/ui/session/createSessionClient.js +22 -16
- package/lib/module/ui/session/createSessionClient.js.map +1 -1
- package/lib/module/ui/session/index.js +9 -9
- package/lib/module/ui/session/index.js.map +1 -1
- package/lib/module/ui/session/tokenTransport.js +20 -33
- package/lib/module/ui/session/tokenTransport.js.map +1 -1
- package/lib/module/ui/utils/isWebBrowser.js +11 -0
- package/lib/module/ui/utils/isWebBrowser.js.map +1 -0
- package/lib/module/utils/deviceFlowSignIn.js +14 -17
- package/lib/module/utils/deviceFlowSignIn.js.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +2 -3
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/SignInAccountChooser.d.ts +29 -0
- package/lib/typescript/commonjs/ui/components/SignInAccountChooser.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/SignInModal.d.ts +15 -10
- package/lib/typescript/commonjs/ui/components/SignInModal.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts +36 -42
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts +18 -23
- package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts +4 -6
- package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/queries/useAccountQueries.d.ts +6 -7
- package/lib/typescript/commonjs/ui/hooks/queries/useAccountQueries.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useAuth.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/usePasswordSignIn.d.ts +31 -0
- package/lib/typescript/commonjs/ui/hooks/usePasswordSignIn.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/hooks/useProfileEditing.d.ts +0 -1
- package/lib/typescript/commonjs/ui/hooks/useProfileEditing.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/navigation/routes.d.ts +1 -1
- package/lib/typescript/commonjs/ui/navigation/routes.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/ConnectedAppsScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/EditProfileFieldScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/EditProfileScreen.d.ts +14 -0
- package/lib/typescript/commonjs/ui/screens/EditProfileScreen.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/screens/ManageAccountScreen.d.ts +1 -1
- package/lib/typescript/commonjs/ui/screens/ManageAccountScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/OxyAuthScreen.d.ts +13 -12
- package/lib/typescript/commonjs/ui/screens/OxyAuthScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/ProfileScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/linkFormat.d.ts +23 -0
- package/lib/typescript/commonjs/ui/screens/linkFormat.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/session/authStore.d.ts +33 -0
- package/lib/typescript/commonjs/ui/session/authStore.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/session/createSessionClient.d.ts +19 -15
- package/lib/typescript/commonjs/ui/session/createSessionClient.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/session/index.d.ts +9 -9
- package/lib/typescript/commonjs/ui/session/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/session/tokenTransport.d.ts +13 -19
- package/lib/typescript/commonjs/ui/session/tokenTransport.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/utils/isWebBrowser.d.ts +9 -0
- package/lib/typescript/commonjs/ui/utils/isWebBrowser.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/utils/storageHelpers.d.ts +7 -7
- package/lib/typescript/commonjs/utils/deviceFlowSignIn.d.ts +15 -13
- package/lib/typescript/commonjs/utils/deviceFlowSignIn.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +2 -3
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/SignInAccountChooser.d.ts +29 -0
- package/lib/typescript/module/ui/components/SignInAccountChooser.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/SignInModal.d.ts +15 -10
- package/lib/typescript/module/ui/components/SignInModal.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/OxyContext.d.ts +36 -42
- package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts +18 -23
- package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts +4 -6
- package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/queries/useAccountQueries.d.ts +6 -7
- package/lib/typescript/module/ui/hooks/queries/useAccountQueries.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useAuth.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/usePasswordSignIn.d.ts +31 -0
- package/lib/typescript/module/ui/hooks/usePasswordSignIn.d.ts.map +1 -0
- package/lib/typescript/module/ui/hooks/useProfileEditing.d.ts +0 -1
- package/lib/typescript/module/ui/hooks/useProfileEditing.d.ts.map +1 -1
- package/lib/typescript/module/ui/navigation/routes.d.ts +1 -1
- package/lib/typescript/module/ui/navigation/routes.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/ConnectedAppsScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/EditProfileFieldScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/EditProfileScreen.d.ts +14 -0
- package/lib/typescript/module/ui/screens/EditProfileScreen.d.ts.map +1 -0
- package/lib/typescript/module/ui/screens/ManageAccountScreen.d.ts +1 -1
- package/lib/typescript/module/ui/screens/ManageAccountScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/OxyAuthScreen.d.ts +13 -12
- package/lib/typescript/module/ui/screens/OxyAuthScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/ProfileScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/linkFormat.d.ts +23 -0
- package/lib/typescript/module/ui/screens/linkFormat.d.ts.map +1 -0
- package/lib/typescript/module/ui/session/authStore.d.ts +33 -0
- package/lib/typescript/module/ui/session/authStore.d.ts.map +1 -0
- package/lib/typescript/module/ui/session/createSessionClient.d.ts +19 -15
- package/lib/typescript/module/ui/session/createSessionClient.d.ts.map +1 -1
- package/lib/typescript/module/ui/session/index.d.ts +9 -9
- package/lib/typescript/module/ui/session/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/session/tokenTransport.d.ts +13 -19
- package/lib/typescript/module/ui/session/tokenTransport.d.ts.map +1 -1
- package/lib/typescript/module/ui/utils/isWebBrowser.d.ts +9 -0
- package/lib/typescript/module/ui/utils/isWebBrowser.d.ts.map +1 -0
- package/lib/typescript/module/ui/utils/storageHelpers.d.ts +7 -7
- package/lib/typescript/module/utils/deviceFlowSignIn.d.ts +15 -13
- package/lib/typescript/module/utils/deviceFlowSignIn.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/index.ts +2 -6
- package/src/ui/components/SignInAccountChooser.tsx +162 -0
- package/src/ui/components/SignInModal.tsx +309 -139
- package/src/ui/context/OxyContext.tsx +532 -1785
- package/src/ui/context/hooks/useAuthOperations.ts +65 -76
- package/src/ui/hooks/mutations/mutationKeys.ts +1 -1
- package/src/ui/hooks/mutations/useAccountMutations.ts +7 -7
- package/src/ui/hooks/queries/queryKeys.ts +1 -1
- package/src/ui/hooks/queries/useAccountQueries.ts +8 -9
- package/src/ui/hooks/useAuth.ts +14 -35
- package/src/ui/hooks/usePasswordSignIn.ts +207 -0
- package/src/ui/hooks/useProfileEditing.ts +2 -8
- package/src/ui/hooks/useSessionManagement.ts +1 -1
- package/src/ui/navigation/routes.ts +3 -1
- package/src/ui/screens/ConnectedAppsScreen.tsx +23 -24
- package/src/ui/screens/EditProfileFieldScreen.tsx +102 -66
- package/src/ui/screens/EditProfileScreen.tsx +155 -0
- package/src/ui/screens/ManageAccountScreen.tsx +3 -7
- package/src/ui/screens/OxyAuthScreen.tsx +259 -112
- package/src/ui/screens/ProfileScreen.tsx +15 -2
- package/src/ui/screens/__tests__/linkFormat.test.ts +73 -0
- package/src/ui/screens/linkFormat.ts +37 -0
- package/src/ui/session/__tests__/createSessionClient.test.ts +22 -7
- package/src/ui/session/__tests__/tokenTransport.test.ts +37 -49
- package/src/ui/session/authStore.ts +164 -0
- package/src/ui/session/createSessionClient.ts +30 -17
- package/src/ui/session/index.ts +10 -9
- package/src/ui/session/tokenTransport.ts +26 -36
- package/src/ui/utils/isWebBrowser.ts +8 -0
- package/src/ui/utils/storageHelpers.ts +7 -7
- package/src/utils/__tests__/deviceFlowSignIn.test.ts +50 -131
- package/src/utils/deviceFlowSignIn.ts +19 -26
- package/lib/commonjs/ui/context/inSessionTokenRefresh.js +0 -243
- package/lib/commonjs/ui/context/inSessionTokenRefresh.js.map +0 -1
- package/lib/commonjs/ui/context/silentSessionRestore.js +0 -56
- package/lib/commonjs/ui/context/silentSessionRestore.js.map +0 -1
- package/lib/commonjs/ui/hooks/useWebSSO.js +0 -28
- package/lib/commonjs/ui/hooks/useWebSSO.js.map +0 -1
- package/lib/commonjs/ui/session/projectSessionState.js +0 -86
- package/lib/commonjs/ui/session/projectSessionState.js.map +0 -1
- package/lib/commonjs/ui/session/sessionClientHost.js +0 -30
- package/lib/commonjs/ui/session/sessionClientHost.js.map +0 -1
- package/lib/commonjs/ui/utils/activeAuthuser.js +0 -142
- package/lib/commonjs/ui/utils/activeAuthuser.js.map +0 -1
- package/lib/commonjs/utils/crossApex.js +0 -74
- package/lib/commonjs/utils/crossApex.js.map +0 -1
- package/lib/module/ui/context/inSessionTokenRefresh.js +0 -238
- package/lib/module/ui/context/inSessionTokenRefresh.js.map +0 -1
- package/lib/module/ui/context/silentSessionRestore.js +0 -53
- package/lib/module/ui/context/silentSessionRestore.js.map +0 -1
- package/lib/module/ui/hooks/useWebSSO.js +0 -24
- package/lib/module/ui/hooks/useWebSSO.js.map +0 -1
- package/lib/module/ui/session/projectSessionState.js +0 -79
- package/lib/module/ui/session/projectSessionState.js.map +0 -1
- package/lib/module/ui/session/sessionClientHost.js +0 -26
- package/lib/module/ui/session/sessionClientHost.js.map +0 -1
- package/lib/module/ui/utils/activeAuthuser.js +0 -134
- package/lib/module/ui/utils/activeAuthuser.js.map +0 -1
- package/lib/module/utils/crossApex.js +0 -69
- package/lib/module/utils/crossApex.js.map +0 -1
- package/lib/typescript/commonjs/ui/context/inSessionTokenRefresh.d.ts +0 -106
- package/lib/typescript/commonjs/ui/context/inSessionTokenRefresh.d.ts.map +0 -1
- package/lib/typescript/commonjs/ui/context/silentSessionRestore.d.ts +0 -36
- package/lib/typescript/commonjs/ui/context/silentSessionRestore.d.ts.map +0 -1
- package/lib/typescript/commonjs/ui/hooks/useWebSSO.d.ts +0 -19
- package/lib/typescript/commonjs/ui/hooks/useWebSSO.d.ts.map +0 -1
- package/lib/typescript/commonjs/ui/session/projectSessionState.d.ts +0 -45
- package/lib/typescript/commonjs/ui/session/projectSessionState.d.ts.map +0 -1
- package/lib/typescript/commonjs/ui/session/sessionClientHost.d.ts +0 -14
- package/lib/typescript/commonjs/ui/session/sessionClientHost.d.ts.map +0 -1
- package/lib/typescript/commonjs/ui/utils/activeAuthuser.d.ts +0 -67
- package/lib/typescript/commonjs/ui/utils/activeAuthuser.d.ts.map +0 -1
- package/lib/typescript/commonjs/utils/crossApex.d.ts +0 -55
- package/lib/typescript/commonjs/utils/crossApex.d.ts.map +0 -1
- package/lib/typescript/module/ui/context/inSessionTokenRefresh.d.ts +0 -106
- package/lib/typescript/module/ui/context/inSessionTokenRefresh.d.ts.map +0 -1
- package/lib/typescript/module/ui/context/silentSessionRestore.d.ts +0 -36
- package/lib/typescript/module/ui/context/silentSessionRestore.d.ts.map +0 -1
- package/lib/typescript/module/ui/hooks/useWebSSO.d.ts +0 -19
- package/lib/typescript/module/ui/hooks/useWebSSO.d.ts.map +0 -1
- package/lib/typescript/module/ui/session/projectSessionState.d.ts +0 -45
- package/lib/typescript/module/ui/session/projectSessionState.d.ts.map +0 -1
- package/lib/typescript/module/ui/session/sessionClientHost.d.ts +0 -14
- package/lib/typescript/module/ui/session/sessionClientHost.d.ts.map +0 -1
- package/lib/typescript/module/ui/utils/activeAuthuser.d.ts +0 -67
- package/lib/typescript/module/ui/utils/activeAuthuser.d.ts.map +0 -1
- package/lib/typescript/module/utils/crossApex.d.ts +0 -55
- package/lib/typescript/module/utils/crossApex.d.ts.map +0 -1
- package/src/ui/context/inSessionTokenRefresh.ts +0 -264
- package/src/ui/context/silentSessionRestore.ts +0 -53
- package/src/ui/hooks/useWebSSO.ts +0 -23
- package/src/ui/session/__tests__/projectSessionState.test.ts +0 -114
- package/src/ui/session/__tests__/sessionClientHost.test.ts +0 -43
- package/src/ui/session/projectSessionState.ts +0 -85
- package/src/ui/session/sessionClientHost.ts +0 -27
- package/src/ui/utils/activeAuthuser.ts +0 -142
- package/src/utils/crossApex.ts +0 -75
|
@@ -3,10 +3,9 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.useOxy = exports.
|
|
6
|
+
exports.useOxy = exports.OxyProvider = exports.OxyContextProvider = void 0;
|
|
7
7
|
var _react = require("react");
|
|
8
8
|
var _core = require("@oxyhq/core");
|
|
9
|
-
var _bloom = require("@oxyhq/bloom");
|
|
10
9
|
var _authStore = require("../stores/authStore.js");
|
|
11
10
|
var _shallow = require("zustand/react/shallow");
|
|
12
11
|
var _useLanguageManagement = require("../hooks/useLanguageManagement.js");
|
|
@@ -14,17 +13,12 @@ var _useSessionManagement = require("../hooks/useSessionManagement.js");
|
|
|
14
13
|
var _useAuthOperations = require("./hooks/useAuthOperations.js");
|
|
15
14
|
var _useDeviceManagement = require("../hooks/useDeviceManagement.js");
|
|
16
15
|
var _storageHelpers = require("../utils/storageHelpers.js");
|
|
17
|
-
var _errorHandlers = require("../utils/errorHandlers.js");
|
|
18
|
-
var _activeAuthuser = require("../utils/activeAuthuser.js");
|
|
19
16
|
var _bottomSheetManager = require("../navigation/bottomSheetManager.js");
|
|
20
17
|
var _reactQuery = require("@tanstack/react-query");
|
|
21
18
|
var _queryClient = require("../hooks/queryClient.js");
|
|
22
19
|
var _useAvatarPicker = require("../hooks/useAvatarPicker.js");
|
|
23
20
|
var _accountStore = require("../stores/accountStore.js");
|
|
24
|
-
var
|
|
25
|
-
var _crossApex = require("../../utils/crossApex.js");
|
|
26
|
-
var _inSessionTokenRefresh = require("./inSessionTokenRefresh.js");
|
|
27
|
-
var _silentSessionRestore = require("./silentSessionRestore.js");
|
|
21
|
+
var _isWebBrowser = require("../utils/isWebBrowser.js");
|
|
28
22
|
var _index = require("../session/index.js");
|
|
29
23
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
30
24
|
const OxyContext = /*#__PURE__*/(0, _react.createContext)(null);
|
|
@@ -32,124 +26,38 @@ const OxyContext = /*#__PURE__*/(0, _react.createContext)(null);
|
|
|
32
26
|
/**
|
|
33
27
|
* Result of {@link OxyContextState.signInWithPassword}.
|
|
34
28
|
*
|
|
35
|
-
* `'ok'` — the password was accepted and the
|
|
36
|
-
*
|
|
37
|
-
* use), so `isAuthenticated` / `user` are updated and the session is durably persisted;
|
|
38
|
-
* the caller can proceed (e.g. navigate into the app).
|
|
29
|
+
* `'ok'` — the password was accepted and the session committed (so
|
|
30
|
+
* `isAuthenticated` / `user` are updated and the refresh family persisted).
|
|
39
31
|
*
|
|
40
32
|
* `'2fa_required'` — the account has two-factor auth enabled, so NO session was
|
|
41
|
-
* created.
|
|
42
|
-
*
|
|
33
|
+
* created. Complete the challenge with the returned short-lived `loginToken`
|
|
34
|
+
* via {@link OxyContextState.completeTwoFactorSignIn}.
|
|
43
35
|
*/
|
|
44
36
|
|
|
45
|
-
// Active-authuser persistence helpers (web localStorage; native no-op) live in
|
|
46
|
-
// `../utils/activeAuthuser` so the session-management and auth-operations hooks
|
|
47
|
-
// can share them without re-importing this 1k-line context file.
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Per-step fail-fast budget for the cold-boot silent iframe (`silentSignIn`
|
|
51
|
-
* against the per-apex `/auth/silent` host).
|
|
52
|
-
*
|
|
53
|
-
* This step ONLY succeeds when a durable per-apex `fedcm_session` cookie exists
|
|
54
|
-
* (established by a prior `/sso` bounce). On the common reload of a logged-out
|
|
55
|
-
* tab — or a tab that restores via the now-earlier stored-session step — the
|
|
56
|
-
* iframe never posts a message, so the full wait would be dead latency in front
|
|
57
|
-
* of the terminal `/sso` bounce. `silentSignIn` already fails fast on a load
|
|
58
|
-
* error via `iframe.onerror`; this caps the no-message case. 2.5s is well above
|
|
59
|
-
* a same-origin iframe handshake without blocking cold boot for several seconds.
|
|
60
|
-
*/
|
|
61
|
-
const SILENT_IFRAME_TIMEOUT = 2500;
|
|
62
|
-
|
|
63
37
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* cross-app identity key; if the device holds no shared identity it returns
|
|
69
|
-
* `null` quickly, but a slow/offline network must not let it block the cold
|
|
70
|
-
* boot. 8s mirrors the stored-session bearer-validation budget
|
|
71
|
-
* (`VALIDATION_TIMEOUT`) since both perform comparable bounded network work; on
|
|
72
|
-
* expiry the step resolves to `null` and cold boot falls through (native then
|
|
73
|
-
* reaches the unauthenticated backstop, web never runs this step at all).
|
|
74
|
-
*/
|
|
75
|
-
const SHARED_KEY_SIGNIN_TIMEOUT = 8000;
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* HARD overall deadline (ms) for the entire cold-boot step loop —
|
|
79
|
-
* defense-in-depth so a single non-settling step can NEVER hang auth resolution
|
|
80
|
-
* forever (the production regression this guards against: a browser silent
|
|
81
|
-
* credential/iframe probe that ignored its own abort signal left a step's
|
|
82
|
-
* promise unsettled, so `runColdBoot` never advanced to the terminal `/sso`
|
|
83
|
-
* bounce and auth hung indefinitely).
|
|
84
|
-
*
|
|
85
|
-
* Every step ALREADY bounds its own network work (the stored-session bearer
|
|
86
|
-
* validation at 8s, the silent iframe at `SILENT_IFRAME_TIMEOUT` plus its hard
|
|
87
|
-
* settle). On a healthy load the FIRST recovering step wins in a
|
|
88
|
-
* single round-trip (1–3s) and the chain short-circuits long before this fires.
|
|
89
|
-
* This budget only trips when one of those per-step bounds regresses.
|
|
90
|
-
*
|
|
91
|
-
* 20s is the chosen value: comfortably ABOVE the worst-case bounded
|
|
92
|
-
* stored-session path under transient slowness (the 8s parallel validation
|
|
93
|
-
* window plus a `switchSession` round-trip) so a genuinely slow-but-healthy
|
|
94
|
-
* reload is never cut off, yet well BELOW the ~28–30s the previous
|
|
95
|
-
* probe-first ordering took — and, critically, finite, so the user can never
|
|
96
|
-
* sit on an indefinite spinner. When the deadline trips, `runColdBoot` keeps
|
|
97
|
-
* iterating to the terminal `sso-bounce` step (whose navigation side effect
|
|
98
|
-
* runs synchronously), so a genuine no-local-session first visit STILL reaches
|
|
99
|
-
* the cross-domain `/sso` fallback. Native runs only the stored-session step,
|
|
100
|
-
* which is bounded well under this, so the deadline never alters native flow.
|
|
38
|
+
* Fallback client-session validity window (ms) — 7 days — applied when a
|
|
39
|
+
* committed session does not carry an explicit `expiresAt`. This is only a
|
|
40
|
+
* local display hint for the multi-session store; the server remains the source
|
|
41
|
+
* of truth for actual session expiry.
|
|
101
42
|
*/
|
|
102
|
-
const
|
|
43
|
+
const DEFAULT_SESSION_VALIDITY_MS = 7 * 24 * 60 * 60 * 1000;
|
|
103
44
|
|
|
104
45
|
/**
|
|
105
|
-
* How long cold boot WAITS for the post-
|
|
106
|
-
* (`addCurrentAccount` + `start` + `syncFromClient`) before it resolves auth
|
|
107
|
-
*
|
|
46
|
+
* How long the cold boot WAITS for the post-boot SessionClient handoff
|
|
47
|
+
* (`addCurrentAccount` + `start` + `syncFromClient`) before it resolves auth and
|
|
48
|
+
* stops blocking. Once a boot step planted a token the user is already
|
|
108
49
|
* authenticated — the handoff only populates the multi-account set and the
|
|
109
50
|
* server-authoritative active account, and those also arrive via the socket
|
|
110
|
-
* subscription
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* sequential `HttpService`-bounded REST calls could push auth resolution up to
|
|
114
|
-
* ~10s past the ladder's own budget, reintroducing the exact spinner-stall
|
|
115
|
-
* `COLD_BOOT_OVERALL_DEADLINE` exists to prevent. On the normal fast path the
|
|
116
|
-
* handoff completes well within this, so the correct account shows with no
|
|
117
|
-
* flash.
|
|
51
|
+
* subscription. So on a slow backend we stop AWAITING the handoff here (it keeps
|
|
52
|
+
* running in the background and projects when it lands) rather than delaying
|
|
53
|
+
* auth resolution.
|
|
118
54
|
*/
|
|
119
55
|
const SESSION_HANDOFF_DEADLINE = 6000;
|
|
120
56
|
|
|
121
|
-
/**
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
* so a single slow/offline session never blocks the whole startup validation
|
|
125
|
-
* sweep. Sessions that don't answer in time are treated as unvalidated and kept
|
|
126
|
-
* in the persisted ID list; only explicit invalid-session responses are pruned.
|
|
127
|
-
*/
|
|
128
|
-
const VALIDATION_TIMEOUT = 8000;
|
|
129
|
-
const VALIDATION_TIMEOUT_RESULT = 'validation-timeout';
|
|
130
|
-
/**
|
|
131
|
-
* Fallback client-session validity window (ms) — 7 days — applied when a
|
|
132
|
-
* restored account/session does not carry an explicit `expiresAt`. This is only
|
|
133
|
-
* a local display/bookkeeping hint for the multi-session store; the server
|
|
134
|
-
* remains the source of truth for actual session expiry. Used in the refresh
|
|
135
|
-
* cookie restore, stored-session restore, and web-SSO session paths.
|
|
136
|
-
*/
|
|
137
|
-
const DEFAULT_SESSION_VALIDITY_MS = 7 * 24 * 60 * 60 * 1000;
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Minimum interval (ms) between visibility-driven IdP `/auth/session-check`
|
|
141
|
-
* probes. Debounces the hidden-iframe session check so rapid tab focus/blur
|
|
142
|
-
* cycles can't spawn a check-iframe storm; at most one probe runs per window.
|
|
143
|
-
*/
|
|
144
|
-
const IDP_SESSION_CHECK_COOLDOWN = 30000;
|
|
57
|
+
/** The internal commit input — a session plus the device-first extras that are
|
|
58
|
+
* not on the public `SessionLoginResponse` (a rotating refresh token, a device
|
|
59
|
+
* token). All extras optional so `SessionLoginResponse` is assignable. */
|
|
145
60
|
|
|
146
|
-
/**
|
|
147
|
-
* Hard timeout (ms) for a single visibility-driven IdP `/auth/session-check`
|
|
148
|
-
* iframe. If the IdP never posts a `oxy-session-check` message back, the iframe
|
|
149
|
-
* and its listener are torn down after this budget so a non-responsive check
|
|
150
|
-
* can never leak an iframe or a `message` listener.
|
|
151
|
-
*/
|
|
152
|
-
const IDP_SESSION_CHECK_TIMEOUT = 5000;
|
|
153
61
|
function getHttpStatus(error) {
|
|
154
62
|
if (!error || typeof error !== 'object') {
|
|
155
63
|
return undefined;
|
|
@@ -174,51 +82,6 @@ function getHttpStatus(error) {
|
|
|
174
82
|
function isUnauthorizedStatus(error) {
|
|
175
83
|
return getHttpStatus(error) === 401;
|
|
176
84
|
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Whether `idpOrigin` is a same-site, first-party host of the current page —
|
|
180
|
-
* i.e. it shares the page's registrable apex (last two labels), so a "no
|
|
181
|
-
* session" answer from its `/auth/session-check` iframe is authoritative for
|
|
182
|
-
* THIS app and may force a local sign-out.
|
|
183
|
-
*
|
|
184
|
-
* On a cross-site IdP (or any host whose relationship to the page can't be
|
|
185
|
-
* positively established) this returns `false`, so the visibility-driven check
|
|
186
|
-
* may surface a session-ended toast but MUST NOT clear local state — a
|
|
187
|
-
* third-party / undetermined IdP answer can never force logout. Returns `false`
|
|
188
|
-
* off-browser.
|
|
189
|
-
*/
|
|
190
|
-
function isSameSiteIdP(idpOrigin) {
|
|
191
|
-
// Native defines a global `window` but no `window.location`; guard the
|
|
192
|
-
// latter so reading `.hostname` can never throw off-browser. (Only reachable
|
|
193
|
-
// from the web-only visibility check, but kept robust for parity.)
|
|
194
|
-
if (typeof window === 'undefined' || typeof window.location === 'undefined') {
|
|
195
|
-
return false;
|
|
196
|
-
}
|
|
197
|
-
let idpHostname;
|
|
198
|
-
try {
|
|
199
|
-
idpHostname = new URL(idpOrigin).hostname;
|
|
200
|
-
} catch (parseError) {
|
|
201
|
-
if (__DEV__) {
|
|
202
|
-
_core.logger.debug('Invalid IdP origin while checking same-site session status', {
|
|
203
|
-
component: 'OxyContext'
|
|
204
|
-
}, parseError);
|
|
205
|
-
}
|
|
206
|
-
return false;
|
|
207
|
-
}
|
|
208
|
-
const pageHostname = window.location.hostname;
|
|
209
|
-
if (!idpHostname || !pageHostname) return false;
|
|
210
|
-
if (idpHostname === pageHostname) return true;
|
|
211
|
-
const pageApex = (0, _core.registrableApex)(pageHostname);
|
|
212
|
-
const idpApex = (0, _core.registrableApex)(idpHostname);
|
|
213
|
-
// Require a real registrable apex (not a shared/public suffix) AND an exact
|
|
214
|
-
// apex match AND that the IdP host is the page apex itself or a subdomain of it.
|
|
215
|
-
if (!pageApex || idpApex !== pageApex) return false;
|
|
216
|
-
return idpHostname === pageApex || idpHostname.endsWith(`.${pageApex}`);
|
|
217
|
-
}
|
|
218
|
-
function isOnSsoCallbackPath() {
|
|
219
|
-
return (0, _useWebSSO.isWebBrowser)() && window.location.pathname === _core.SSO_CALLBACK_PATH;
|
|
220
|
-
}
|
|
221
|
-
const useBrowserLayoutEffect = typeof document !== 'undefined' ? _react.useLayoutEffect : _react.useEffect;
|
|
222
85
|
let cachedUseFollowHook = null;
|
|
223
86
|
const loadUseFollowHook = () => {
|
|
224
87
|
if (cachedUseFollowHook) {
|
|
@@ -261,19 +124,11 @@ const OxyProvider = ({
|
|
|
261
124
|
if (providedOxyServices) {
|
|
262
125
|
oxyServicesRef.current = providedOxyServices;
|
|
263
126
|
} else if (baseURL) {
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
// `auth.oxy.so` — it owns the host-only `fedcm_session` cookie and the
|
|
267
|
-
// central session store reached via `api.oxy.so`, so a single sign-in
|
|
268
|
-
// there is observed by all RPs through the opaque-code `/sso` bounce.
|
|
269
|
-
// `resolveCentralAuthUrl(authWebUrl)` returns the explicit `authWebUrl`
|
|
270
|
-
// prop when provided (explicit always wins) and the central default
|
|
271
|
-
// otherwise. This is NOT per-apex auto-detection — central SSO is
|
|
272
|
-
// deliberately central. A consumer-provided `OxyServices` instance is
|
|
273
|
-
// never mutated; only the baseURL-only construction path applies this.
|
|
127
|
+
// `authWebUrl` now only points the OAuth "Sign in with Oxy" third-party
|
|
128
|
+
// link builder at the central auth host; there is no cold-boot redirect.
|
|
274
129
|
oxyServicesRef.current = new _core.OxyServices({
|
|
275
130
|
baseURL,
|
|
276
|
-
authWebUrl
|
|
131
|
+
authWebUrl,
|
|
277
132
|
authRedirectUri
|
|
278
133
|
});
|
|
279
134
|
} else {
|
|
@@ -281,6 +136,15 @@ const OxyProvider = ({
|
|
|
281
136
|
}
|
|
282
137
|
}
|
|
283
138
|
const oxyServices = oxyServicesRef.current;
|
|
139
|
+
|
|
140
|
+
// The device-first persisted auth-state store (per-origin refresh family on
|
|
141
|
+
// web; SecureStore session blob + shared-keychain device token on native).
|
|
142
|
+
// Built ONCE per provider mount.
|
|
143
|
+
const authStoreRef = (0, _react.useRef)(null);
|
|
144
|
+
if (!authStoreRef.current) {
|
|
145
|
+
authStoreRef.current = (0, _index.createPlatformAuthStateStore)();
|
|
146
|
+
}
|
|
147
|
+
const authStore = authStoreRef.current;
|
|
284
148
|
const {
|
|
285
149
|
user,
|
|
286
150
|
isAuthenticated,
|
|
@@ -300,13 +164,6 @@ const OxyProvider = ({
|
|
|
300
164
|
})));
|
|
301
165
|
const [tokenReady, setTokenReady] = (0, _react.useState)(true);
|
|
302
166
|
const [hasAccessToken, setHasAccessToken] = (0, _react.useState)(() => Boolean(oxyServices.getAccessToken()));
|
|
303
|
-
// Whether the FIRST cold-boot auth restore has concluded. Starts `false`
|
|
304
|
-
// (auth undetermined) and flips to `true` exactly once — monotonic, never
|
|
305
|
-
// reverts. It now flips the MOMENT a session commits (the common reload case
|
|
306
|
-
// unblocks immediately, without waiting for the rest of the cold-boot chain),
|
|
307
|
-
// with the restore `finally` as the no-session/error backstop. The ref makes
|
|
308
|
-
// the flip idempotent across both sites so the setters fire at most once. See
|
|
309
|
-
// `isAuthResolved` on the context type for the consumer contract.
|
|
310
167
|
const [authResolved, setAuthResolved] = (0, _react.useState)(false);
|
|
311
168
|
const authResolvedRef = (0, _react.useRef)(false);
|
|
312
169
|
const userRef = (0, _react.useRef)(user);
|
|
@@ -314,24 +171,12 @@ const OxyProvider = ({
|
|
|
314
171
|
userRef.current = user;
|
|
315
172
|
isAuthenticatedRef.current = isAuthenticated;
|
|
316
173
|
const [initialized, setInitialized] = (0, _react.useState)(false);
|
|
317
|
-
const [ssoCallbackIntercepting, setSsoCallbackIntercepting] = (0, _react.useState)(false);
|
|
318
174
|
const setAuthState = _authStore.useAuthStore.setState;
|
|
319
175
|
|
|
320
176
|
// Keep the shared `oxyClient` singleton's token store in lockstep with the
|
|
321
|
-
// session owned by THIS provider's instance
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
// only `baseURL` to OxyProvider — in which case the provider owns a DIFFERENT
|
|
325
|
-
// `OxyServices` instance and the singleton would otherwise never receive the
|
|
326
|
-
// token, producing `Authorization: Bearer null`.
|
|
327
|
-
//
|
|
328
|
-
// Subscribing to `onTokensChanged` mirrors EVERY token mutation — sign-in,
|
|
329
|
-
// session restore, switch, silent refresh, and sign-out/clear — onto the
|
|
330
|
-
// singleton at the single source of truth in @oxyhq/core, with no per-app
|
|
331
|
-
// plumbing and regardless of which auth code path fired.
|
|
332
|
-
//
|
|
333
|
-
// When the app passed the singleton itself as `oxyServices` (Mention's
|
|
334
|
-
// pattern), `oxyServices === oxyClient`, so we skip the redundant self-write.
|
|
177
|
+
// session owned by THIS provider's instance (many apps build API clients
|
|
178
|
+
// against the exported singleton while passing only `baseURL` here). Skipped
|
|
179
|
+
// when the app passed the singleton itself as `oxyServices`.
|
|
335
180
|
(0, _react.useEffect)(() => {
|
|
336
181
|
if (oxyServices === _core.oxyClient) {
|
|
337
182
|
return;
|
|
@@ -343,10 +188,6 @@ const OxyProvider = ({
|
|
|
343
188
|
_core.oxyClient.clearTokens();
|
|
344
189
|
}
|
|
345
190
|
};
|
|
346
|
-
|
|
347
|
-
// Seed the singleton with whatever token the instance already holds (it may
|
|
348
|
-
// have been planted synchronously before this effect ran — e.g. a token set
|
|
349
|
-
// during the same tick as mount), then keep it in sync going forward.
|
|
350
191
|
applyToSingleton(oxyServices.getAccessToken());
|
|
351
192
|
return oxyServices.onTokensChanged(applyToSingleton);
|
|
352
193
|
}, [oxyServices]);
|
|
@@ -358,39 +199,16 @@ const OxyProvider = ({
|
|
|
358
199
|
}
|
|
359
200
|
}, []);
|
|
360
201
|
const storageKeys = (0, _react.useMemo)(() => (0, _storageHelpers.getStorageKeys)(storageKeyPrefix), [storageKeyPrefix]);
|
|
361
|
-
|
|
362
|
-
// The app's Oxy OAuth client id surfaced on the context so the cross-app
|
|
363
|
-
// device sign-in components (SignInModal / OxyAuthScreen) can identify the
|
|
364
|
-
// requesting app to `POST /auth/session/create`. Normalized to a trimmed
|
|
365
|
-
// non-empty string, or `null` when the consumer did not configure one — the
|
|
366
|
-
// sign-in components surface a clear configuration error in that case rather
|
|
367
|
-
// than falling back to any display string.
|
|
368
202
|
const clientId = (0, _react.useMemo)(() => {
|
|
369
203
|
const trimmed = clientIdProp?.trim();
|
|
370
204
|
return trimmed ? trimmed : null;
|
|
371
205
|
}, [clientIdProp]);
|
|
372
206
|
|
|
373
|
-
//
|
|
374
|
-
//
|
|
375
|
-
//
|
|
376
|
-
// hooks below. But it is `null` for a brief window after mount while
|
|
377
|
-
// `createPlatformStorage()` resolves (a microtask on web; a dynamic
|
|
378
|
-
// `import()` on native). Any persistence path that fires during that window
|
|
379
|
-
// — e.g. an interactive password sign-in the instant the screen mounts —
|
|
380
|
-
// would read `storage === null` and SILENTLY skip writing the session,
|
|
381
|
-
// leaving the user signed-in in-memory but with nothing to restore on
|
|
382
|
-
// reload.
|
|
383
|
-
//
|
|
384
|
-
// To make persistence robust regardless of timing we ALSO expose the storage
|
|
385
|
-
// as an awaitable promise (`getReadyStorage`). Persistence code awaits the
|
|
386
|
-
// ready instance instead of branching on the possibly-null state, so a write
|
|
387
|
-
// is never silently dropped just because it raced storage init.
|
|
207
|
+
// Local display/bookkeeping storage (session id list, language). Distinct
|
|
208
|
+
// from `authStore` (the durable credential blob). Exposed as an awaitable so
|
|
209
|
+
// a persistence write is never dropped because it raced storage init.
|
|
388
210
|
const storageRef = (0, _react.useRef)(null);
|
|
389
211
|
const [storage, setStorage] = (0, _react.useState)(null);
|
|
390
|
-
|
|
391
|
-
// A single, stable deferred that resolves with the initialized storage. Built
|
|
392
|
-
// lazily via a ref initializer so the resolver is captured exactly once and
|
|
393
|
-
// the promise identity is stable across renders.
|
|
394
212
|
const buildStorageDeferred = () => {
|
|
395
213
|
let resolve = () => undefined;
|
|
396
214
|
const promise = new Promise(res => {
|
|
@@ -406,40 +224,9 @@ const OxyProvider = ({
|
|
|
406
224
|
storageReadyRef.current = buildStorageDeferred();
|
|
407
225
|
}
|
|
408
226
|
const storageReady = storageReadyRef.current;
|
|
409
|
-
|
|
410
|
-
// Resolve the storage instance that is guaranteed to be ready. Returns the
|
|
411
|
-
// already-initialized instance synchronously when available, otherwise awaits
|
|
412
|
-
// the init promise. Never resolves to `null`.
|
|
413
|
-
const getReadyStorage = (0, _react.useCallback)(() => {
|
|
414
|
-
if (storageRef.current) {
|
|
415
|
-
return Promise.resolve(storageRef.current);
|
|
416
|
-
}
|
|
417
|
-
return storageReady.promise;
|
|
418
|
-
}, [storageReady]);
|
|
419
|
-
|
|
420
|
-
// Clear the durable "had a signed-in Oxy session before" hint.
|
|
421
|
-
//
|
|
422
|
-
// The hint (`storageKeys.priorSession`) is WRITTEN whenever a session is
|
|
423
|
-
// established/restored — every commit funnels through `persistSessionDurably`,
|
|
424
|
-
// and the stored-session reload winner sets it directly to backfill
|
|
425
|
-
// pre-existing installs. It lives in the SAME `storageKeyPrefix`-scoped
|
|
426
|
-
// durable store as the session ids, so it SURVIVES a session expiring; it is
|
|
427
|
-
// cleared ONLY here, on EXPLICIT full sign-out (wired into `clearAllAccountData`
|
|
428
|
-
// and the `useAuthOperations` logout paths — never the passive token-expiry
|
|
429
|
-
// path). At cold boot the hint is read into `hadPriorSession` and feeds
|
|
430
|
-
// `allowSsoBounce`: a RETURNING visitor (hint present) whose local session has
|
|
431
|
-
// lapsed still gets ONE terminal `/sso` establish bounce so a central-only
|
|
432
|
-
// cross-domain session recovers, while a truly first-time anonymous visitor is
|
|
433
|
-
// never force-redirected.
|
|
434
|
-
const clearPriorSessionHint = (0, _react.useCallback)(async () => {
|
|
435
|
-
const readyStorage = await getReadyStorage();
|
|
436
|
-
await readyStorage.removeItem(storageKeys.priorSession);
|
|
437
|
-
}, [getReadyStorage, storageKeys.priorSession]);
|
|
438
227
|
(0, _react.useEffect)(() => {
|
|
439
228
|
let mounted = true;
|
|
440
229
|
(0, _storageHelpers.createPlatformStorage)().then(storageInstance => {
|
|
441
|
-
// Resolve the ready-promise even if the component unmounted: in-flight
|
|
442
|
-
// persistence awaiting it must still complete against a real store.
|
|
443
230
|
storageRef.current = storageInstance;
|
|
444
231
|
storageReady.resolve(storageInstance);
|
|
445
232
|
if (mounted) {
|
|
@@ -459,10 +246,6 @@ const OxyProvider = ({
|
|
|
459
246
|
mounted = false;
|
|
460
247
|
};
|
|
461
248
|
}, [logger, onError, storageReady]);
|
|
462
|
-
|
|
463
|
-
// Offline queuing is now handled by TanStack Query mutations
|
|
464
|
-
// No need for custom offline queue
|
|
465
|
-
|
|
466
249
|
const {
|
|
467
250
|
currentLanguage,
|
|
468
251
|
metadata: currentLanguageMetadata,
|
|
@@ -502,117 +285,72 @@ const OxyProvider = ({
|
|
|
502
285
|
queryClient
|
|
503
286
|
});
|
|
504
287
|
|
|
505
|
-
//
|
|
506
|
-
//
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
288
|
+
// Refs so callbacks below can invoke the latest session-management functions
|
|
289
|
+
// without widening dependency arrays.
|
|
290
|
+
const switchSessionRef = (0, _react.useRef)(switchSession);
|
|
291
|
+
switchSessionRef.current = switchSession;
|
|
292
|
+
const clearSessionStateRef = (0, _react.useRef)(clearSessionState);
|
|
293
|
+
clearSessionStateRef.current = clearSessionState;
|
|
294
|
+
const setActiveSessionIdRef = (0, _react.useRef)(setActiveSessionId);
|
|
295
|
+
setActiveSessionIdRef.current = setActiveSessionId;
|
|
296
|
+
const loginSuccessRef = (0, _react.useRef)(loginSuccess);
|
|
297
|
+
loginSuccessRef.current = loginSuccess;
|
|
298
|
+
const onAuthStateChangeRef = (0, _react.useRef)(onAuthStateChange);
|
|
299
|
+
onAuthStateChangeRef.current = onAuthStateChange;
|
|
300
|
+
|
|
301
|
+
// Flip the auth-resolution gate (`authResolved` + `tokenReady`) the moment a
|
|
302
|
+
// session commits or the boot concludes signed out. Idempotent + monotonic.
|
|
303
|
+
const markAuthResolved = (0, _react.useCallback)(() => {
|
|
304
|
+
if (authResolvedRef.current) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
authResolvedRef.current = true;
|
|
308
|
+
setTokenReady(true);
|
|
309
|
+
setAuthResolved(true);
|
|
310
|
+
}, []);
|
|
311
|
+
const markAuthResolvedRef = (0, _react.useRef)(markAuthResolved);
|
|
312
|
+
markAuthResolvedRef.current = markAuthResolved;
|
|
313
|
+
|
|
314
|
+
// Server-authoritative device session client. Built ONCE per `oxyServices`
|
|
315
|
+
// instance. `onUnauthenticated` (a device signout-all pushed over the socket,
|
|
316
|
+
// or bootstrapped as zero-account) clears the persisted store + local state so
|
|
317
|
+
// a reload does not try to restore a session the device no longer has.
|
|
517
318
|
const sessionClientPairRef = (0, _react.useRef)(null);
|
|
518
319
|
if (!sessionClientPairRef.current) {
|
|
519
|
-
sessionClientPairRef.current = (0, _index.createSessionClient)(oxyServices)
|
|
320
|
+
sessionClientPairRef.current = (0, _index.createSessionClient)(oxyServices, authStore, () => {
|
|
321
|
+
(0, _useAuthOperations.clearPersistedAuthSafe)(authStore, logger);
|
|
322
|
+
void clearSessionStateRef.current().catch(clearError => {
|
|
323
|
+
logger('Failed to clear local state on remote sign-out', clearError);
|
|
324
|
+
});
|
|
325
|
+
});
|
|
520
326
|
}
|
|
521
327
|
const {
|
|
522
328
|
client: sessionClient,
|
|
523
329
|
host: sessionClientHost
|
|
524
330
|
} = sessionClientPairRef.current;
|
|
525
331
|
|
|
526
|
-
//
|
|
527
|
-
//
|
|
528
|
-
//
|
|
529
|
-
// the recovered account into the device set
|
|
530
|
-
// itself (`sessionClient.addCurrentAccount()`). The cold-boot post-ladder
|
|
531
|
-
// handoff ALSO registers, but only as a FALLBACK for the `stored-session`
|
|
532
|
-
// step, which commits through a completely different path
|
|
533
|
-
// (`switchSessionRef`) and never touches `sessionClient` on its own. This
|
|
534
|
-
// ref lets the handoff detect "did a ladder step already register this
|
|
535
|
-
// boot?" and skip a redundant second `POST /session/device/add`. Reset at
|
|
536
|
-
// the start of every cold-boot pass.
|
|
537
|
-
const registeredDuringBootRef = (0, _react.useRef)(false);
|
|
538
|
-
|
|
539
|
-
// Projects `client.getState()` onto the exposed `sessions` / `activeSessionId`
|
|
540
|
-
// / `user` via the SAME setters the existing cold-boot/session-management
|
|
541
|
-
// paths use. This is the SOLE authority for both a locally-initiated mutation
|
|
542
|
-
// (switch/logout resolving here) AND a REMOTELY-pushed `session_state` (the
|
|
543
|
-
// `device:<deviceId>` socket owned by `client.start()`, subscribed to below) —
|
|
544
|
-
// there is no per-domain `useSessionSocket` anymore.
|
|
545
|
-
//
|
|
546
|
-
// GAP FIX (Task 4): when the pushed/bootstrapped state reports ZERO device
|
|
547
|
-
// accounts, this device has been fully signed out — either by the LOCAL
|
|
548
|
-
// `logout()`/`logoutAll()` mutation (which also runs its own explicit
|
|
549
|
-
// cleanup) or, just as importantly, by a REMOTE actor (another tab/device
|
|
550
|
-
// removing the last session, or an admin revoking every device account)
|
|
551
|
-
// pushing that same empty state over the socket. The prior per-domain
|
|
552
|
-
// `useSessionSocket` forced a local sign-out in this situation
|
|
553
|
-
// (`triggerLocalSignOut`); without an equivalent here a remote full sign-out
|
|
554
|
-
// would leave `user`/`isAuthenticated`/the bearer token stale until the next
|
|
555
|
-
// 401. Route through the SAME `clearSessionState()` a local full sign-out
|
|
556
|
-
// uses (authStore reset, token clear, query-cache clear, storage clear) —
|
|
557
|
-
// idempotent with the local-logout caller's own explicit call.
|
|
558
|
-
//
|
|
559
|
-
// DELIBERATE-SIGN-OUT GUARD (Task 5): a zero-account state — however it
|
|
560
|
-
// arrived — is just as terminal as a local `logout()`/`logoutAll()`, so it
|
|
561
|
-
// sets the SAME durable "deliberately signed out" flag (web-only,
|
|
562
|
-
// best-effort) that those two callers set directly. Without this, a remote
|
|
563
|
-
// full wipe would leave the flag unset, and the `silent-iframe` cold-boot
|
|
564
|
-
// step could silently re-mint a session from a still-live per-apex
|
|
565
|
-
// `fedcm_session` cookie right after this device was authoritatively signed
|
|
566
|
-
// out everywhere.
|
|
567
|
-
//
|
|
568
|
-
// REMOTE-SIGN-OUT SUPPRESSION FIX (review H1): the terminal `sso-bounce`
|
|
569
|
-
// cold-boot step is gated on the durable prior-session hint
|
|
570
|
-
// (`allowSsoBounce` = `hasPriorSession || hasLocalSession`), NOT on the
|
|
571
|
-
// deliberately-signed-out flag set above. Leaving that hint in place after a
|
|
572
|
-
// REMOTE full sign-out means the next reload still performs one terminal
|
|
573
|
-
// `/sso` establish bounce, which can silently re-mint a session from a
|
|
574
|
-
// still-live central `fedcm_session` — signing the user back in right after
|
|
575
|
-
// they were signed out everywhere. Clear the SAME cleanup set the LOCAL
|
|
576
|
-
// `logout()`/`logoutAll()` paths use (`useAuthOperations.ts`) — SSO bounce
|
|
577
|
-
// state + the prior-session hint — so a remote sign-out is indistinguishable
|
|
578
|
-
// from a local one to the next cold boot.
|
|
332
|
+
// Projects `client.getState()` onto the exposed `sessions`/`activeSessionId`/
|
|
333
|
+
// `user`. Sole authority for both locally-initiated mutations (switch/logout)
|
|
334
|
+
// AND remotely-pushed `session_state` over the `device:<deviceId>` socket.
|
|
579
335
|
const syncFromClient = (0, _react.useCallback)(async () => {
|
|
580
336
|
const state = sessionClient.getState();
|
|
581
337
|
if (state === null) {
|
|
582
|
-
// INERT: no session state has been bootstrapped (client.start() has not
|
|
583
|
-
// run). Never overwrite the existing cold-boot-driven state.
|
|
584
338
|
return;
|
|
585
339
|
}
|
|
586
340
|
if (state.accounts.length === 0) {
|
|
587
341
|
sessionClientHost.setCurrentAccountId(null);
|
|
588
|
-
|
|
589
|
-
(0, _activeAuthuser.markSignedOut)();
|
|
590
|
-
}
|
|
591
|
-
(0, _activeAuthuser.clearSsoBounceState)();
|
|
592
|
-
(0, _useAuthOperations.clearPriorSessionHintSafe)(clearPriorSessionHint, logger);
|
|
342
|
+
(0, _useAuthOperations.clearPersistedAuthSafe)(authStore, logger);
|
|
593
343
|
await clearSessionState();
|
|
594
344
|
return;
|
|
595
345
|
}
|
|
596
|
-
//
|
|
597
|
-
//
|
|
598
|
-
// mutation call — and each invocation captures `state` before an async
|
|
599
|
-
// profile fetch (`getUsersByIds`). `SessionClient`'s own state is
|
|
600
|
-
// monotonic by revision, but this projection previously was not: a
|
|
601
|
-
// SLOWER, OLDER fetch resolving AFTER a newer one would still apply its
|
|
602
|
-
// now-stale captured `state`, clobbering `user`/`sessions`/
|
|
603
|
-
// `activeSessionId` back to an outdated account. Capture the revision
|
|
604
|
-
// this fetch is FOR, then after the await, re-read the client's current
|
|
605
|
-
// state and bail if it has since moved past the captured revision — a
|
|
606
|
-
// fresher call has already applied (or will apply) the current truth.
|
|
346
|
+
// Last-write-wins guard: capture the revision this projection is for, then
|
|
347
|
+
// after the async profile fetch bail if a fresher state has landed.
|
|
607
348
|
const capturedRevision = state.revision;
|
|
608
349
|
const ids = (0, _index.accountIdsOf)(state);
|
|
609
350
|
let users = [];
|
|
610
351
|
try {
|
|
611
352
|
users = ids.length > 0 ? await oxyServices.getUsersByIds(ids) : [];
|
|
612
353
|
} catch (fetchError) {
|
|
613
|
-
// Invoked fire-and-forget from the socket `notify()` subscription, so a
|
|
614
|
-
// rejected profile fetch would surface as an unhandled rejection. Log and
|
|
615
|
-
// bail; the next `notify()`/mutation will re-project the current truth.
|
|
616
354
|
logger('Failed to resolve account profiles during syncFromClient', fetchError);
|
|
617
355
|
return;
|
|
618
356
|
}
|
|
@@ -628,7 +366,9 @@ const OxyProvider = ({
|
|
|
628
366
|
loginSuccess(activeUser);
|
|
629
367
|
}
|
|
630
368
|
sessionClientHost.setCurrentAccountId(latest.activeAccountId);
|
|
631
|
-
}, [oxyServices, sessionClient, sessionClientHost, updateSessions, setActiveSessionId, loginSuccess, clearSessionState,
|
|
369
|
+
}, [oxyServices, sessionClient, sessionClientHost, authStore, updateSessions, setActiveSessionId, loginSuccess, clearSessionState, logger]);
|
|
370
|
+
const syncFromClientRef = (0, _react.useRef)(syncFromClient);
|
|
371
|
+
syncFromClientRef.current = syncFromClient;
|
|
632
372
|
(0, _react.useEffect)(() => {
|
|
633
373
|
return sessionClient.subscribe(() => {
|
|
634
374
|
void syncFromClient();
|
|
@@ -640,13 +380,13 @@ const OxyProvider = ({
|
|
|
640
380
|
logoutAll
|
|
641
381
|
} = (0, _useAuthOperations.useAuthOperations)({
|
|
642
382
|
oxyServices,
|
|
383
|
+
store: authStore,
|
|
643
384
|
storage,
|
|
644
385
|
activeSessionId,
|
|
645
386
|
setActiveSessionId,
|
|
646
387
|
updateSessions,
|
|
647
388
|
saveActiveSessionId,
|
|
648
389
|
clearSessionState,
|
|
649
|
-
clearPriorSessionHint,
|
|
650
390
|
switchSession,
|
|
651
391
|
sessionClient,
|
|
652
392
|
syncFromClient,
|
|
@@ -659,13 +399,8 @@ const OxyProvider = ({
|
|
|
659
399
|
setAuthState,
|
|
660
400
|
logger
|
|
661
401
|
});
|
|
662
|
-
|
|
663
|
-
// Clear all account data (sessions, cache, etc.)
|
|
664
402
|
const clearAllAccountData = (0, _react.useCallback)(async () => {
|
|
665
|
-
// Clear TanStack Query cache (in-memory)
|
|
666
403
|
queryClient.clear();
|
|
667
|
-
|
|
668
|
-
// Clear persisted query cache
|
|
669
404
|
if (storage) {
|
|
670
405
|
try {
|
|
671
406
|
await (0, _queryClient.clearQueryCache)(storage);
|
|
@@ -673,22 +408,13 @@ const OxyProvider = ({
|
|
|
673
408
|
logger('Failed to clear persisted query cache', error);
|
|
674
409
|
}
|
|
675
410
|
}
|
|
676
|
-
|
|
677
|
-
// Clear session state (sessions, activeSessionId, storage)
|
|
678
411
|
await clearSessionState();
|
|
679
|
-
|
|
680
|
-
//
|
|
681
|
-
|
|
682
|
-
// `/sso` bounce). NOT done on the passive token-expiry path, so an expired
|
|
683
|
-
// session still recovers via a returning-user bounce.
|
|
684
|
-
await clearPriorSessionHint();
|
|
685
|
-
|
|
686
|
-
// Reset account store
|
|
412
|
+
// Explicit FULL wipe: also drop the persisted refresh family so a reload
|
|
413
|
+
// finds nothing to restore.
|
|
414
|
+
await authStore.clear();
|
|
687
415
|
_accountStore.useAccountStore.getState().reset();
|
|
688
|
-
|
|
689
|
-
// Clear HTTP service cache
|
|
690
416
|
oxyServices.clearCache();
|
|
691
|
-
}, [queryClient, storage, clearSessionState,
|
|
417
|
+
}, [queryClient, storage, clearSessionState, authStore, logger, oxyServices]);
|
|
692
418
|
const {
|
|
693
419
|
getDeviceSessions,
|
|
694
420
|
logoutAllDeviceSessions,
|
|
@@ -702,13 +428,12 @@ const OxyProvider = ({
|
|
|
702
428
|
});
|
|
703
429
|
const useFollowHook = loadUseFollowHook();
|
|
704
430
|
|
|
705
|
-
//
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
clearSessionStateRef.current = clearSessionState;
|
|
431
|
+
// Token-change side effects: an invalidated bearer (HttpService clears tokens
|
|
432
|
+
// on an unrecoverable 401 and emits `null`) must locally sign out an
|
|
433
|
+
// authenticated user so `isAuthenticated` never lingers true with no token.
|
|
434
|
+
// The persisted store is NOT cleared here — the refresh handler already
|
|
435
|
+
// cleared it if the family was revoked; a transient null leaves it intact so a
|
|
436
|
+
// later reload can still restore.
|
|
712
437
|
const clearingInvalidTokenRef = (0, _react.useRef)(false);
|
|
713
438
|
(0, _react.useEffect)(() => {
|
|
714
439
|
const handleTokenChange = accessToken => {
|
|
@@ -741,1124 +466,279 @@ const OxyProvider = ({
|
|
|
741
466
|
return oxyServices.onTokensChanged(handleTokenChange);
|
|
742
467
|
}, [logger, oxyServices]);
|
|
743
468
|
|
|
744
|
-
//
|
|
745
|
-
//
|
|
746
|
-
//
|
|
747
|
-
//
|
|
748
|
-
//
|
|
749
|
-
// a zombie logged-in state whose cross-apex feed calls 401-looped. Here we (1)
|
|
750
|
-
// install a handler that re-mints a fresh token WITHOUT a reload using the SAME
|
|
751
|
-
// durable silent-restore arms cold boot uses, and (2) start a proactive
|
|
752
|
-
// scheduler that refreshes ~60s before expiry (and on tab-focus / app-
|
|
753
|
-
// foreground) so the common case never even hits the reactive 401 path. The
|
|
754
|
-
// linked client (`createLinkedClient`) delegates its refresh back to this owner
|
|
755
|
-
// handler, so it is fixed for free.
|
|
756
|
-
//
|
|
757
|
-
// Runs once per `oxyServices` instance (stable, ref-constructed), and BEFORE
|
|
758
|
-
// any cold-boot request can 401: cold boot is gated on async storage init, so
|
|
759
|
-
// this synchronous mount effect installs the handler first. On cleanup the
|
|
760
|
-
// handler is detached and the scheduler torn down (timer + foreground listener)
|
|
761
|
-
// so nothing leaks across a provider remount. `setAuthRefreshHandler` is
|
|
762
|
-
// optional-chained to tolerate partial test stubs.
|
|
469
|
+
// Unified in-session refresh (SDK-owned; every RP inherits it). Installs the
|
|
470
|
+
// ONE core refresh handler (rotate the persisted refresh family; native falls
|
|
471
|
+
// back to the shared keychain) plus the proactive scheduler that refreshes
|
|
472
|
+
// ~60s before expiry and on tab-focus. Replaces the deleted services-local
|
|
473
|
+
// `inSessionTokenRefresh` module.
|
|
763
474
|
(0, _react.useEffect)(() => {
|
|
764
|
-
|
|
765
|
-
|
|
475
|
+
const dispose = (0, _core.installAuthRefreshHandler)({
|
|
476
|
+
oxy: oxyServices,
|
|
477
|
+
store: authStore
|
|
478
|
+
});
|
|
479
|
+
const scheduler = (0, _core.startTokenRefreshScheduler)(oxyServices);
|
|
766
480
|
return () => {
|
|
767
481
|
scheduler.dispose();
|
|
768
|
-
|
|
482
|
+
dispose();
|
|
769
483
|
};
|
|
770
|
-
}, [oxyServices]);
|
|
771
|
-
|
|
772
|
-
//
|
|
773
|
-
//
|
|
774
|
-
//
|
|
775
|
-
//
|
|
776
|
-
//
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
try {
|
|
788
|
-
sessionIds = existingIds ? JSON.parse(existingIds) : [];
|
|
789
|
-
} catch (parseError) {
|
|
790
|
-
logger('Failed to parse persisted session ids; replacing corrupted storage value', parseError);
|
|
791
|
-
}
|
|
792
|
-
if (!sessionIds.includes(sessionId)) {
|
|
793
|
-
sessionIds.push(sessionId);
|
|
794
|
-
await readyStorage.setItem(storageKeys.sessionIds, JSON.stringify(sessionIds));
|
|
795
|
-
}
|
|
796
|
-
// A session is now durably committed — set the returning-user hint so a
|
|
797
|
-
// future cold boot whose local session has lapsed still gets ONE `/sso`
|
|
798
|
-
// establish bounce (see `markPriorSessionHint`). Every web commit path
|
|
799
|
-
// (silent iframe / SSO return / password / cookie restore) funnels
|
|
800
|
-
// through here, so this is the single chokepoint for the hint.
|
|
801
|
-
await readyStorage.setItem(storageKeys.priorSession, '1');
|
|
802
|
-
}, [getReadyStorage, logger, storageKeys.activeSessionId, storageKeys.sessionIds, storageKeys.priorSession]);
|
|
803
|
-
|
|
804
|
-
// Refs so the cold-boot restore can plant session state without widening its
|
|
805
|
-
// dependency array (mirrors the existing ref pattern above).
|
|
806
|
-
const setActiveSessionIdRef = (0, _react.useRef)(setActiveSessionId);
|
|
807
|
-
setActiveSessionIdRef.current = setActiveSessionId;
|
|
808
|
-
const loginSuccessRef = (0, _react.useRef)(loginSuccess);
|
|
809
|
-
loginSuccessRef.current = loginSuccess;
|
|
810
|
-
const onAuthStateChangeRef = (0, _react.useRef)(onAuthStateChange);
|
|
811
|
-
onAuthStateChangeRef.current = onAuthStateChange;
|
|
812
|
-
|
|
813
|
-
// Flip the auth-resolution gate (`authResolved` + `tokenReady`) the MOMENT a
|
|
814
|
-
// session commits, instead of waiting for the whole cold-boot chain to finish.
|
|
815
|
-
// Idempotent and monotonic via `authResolvedRef`: the first call wins and the
|
|
816
|
-
// setters fire at most once, so the restore `finally` backstop becomes a no-op
|
|
817
|
-
// once a commit site has already marked resolution. Called from EVERY place a
|
|
818
|
-
// user is actually committed (the iframe/SSO path
|
|
819
|
-
// `handleWebSSOSession` and the stored-session path) so the common reload
|
|
820
|
-
// case unblocks the loading gate without sitting behind the remaining
|
|
821
|
-
// (now-skipped) cold-boot steps.
|
|
822
|
-
const markAuthResolved = (0, _react.useCallback)(() => {
|
|
823
|
-
if (authResolvedRef.current) {
|
|
824
|
-
return;
|
|
825
|
-
}
|
|
826
|
-
authResolvedRef.current = true;
|
|
827
|
-
setTokenReady(true);
|
|
828
|
-
setAuthResolved(true);
|
|
829
|
-
}, []);
|
|
830
|
-
const markAuthResolvedRef = (0, _react.useRef)(markAuthResolved);
|
|
831
|
-
markAuthResolvedRef.current = markAuthResolved;
|
|
832
|
-
|
|
833
|
-
// `handleWebSSOSession` is declared further down (it depends on values that
|
|
834
|
-
// are only available there). The iframe/SSO cold-boot steps need to commit
|
|
835
|
-
// a recovered session through it, so we route the call through a ref that is
|
|
836
|
-
// populated once the callback exists. The ref is assigned synchronously on
|
|
837
|
-
// every render before the cold-boot effect can fire (the effect is gated on
|
|
838
|
-
// `storage` + `initialized`, both of which settle after first render).
|
|
839
|
-
const handleWebSSOSessionRef = (0, _react.useRef)(null);
|
|
840
|
-
|
|
841
|
-
// Native (and offline) stored-session restore — the ONLY restore path that
|
|
842
|
-
// runs on React Native, and the web fallback when no cross-domain step won.
|
|
843
|
-
//
|
|
844
|
-
// Stored-session restore. Web uses this only as a fast local winner after
|
|
845
|
-
// URL-return handling; native uses it as the durable SecureStore path. Native
|
|
846
|
-
// first plants the shared access token from KeyManager, then validates the
|
|
847
|
-
// stored session ids with the bearer already in memory.
|
|
848
|
-
const restoreStoredSession = (0, _react.useCallback)(async () => {
|
|
849
|
-
if (!storage) {
|
|
850
|
-
return false;
|
|
851
|
-
}
|
|
852
|
-
const storedSessionIdsJson = await storage.getItem(storageKeys.sessionIds);
|
|
853
|
-
const storedSessionIdsFromStorage = storedSessionIdsJson ? JSON.parse(storedSessionIdsJson) : [];
|
|
854
|
-
let storedActiveSessionId = await storage.getItem(storageKeys.activeSessionId);
|
|
855
|
-
// OPTIONAL HINT ONLY (no longer a gate). Nothing writes the active-`authuser`
|
|
856
|
-
// marker since the SessionClient cutover (`writeActiveAuthuser` was deleted),
|
|
857
|
-
// so it is `null` on every fresh install; when a legacy value is present it
|
|
858
|
-
// is used purely to backfill `clientSession.authuser` for the active session
|
|
859
|
-
// below. It MUST NOT decide whether restore is attempted — see the guard.
|
|
860
|
-
const storedActiveAuthuser = (0, _useWebSSO.isWebBrowser)() ? (0, _activeAuthuser.readActiveAuthuser)() : null;
|
|
861
|
-
|
|
862
|
-
// On web with no in-memory bearer, bail ONLY when there is genuinely nothing
|
|
863
|
-
// to restore — neither a stored active id NOR any stored session ids. A
|
|
864
|
-
// surviving `sessionIds` list (e.g. the active-id key was removed after a
|
|
865
|
-
// server-side revocation while sibling sessions remain) MUST still be
|
|
866
|
-
// validated over the network and have a valid session elected as active
|
|
867
|
-
// below. The old guard also required the deleted `activeAuthuser` marker,
|
|
868
|
-
// which since the cutover is never written — that made EVERY web reload
|
|
869
|
-
// whose local bearer had lapsed bail before any validation. Native always
|
|
870
|
-
// proceeds (it plants the shared KeyManager bearer next).
|
|
871
|
-
if ((0, _useWebSSO.isWebBrowser)() && !oxyServices.getAccessToken() && storedActiveSessionId === null && storedSessionIdsFromStorage.length === 0) {
|
|
872
|
-
return false;
|
|
873
|
-
}
|
|
874
|
-
const nativeSharedSession = !(0, _useWebSSO.isWebBrowser)() ? await _core.KeyManager.getSharedSession().catch(() => null) : null;
|
|
875
|
-
if (nativeSharedSession?.accessToken) {
|
|
876
|
-
oxyServices.setTokens(nativeSharedSession.accessToken);
|
|
877
|
-
storedActiveSessionId = storedActiveSessionId ?? nativeSharedSession.sessionId;
|
|
878
|
-
}
|
|
879
|
-
const storedSessionIds = Array.from(new Set([...storedSessionIdsFromStorage, ...(nativeSharedSession?.sessionId ? [nativeSharedSession.sessionId] : [])]));
|
|
880
|
-
let validSessions = [];
|
|
881
|
-
let unvalidatedSessionIds = [];
|
|
882
|
-
if (storedSessionIds.length > 0) {
|
|
883
|
-
// Validate all sessions in parallel (with a per-session timeout) to avoid
|
|
884
|
-
// sequential blocking that freezes the app on startup
|
|
885
|
-
const results = await Promise.allSettled(storedSessionIds.map(async sessionId => {
|
|
886
|
-
const timeoutPromise = new Promise(resolve => setTimeout(() => resolve(VALIDATION_TIMEOUT_RESULT), VALIDATION_TIMEOUT));
|
|
887
|
-
const validationPromise = oxyServices.validateSession(sessionId, {
|
|
888
|
-
useHeaderValidation: true
|
|
889
|
-
}).catch(validationError => {
|
|
890
|
-
if (!(0, _errorHandlers.isInvalidSessionError)(validationError) && !(0, _errorHandlers.isTimeoutOrNetworkError)(validationError)) {
|
|
891
|
-
logger('Session validation failed during init', validationError);
|
|
892
|
-
} else if (__DEV__ && (0, _errorHandlers.isTimeoutOrNetworkError)(validationError)) {
|
|
893
|
-
_core.logger.debug('Session validation timeout (expected when offline)', {
|
|
894
|
-
component: 'OxyContext',
|
|
895
|
-
method: 'restoreStoredSession'
|
|
896
|
-
}, validationError);
|
|
897
|
-
}
|
|
898
|
-
return null;
|
|
899
|
-
});
|
|
900
|
-
return Promise.race([validationPromise, timeoutPromise]).then(validation => {
|
|
901
|
-
if (validation === VALIDATION_TIMEOUT_RESULT) {
|
|
902
|
-
return {
|
|
903
|
-
sessionId,
|
|
904
|
-
timedOut: true
|
|
905
|
-
};
|
|
906
|
-
}
|
|
907
|
-
if (validation?.valid && validation.user) {
|
|
908
|
-
const now = new Date();
|
|
909
|
-
const clientSession = {
|
|
910
|
-
sessionId,
|
|
911
|
-
deviceId: '',
|
|
912
|
-
expiresAt: new Date(now.getTime() + DEFAULT_SESSION_VALIDITY_MS).toISOString(),
|
|
913
|
-
lastActive: now.toISOString(),
|
|
914
|
-
userId: validation.user.id?.toString() ?? '',
|
|
915
|
-
isCurrent: sessionId === storedActiveSessionId
|
|
916
|
-
};
|
|
917
|
-
if ((0, _useWebSSO.isWebBrowser)() && sessionId === storedActiveSessionId && storedActiveAuthuser !== null) {
|
|
918
|
-
clientSession.authuser = storedActiveAuthuser;
|
|
919
|
-
}
|
|
920
|
-
return {
|
|
921
|
-
session: clientSession,
|
|
922
|
-
timedOut: false
|
|
923
|
-
};
|
|
924
|
-
}
|
|
925
|
-
return {
|
|
926
|
-
session: null,
|
|
927
|
-
timedOut: false
|
|
928
|
-
};
|
|
929
|
-
});
|
|
930
|
-
}));
|
|
931
|
-
for (const result of results) {
|
|
932
|
-
if (result.status !== 'fulfilled') continue;
|
|
933
|
-
const value = result.value;
|
|
934
|
-
if (value.timedOut) {
|
|
935
|
-
unvalidatedSessionIds.push(value.sessionId);
|
|
936
|
-
} else if (value.session) {
|
|
937
|
-
validSessions.push(value.session);
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
// Persist validated sessions while preserving soft-timed-out IDs. This
|
|
942
|
-
// still clears sessions that were explicitly rejected as invalid, but
|
|
943
|
-
// avoids logging out valid secondary accounts solely because the API was
|
|
944
|
-
// slow during startup.
|
|
945
|
-
updateSessionsRef.current(validSessions, {
|
|
946
|
-
merge: false,
|
|
947
|
-
preserveSessionIds: unvalidatedSessionIds
|
|
948
|
-
});
|
|
949
|
-
}
|
|
950
|
-
if (storedActiveSessionId) {
|
|
484
|
+
}, [oxyServices, authStore]);
|
|
485
|
+
|
|
486
|
+
// ── Session commit funnel ────────────────────────────────────────────────
|
|
487
|
+
// The single place a session becomes committed: plant tokens, persist the
|
|
488
|
+
// rotating refresh family, register the account into the server-authoritative
|
|
489
|
+
// device set, and hydrate the full user. Used by the QR device flow
|
|
490
|
+
// (`handleWebSession`), password sign-in, 2FA completion, and the cold boot.
|
|
491
|
+
const commitSession = (0, _react.useCallback)(async (input, options) => {
|
|
492
|
+
if (input.accessToken) {
|
|
493
|
+
oxyServices.setTokens(input.accessToken);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Persist the durable blob when a rotating refresh token is present
|
|
497
|
+
// (trusted-lane sign-ins). The cold boot has usually persisted already;
|
|
498
|
+
// this is idempotent. A missing refresh token means the session survives
|
|
499
|
+
// reload via the device cookie (web) / shared keychain (native) instead.
|
|
500
|
+
if (input.refreshToken && input.userId) {
|
|
951
501
|
try {
|
|
952
|
-
await
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
// below to promote a surviving valid session — a stale/removed active
|
|
971
|
-
// marker must not strand the other still-valid accounts.
|
|
972
|
-
await storage.removeItem(storageKeys.activeSessionId);
|
|
973
|
-
validSessions = validSessions.filter(session => session.sessionId !== storedActiveSessionId);
|
|
974
|
-
updateSessionsRef.current(validSessions, {
|
|
975
|
-
merge: false,
|
|
976
|
-
preserveSessionIds: unvalidatedSessionIds
|
|
977
|
-
});
|
|
978
|
-
// Don't log expected session errors during restoration
|
|
979
|
-
} else if ((0, _errorHandlers.isTimeoutOrNetworkError)(switchError)) {
|
|
980
|
-
// Timeout/network — the stored active session may still be valid; we
|
|
981
|
-
// just could not confirm it. Do NOT elect a different session (that
|
|
982
|
-
// could switch accounts under the user); leave restore to a retry / the
|
|
983
|
-
// remaining cold-boot steps.
|
|
984
|
-
if (__DEV__) {
|
|
985
|
-
_core.logger.debug('Active session validation timeout (expected when offline)', {
|
|
986
|
-
component: 'OxyContext',
|
|
987
|
-
method: 'restoreStoredSession'
|
|
988
|
-
}, switchError);
|
|
989
|
-
}
|
|
990
|
-
return false;
|
|
991
|
-
} else {
|
|
992
|
-
// Only log unexpected errors, and do not elect on an ambiguous failure.
|
|
993
|
-
logger('Active session validation error', switchError);
|
|
994
|
-
return false;
|
|
995
|
-
}
|
|
502
|
+
const deviceToken = (await authStore.loadDeviceToken()) ?? undefined;
|
|
503
|
+
const next = {
|
|
504
|
+
sessionId: input.sessionId,
|
|
505
|
+
refreshToken: input.refreshToken,
|
|
506
|
+
userId: input.userId,
|
|
507
|
+
...(deviceToken ? {
|
|
508
|
+
deviceToken
|
|
509
|
+
} : {}),
|
|
510
|
+
...(input.accessToken ? {
|
|
511
|
+
accessToken: input.accessToken
|
|
512
|
+
} : {}),
|
|
513
|
+
...(input.expiresAt ? {
|
|
514
|
+
expiresAt: input.expiresAt
|
|
515
|
+
} : {})
|
|
516
|
+
};
|
|
517
|
+
await authStore.save(next);
|
|
518
|
+
} catch (persistError) {
|
|
519
|
+
logger('Failed to persist auth state on commit', persistError);
|
|
996
520
|
}
|
|
997
521
|
}
|
|
998
522
|
|
|
999
|
-
//
|
|
1000
|
-
//
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
} catch (electionError) {
|
|
1015
|
-
// Activation requires an in-memory bearer on web; without one (a plain
|
|
1016
|
-
// web reload) the switch throws and recovery defers to the cross-domain
|
|
1017
|
-
// cold-boot steps. Invalid/timeout/network are expected here — surface
|
|
1018
|
-
// only genuinely unexpected failures.
|
|
1019
|
-
if (!(0, _errorHandlers.isInvalidSessionError)(electionError) && !(0, _errorHandlers.isTimeoutOrNetworkError)(electionError)) {
|
|
1020
|
-
logger('Elected session activation error', electionError);
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
523
|
+
// Fast local mirror so the UI updates before the server round-trip; the
|
|
524
|
+
// SessionClient projection below overwrites it with server truth.
|
|
525
|
+
if (input.userId) {
|
|
526
|
+
const now = new Date();
|
|
527
|
+
updateSessions([{
|
|
528
|
+
sessionId: input.sessionId,
|
|
529
|
+
deviceId: input.deviceId ?? '',
|
|
530
|
+
expiresAt: input.expiresAt || new Date(now.getTime() + DEFAULT_SESSION_VALIDITY_MS).toISOString(),
|
|
531
|
+
lastActive: now.toISOString(),
|
|
532
|
+
userId: input.userId,
|
|
533
|
+
isCurrent: true
|
|
534
|
+
}], {
|
|
535
|
+
merge: true
|
|
536
|
+
});
|
|
537
|
+
setActiveSessionId(input.sessionId);
|
|
1023
538
|
}
|
|
1024
|
-
return false;
|
|
1025
|
-
}, [logger, oxyServices, storage, storageKeys.activeSessionId, storageKeys.sessionIds, storageKeys.priorSession]);
|
|
1026
|
-
|
|
1027
|
-
// Shared in-flight `runSsoReturn` promise — see the CONCURRENCY note on
|
|
1028
|
-
// `runSsoReturn` below. Lets the eager interception effect and the cold-boot
|
|
1029
|
-
// `sso-return` step share one `consumeSsoReturn` invocation race-free.
|
|
1030
|
-
const inFlightSsoReturnRef = (0, _react.useRef)(null);
|
|
1031
539
|
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
// the page was (re)shown.
|
|
1043
|
-
//
|
|
1044
|
-
// Returns `true` when a session was committed (caller short-circuits), `false`
|
|
1045
|
-
// otherwise. Off-browser `consumeSsoReturn` is a no-op returning `null`, so
|
|
1046
|
-
// this returns `false` (native never reaches it).
|
|
1047
|
-
//
|
|
1048
|
-
// CONCURRENCY: the eager SSO-callback interception effect and the cold-boot
|
|
1049
|
-
// `sso-return` step can both invoke this in the same tick when we land on the
|
|
1050
|
-
// callback path. `consumeSsoReturn` strips the fragment FIRST, so a naive
|
|
1051
|
-
// second invocation would parse an already-stripped URL and return `null` —
|
|
1052
|
-
// leaving the losing caller with no session (and on an `ok` outcome could let
|
|
1053
|
-
// the terminal bounce fire spuriously). The FIRST call's promise is memoised in
|
|
1054
|
-
// `inFlightSsoReturnRef` and SHARED with every concurrent caller, so the single
|
|
1055
|
-
// `consumeSsoReturn` invocation — and its single commit via `handleWebSSOSession`
|
|
1056
|
-
// — is delivered identically to both paths. Cleared once it settles so a later,
|
|
1057
|
-
// genuinely-separate return (e.g. a bfcache restore) runs a fresh pass.
|
|
1058
|
-
const runSsoReturn = (0, _react.useCallback)(() => {
|
|
1059
|
-
if (inFlightSsoReturnRef.current) {
|
|
1060
|
-
return inFlightSsoReturnRef.current;
|
|
1061
|
-
}
|
|
1062
|
-
const inFlight = (0, _core.consumeSsoReturn)(oxyServices, {
|
|
1063
|
-
isWeb: _useWebSSO.isWebBrowser,
|
|
1064
|
-
onExchangeError: error => {
|
|
1065
|
-
if (__DEV__) {
|
|
1066
|
-
_core.logger.debug('SSO code exchange failed (treating as no session)', {
|
|
1067
|
-
component: 'OxyContext',
|
|
1068
|
-
method: 'runSsoReturn'
|
|
1069
|
-
}, error);
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
}).then(async session => {
|
|
1073
|
-
if (!session) {
|
|
1074
|
-
return false;
|
|
1075
|
-
}
|
|
1076
|
-
const commitWebSession = handleWebSSOSessionRef.current;
|
|
1077
|
-
if (!commitWebSession) {
|
|
1078
|
-
return false;
|
|
540
|
+
// Register into the device set. A deliberate sign-in ACTIVATES the account
|
|
541
|
+
// (`registerAndActivate`); the cold boot only ensures membership and lets
|
|
542
|
+
// the server's own `activeAccountId` win (`addCurrentAccount`). Both then
|
|
543
|
+
// start the socket + project. Best-effort — a failure never fails the
|
|
544
|
+
// sign-in (cold boot re-registers on the next load).
|
|
545
|
+
try {
|
|
546
|
+
if (options.activate) {
|
|
547
|
+
await sessionClient.registerAndActivate(input.userId);
|
|
548
|
+
} else {
|
|
549
|
+
await sessionClient.addCurrentAccount();
|
|
1079
550
|
}
|
|
1080
|
-
await
|
|
1081
|
-
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
}, [oxyServices]);
|
|
1088
|
-
|
|
1089
|
-
// Cold boot — the single, ordered, short-circuit session-recovery sequence,
|
|
1090
|
-
// consuming the SAME `runColdBoot` core primitive as `WebOxyProvider`. The
|
|
1091
|
-
// FIRST step that yields a session wins; every later step is skipped. Each
|
|
1092
|
-
// web-only step is gated by `isWebBrowser()`, so on native ONLY
|
|
1093
|
-
// `stored-session` runs.
|
|
1094
|
-
//
|
|
1095
|
-
// Order (web): SSO return → stored session → shared-key sign-in (native
|
|
1096
|
-
// only, disabled here) → silent iframe (per-apex, the durable reload path)
|
|
1097
|
-
// → SSO bounce (terminal). This is a pure TOKEN-ACQUISITION ladder — it
|
|
1098
|
-
// mints the first per-domain access token by whichever means recovers one
|
|
1099
|
-
// fastest. Once a token is acquired (or the ladder exhausts), the
|
|
1100
|
-
// SERVER-authoritative `SessionClient` takes over: `addCurrentAccount` +
|
|
1101
|
-
// `start` bootstrap the device session set and multi-account/active-account
|
|
1102
|
-
// state from `GET /session/device/state`, so WHICH account is active is
|
|
1103
|
-
// decided server-side, not by a client-persisted slot. There is no oxy_rt
|
|
1104
|
-
// refresh-cookie restore step in this ladder, and no FedCM step — FedCM was
|
|
1105
|
-
// removed from the client sign-in/cold-boot path entirely (Chrome-only, and
|
|
1106
|
-
// its fast/silent failure mode combined with a caller's auth-guard effect
|
|
1107
|
-
// could re-trigger sign-in in a tight loop; see `CrossDomainAuth`'s doc
|
|
1108
|
-
// comment in `@oxyhq/core`).
|
|
1109
|
-
//
|
|
1110
|
-
// LATENCY (FIX A): `stored-session` runs BEFORE the slow no-redirect
|
|
1111
|
-
// `silent-iframe` probe. On a normal reload the local bearer validates in
|
|
1112
|
-
// one round-trip and wins, so `runColdBoot` short-circuits and never sits
|
|
1113
|
-
// through that probe's timeout. `sso-return` MUST stay first — it consumes
|
|
1114
|
-
// the URL fragment before anything can strip it. On a first visit with no
|
|
1115
|
-
// local session, `stored-session` skips and the cross-domain fallback chain
|
|
1116
|
-
// (silent-iframe → sso-bounce) runs exactly as before; the per-apex silent
|
|
1117
|
-
// iframe still restores a durable cross-domain session on reload WITHOUT a
|
|
1118
|
-
// top-level bounce, so when it wins `sso-bounce` never fires (no flash, no
|
|
1119
|
-
// loop).
|
|
1120
|
-
// Order (native): stored session only (every web-only step is disabled
|
|
1121
|
-
// off-browser).
|
|
1122
|
-
const restoreSessionsFromStorage = (0, _react.useCallback)(async () => {
|
|
1123
|
-
if (!storage) {
|
|
1124
|
-
return;
|
|
551
|
+
await sessionClient.start();
|
|
552
|
+
await syncFromClient();
|
|
553
|
+
} catch (registrationError) {
|
|
554
|
+
_core.logger.warn('commitSession: device-set registration failed', {
|
|
555
|
+
component: 'OxyContext',
|
|
556
|
+
method: 'commitSession'
|
|
557
|
+
}, registrationError);
|
|
1125
558
|
}
|
|
1126
|
-
setTokenReady(false);
|
|
1127
|
-
// Fresh per-boot flag — see the declaration comment above `sessionClient`.
|
|
1128
|
-
registeredDuringBootRef.current = false;
|
|
1129
|
-
const commitWebSession = handleWebSSOSessionRef.current;
|
|
1130
559
|
|
|
1131
|
-
//
|
|
1132
|
-
//
|
|
1133
|
-
|
|
1134
|
-
// so it is explicitly skipped once a local session won. `runColdBoot`
|
|
1135
|
-
// already short-circuits on the first `{kind:'session'}`, so on a winning
|
|
1136
|
-
// reload that `enabled` body is never even reached — this flag makes the
|
|
1137
|
-
// intent explicit and is redundant-safe. On a first-visit-no-local-session,
|
|
1138
|
-
// `stored-session` skips, this stays false, and the probe runs as before.
|
|
1139
|
-
let storedSessionRestored = false;
|
|
1140
|
-
|
|
1141
|
-
// FIX-B smart-gate input: has this device/app EVER had a signed-in Oxy
|
|
1142
|
-
// session (the durable `priorSession` hint, set on every commit, cleared
|
|
1143
|
-
// only on explicit full sign-out)? Read ONCE here — synchronously usable by
|
|
1144
|
-
// the terminal `sso-bounce` `enabled` gate below — so a RETURNING visitor
|
|
1145
|
-
// whose local session has lapsed still earns one `/sso` establish bounce,
|
|
1146
|
-
// while a truly first-time anonymous visitor is never force-redirected.
|
|
1147
|
-
// `storage` is non-null (guarded above); a read failure is treated as "no
|
|
1148
|
-
// prior session" (fail safe toward anonymous-browse).
|
|
1149
|
-
let hadPriorSession = false;
|
|
560
|
+
// Hydrate the full user (the commit input carries only minimal data). Fall
|
|
561
|
+
// back to the minimal shape if the profile fetch fails.
|
|
562
|
+
let fullUser = null;
|
|
1150
563
|
try {
|
|
1151
|
-
|
|
1152
|
-
} catch (
|
|
564
|
+
fullUser = await oxyServices.getCurrentUser();
|
|
565
|
+
} catch (profileError) {
|
|
1153
566
|
if (__DEV__) {
|
|
1154
|
-
_core.logger.debug('Failed to
|
|
567
|
+
_core.logger.debug('Failed to fetch full user on commit; using minimal fallback', {
|
|
1155
568
|
component: 'OxyContext',
|
|
1156
|
-
method: '
|
|
1157
|
-
},
|
|
569
|
+
method: 'commitSession'
|
|
570
|
+
}, profileError);
|
|
1158
571
|
}
|
|
572
|
+
fullUser = input.user ?? null;
|
|
573
|
+
}
|
|
574
|
+
if (fullUser) {
|
|
575
|
+
loginSuccess(fullUser);
|
|
576
|
+
onAuthStateChangeRef.current?.(fullUser);
|
|
1159
577
|
}
|
|
578
|
+
markAuthResolvedRef.current();
|
|
579
|
+
}, [oxyServices, authStore, updateSessions, setActiveSessionId, sessionClient, syncFromClient, loginSuccess, logger]);
|
|
580
|
+
const commitSessionRef = (0, _react.useRef)(commitSession);
|
|
581
|
+
commitSessionRef.current = commitSession;
|
|
582
|
+
|
|
583
|
+
// Public `handleWebSession`: commit a session from the QR device flow. It is a
|
|
584
|
+
// deliberate sign-in on THIS device, so it activates the account.
|
|
585
|
+
const handleWebSession = (0, _react.useCallback)(async session => {
|
|
586
|
+
if (!session?.user || !session?.sessionId || !session.accessToken) {
|
|
587
|
+
throw new Error('Session response did not include a usable session');
|
|
588
|
+
}
|
|
589
|
+
await commitSession({
|
|
590
|
+
sessionId: session.sessionId,
|
|
591
|
+
accessToken: session.accessToken,
|
|
592
|
+
// deviceFlow threads a rotating refresh token on the runtime object
|
|
593
|
+
// even though `SessionLoginResponse` does not type it.
|
|
594
|
+
refreshToken: session.refreshToken,
|
|
595
|
+
deviceId: session.deviceId,
|
|
596
|
+
expiresAt: session.expiresAt,
|
|
597
|
+
userId: session.user.id,
|
|
598
|
+
user: session.user
|
|
599
|
+
}, {
|
|
600
|
+
activate: true
|
|
601
|
+
});
|
|
602
|
+
}, [commitSession]);
|
|
1160
603
|
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
604
|
+
// Keyless password sign-in: username/email + password.
|
|
605
|
+
const signInWithPassword = (0, _react.useCallback)(async (identifier, password, opts) => {
|
|
606
|
+
const deviceToken = (await authStore.loadDeviceToken()) ?? undefined;
|
|
607
|
+
const result = await oxyServices.passwordSignIn(identifier, password, {
|
|
608
|
+
deviceToken,
|
|
609
|
+
deviceName: opts?.deviceName,
|
|
610
|
+
deviceFingerprint: opts?.deviceFingerprint
|
|
611
|
+
});
|
|
612
|
+
if ('twoFactorRequired' in result) {
|
|
613
|
+
return {
|
|
614
|
+
status: '2fa_required',
|
|
615
|
+
loginToken: result.loginToken
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
// `passwordSignIn` already planted the access token on the session arm.
|
|
619
|
+
await commitSession({
|
|
620
|
+
sessionId: result.sessionId,
|
|
621
|
+
accessToken: result.accessToken,
|
|
622
|
+
refreshToken: result.refreshToken,
|
|
623
|
+
deviceId: result.deviceId,
|
|
624
|
+
expiresAt: result.expiresAt,
|
|
625
|
+
userId: result.user.id,
|
|
626
|
+
user: result.user
|
|
627
|
+
}, {
|
|
628
|
+
activate: true
|
|
629
|
+
});
|
|
630
|
+
return {
|
|
631
|
+
status: 'ok'
|
|
632
|
+
};
|
|
633
|
+
}, [oxyServices, authStore, commitSession]);
|
|
634
|
+
const completeTwoFactorSignIn = (0, _react.useCallback)(async params => {
|
|
635
|
+
const deviceToken = (await authStore.loadDeviceToken()) ?? undefined;
|
|
636
|
+
const result = await oxyServices.completeTwoFactorSignIn({
|
|
637
|
+
loginToken: params.loginToken,
|
|
638
|
+
token: params.token,
|
|
639
|
+
backupCode: params.backupCode,
|
|
640
|
+
deviceToken,
|
|
641
|
+
deviceName: params.deviceName
|
|
642
|
+
});
|
|
643
|
+
await commitSession({
|
|
644
|
+
sessionId: result.sessionId,
|
|
645
|
+
accessToken: result.accessToken,
|
|
646
|
+
refreshToken: result.refreshToken,
|
|
647
|
+
deviceId: result.deviceId,
|
|
648
|
+
expiresAt: result.expiresAt,
|
|
649
|
+
userId: result.user.id,
|
|
650
|
+
user: result.user
|
|
651
|
+
}, {
|
|
652
|
+
activate: true
|
|
653
|
+
});
|
|
654
|
+
}, [oxyServices, authStore, commitSession]);
|
|
655
|
+
|
|
656
|
+
// ── Cold boot ────────────────────────────────────────────────────────────
|
|
657
|
+
// The single device-first session restore. Runs ONCE after storage init.
|
|
658
|
+
// `runSessionColdBoot` resolves the device's session (bootstrap-return →
|
|
659
|
+
// stored-tokens → shared-key (native) → bootstrap-hop (web)) WITHOUT ever
|
|
660
|
+
// redirecting to a login page; an unresolved boot ends signed-out and the app
|
|
661
|
+
// renders its "Sign in with Oxy" affordance. On a winning boot the token is
|
|
662
|
+
// already planted + persisted, so `onSession` only hands off to the
|
|
663
|
+
// SessionClient (membership + active account + realtime socket) and marks
|
|
664
|
+
// auth resolved. `onSignedOut` marks auth resolved with no session.
|
|
665
|
+
const runColdBoot = (0, _react.useCallback)(async () => {
|
|
666
|
+
setTokenReady(false);
|
|
1172
667
|
try {
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
// ORDERING (FIX A): this step now runs BEFORE the slow web-only
|
|
1198
|
-
// `silent-iframe` probe. On a normal reload the local bearer
|
|
1199
|
-
// validates in one round-trip and wins; `runColdBoot`
|
|
1200
|
-
// then short-circuits and never even evaluates the slow
|
|
1201
|
-
// no-redirect probe that would otherwise time out (the ~20-30s
|
|
1202
|
-
// serial stall). The `sso-return` step stays AHEAD of this one —
|
|
1203
|
-
// it must consume the URL fragment before any later step (or
|
|
1204
|
-
// anything else) strips it. On a first visit with no local
|
|
1205
|
-
// session this step skips and the cross-domain fallback chain
|
|
1206
|
-
// (silent-iframe → sso-bounce) runs exactly as before.
|
|
1207
|
-
id: 'stored-session',
|
|
1208
|
-
run: async () => {
|
|
1209
|
-
const restored = await restoreStoredSession();
|
|
1210
|
-
if (restored) {
|
|
1211
|
-
// FIX-B: record the win so the slow probes below explicitly skip
|
|
1212
|
-
// (belt-and-suspenders; `runColdBoot` already short-circuits).
|
|
1213
|
-
storedSessionRestored = true;
|
|
1214
|
-
return {
|
|
1215
|
-
kind: 'session',
|
|
1216
|
-
session: true
|
|
1217
|
-
};
|
|
1218
|
-
}
|
|
1219
|
-
return {
|
|
1220
|
-
kind: 'skip'
|
|
1221
|
-
};
|
|
1222
|
-
}
|
|
1223
|
-
}, {
|
|
1224
|
-
// 2.5) Shared-key SSO (NATIVE ONLY) — same-device, no interaction.
|
|
1225
|
-
//
|
|
1226
|
-
// When a sibling Oxy app (the Commons identity vault) has written
|
|
1227
|
-
// the cross-app shared identity into the `group.so.oxy.shared`
|
|
1228
|
-
// keychain, this mints THIS app's session from it silently:
|
|
1229
|
-
// `signInWithSharedIdentity()` proves control of the shared key
|
|
1230
|
-
// (challenge → sign → verify, which plants the tokens server-side)
|
|
1231
|
-
// and returns a `SessionLoginResponse`, which we commit through the
|
|
1232
|
-
// SAME path the web SSO steps use (`handleWebSSOSession`), so state,
|
|
1233
|
-
// durable persistence, profile fetch, and `markAuthResolved` all run
|
|
1234
|
-
// identically. Returns `null` when no shared identity is present, so
|
|
1235
|
-
// a device without Commons simply falls through.
|
|
1236
|
-
//
|
|
1237
|
-
// ORDERING: runs immediately AFTER `stored-session` (an already
|
|
1238
|
-
// restored local bearer always wins first — `storedSessionRestored`
|
|
1239
|
-
// gates this off) and BEFORE the web-only probes (which are gated
|
|
1240
|
-
// off on native anyway). Native-only: `enabled` requires
|
|
1241
|
-
// `!isWebBrowser()`, and the core method also returns `null` on web,
|
|
1242
|
-
// so `WebOxyProvider` / the web path are entirely unaffected.
|
|
1243
|
-
// Bounded by `SHARED_KEY_SIGNIN_TIMEOUT` so a slow/offline network
|
|
1244
|
-
// cannot stall cold boot.
|
|
1245
|
-
id: 'shared-key-signin',
|
|
1246
|
-
enabled: () => !(0, _useWebSSO.isWebBrowser)() && !storedSessionRestored,
|
|
1247
|
-
run: async () => {
|
|
1248
|
-
if (!commitWebSession) {
|
|
1249
|
-
return {
|
|
1250
|
-
kind: 'skip'
|
|
1251
|
-
};
|
|
1252
|
-
}
|
|
1253
|
-
let timeoutId;
|
|
1254
|
-
const session = await Promise.race([oxyServices.signInWithSharedIdentity?.() ?? Promise.resolve(null), new Promise(resolve => {
|
|
1255
|
-
timeoutId = setTimeout(() => resolve(null), SHARED_KEY_SIGNIN_TIMEOUT);
|
|
1256
|
-
})]).finally(() => {
|
|
1257
|
-
if (timeoutId !== undefined) {
|
|
1258
|
-
clearTimeout(timeoutId);
|
|
1259
|
-
}
|
|
1260
|
-
});
|
|
1261
|
-
if (!session) {
|
|
1262
|
-
return {
|
|
1263
|
-
kind: 'skip'
|
|
1264
|
-
};
|
|
1265
|
-
}
|
|
1266
|
-
await commitWebSession(session);
|
|
1267
|
-
// Record the win so the (web-only) probes below explicitly skip —
|
|
1268
|
-
// belt-and-suspenders; `runColdBoot` already short-circuits on the
|
|
1269
|
-
// first `{kind:'session'}`, and those probes are off on native.
|
|
1270
|
-
storedSessionRestored = true;
|
|
1271
|
-
return {
|
|
1272
|
-
kind: 'session',
|
|
1273
|
-
session: true
|
|
1274
|
-
};
|
|
1275
|
-
}
|
|
1276
|
-
}, {
|
|
1277
|
-
// 3) First-party silent iframe at the PER-APEX IdP — the DURABLE
|
|
1278
|
-
// cross-domain reload-restore path. The durable session lives as a
|
|
1279
|
-
// first-party `fedcm_session` cookie on `auth.<rp-apex>` (e.g.
|
|
1280
|
-
// `auth.mention.earth`), established during the `/sso` bounce's
|
|
1281
|
-
// `/sso/establish` hop. That host is SAME-SITE to the RP page, so
|
|
1282
|
-
// the cookie is first-party under Safari ITP / Firefox TCP — and
|
|
1283
|
-
// an iframe read is NOT a top-level navigation, so it restores on
|
|
1284
|
-
// reload with NO flash and works in a backgrounded tab. This is the
|
|
1285
|
-
// step that prevents the re-bounce loop: when it finds a session,
|
|
1286
|
-
// the terminal `sso-bounce` never fires.
|
|
1287
|
-
//
|
|
1288
|
-
// The per-apex iframe mint itself lives in
|
|
1289
|
-
// `mintSessionViaPerApexIframe` (shared verbatim with the in-session
|
|
1290
|
-
// refresh handler so the two paths can never drift): it points the
|
|
1291
|
-
// iframe at `autoDetectAuthWebUrl()` and skips when there is no
|
|
1292
|
-
// per-apex IdP (localhost/IP/single-label/off-browser). Web only; on
|
|
1293
|
-
// native `isWebBrowser()` gates it off, so native never runs an
|
|
1294
|
-
// iframe.
|
|
1295
|
-
//
|
|
1296
|
-
// FIX-B: additionally skipped when `stored-session` already won.
|
|
1297
|
-
// FIX-D: bounded by `SILENT_IFRAME_TIMEOUT` (plus `iframe.onerror`
|
|
1298
|
-
// fail-fast in core) so a no-message iframe cannot stall cold boot.
|
|
1299
|
-
id: 'silent-iframe',
|
|
1300
|
-
enabled: () => !storedSessionRestored && !silentRestoreBlocked && (0, _useWebSSO.isWebBrowser)(),
|
|
1301
|
-
run: async () => {
|
|
1302
|
-
if (!commitWebSession) {
|
|
1303
|
-
return {
|
|
1304
|
-
kind: 'skip'
|
|
1305
|
-
};
|
|
1306
|
-
}
|
|
1307
|
-
// Shared with the in-session refresh handler: the ONE
|
|
1308
|
-
// implementation of "mint a first-party per-apex token without a
|
|
1309
|
-
// reload" lives in `silentSessionRestore` so cold boot and refresh
|
|
1310
|
-
// never drift.
|
|
1311
|
-
const session = await (0, _silentSessionRestore.mintSessionViaPerApexIframe)(oxyServices, SILENT_IFRAME_TIMEOUT);
|
|
1312
|
-
if (!session) {
|
|
1313
|
-
return {
|
|
1314
|
-
kind: 'skip'
|
|
1315
|
-
};
|
|
1316
|
-
}
|
|
1317
|
-
await commitWebSession(session);
|
|
1318
|
-
return {
|
|
1319
|
-
kind: 'session',
|
|
1320
|
-
session: true
|
|
1321
|
-
};
|
|
1322
|
-
}
|
|
1323
|
-
}, {
|
|
1324
|
-
// 4) SSO bounce (TERMINAL, web only, at most once). No local session
|
|
1325
|
-
// was found by any step above. Top-level navigate to the central
|
|
1326
|
-
// `auth.oxy.so/sso?prompt=none` so the IdP can either mint a session
|
|
1327
|
-
// (returning an opaque code we exchange on the callback) or report
|
|
1328
|
-
// `none`. This step tears the document down on success — its `skip`
|
|
1329
|
-
// result is only observed if `assign` no-ops. Disabled on the IdP
|
|
1330
|
-
// itself, once the NO_SESSION flag is set, or while a bounce guard is
|
|
1331
|
-
// still active (loop + self-heal protection).
|
|
1332
|
-
id: 'sso-bounce',
|
|
1333
|
-
enabled: () => {
|
|
1334
|
-
// Smart gate (SDK-owned, shared with `WebOxyProvider` via core's
|
|
1335
|
-
// `allowSsoBounce`). The terminal `/sso` establish-bounce is the
|
|
1336
|
-
// ONLY cold-boot step that can recover a session living SOLELY at
|
|
1337
|
-
// the central IdP (a cross-apex RP whose local session expired),
|
|
1338
|
-
// and it is what plants the per-apex `fedcm_session` cookie the
|
|
1339
|
-
// earlier silent-iframe step relies on. So it is allowed iff a
|
|
1340
|
-
// prior-signed-in hint exists OR a local session was recovered this
|
|
1341
|
-
// boot (`storedSessionRestored`, always false here — an earlier step
|
|
1342
|
-
// would have won — but passed for spec fidelity): a RETURNING user
|
|
1343
|
-
// still gets ONE bounce, while a truly first-time anonymous visitor
|
|
1344
|
-
// does NOT (anonymous browse). The per-tab loop guards below
|
|
1345
|
-
// (`ssoNoSessionKey`, `ssoAttemptedKey`, `guardActive`) still cap an
|
|
1346
|
-
// allowed bounce at one per cold boot.
|
|
1347
|
-
if (!(0, _core.allowSsoBounce)({
|
|
1348
|
-
hasPriorSession: hadPriorSession,
|
|
1349
|
-
hasLocalSession: storedSessionRestored
|
|
1350
|
-
})) {
|
|
1351
|
-
return false;
|
|
1352
|
-
}
|
|
1353
|
-
if (!(0, _useWebSSO.isWebBrowser)() || window.top !== window.self) {
|
|
1354
|
-
return false;
|
|
1355
|
-
}
|
|
1356
|
-
const origin = window.location.origin;
|
|
1357
|
-
if ((0, _core.isCentralIdPOrigin)(origin)) {
|
|
1358
|
-
return false;
|
|
1359
|
-
}
|
|
1360
|
-
if (window.sessionStorage.getItem((0, _core.ssoNoSessionKey)(origin)) === '1') {
|
|
1361
|
-
return false;
|
|
1362
|
-
}
|
|
1363
|
-
if (window.sessionStorage.getItem((0, _core.ssoAttemptedKey)(origin)) === '1') {
|
|
1364
|
-
return false;
|
|
1365
|
-
}
|
|
1366
|
-
if ((0, _core.guardActive)(window.sessionStorage, origin, Date.now())) {
|
|
1367
|
-
return false;
|
|
668
|
+
await (0, _core.runSessionColdBoot)({
|
|
669
|
+
oxy: oxyServices,
|
|
670
|
+
store: authStore,
|
|
671
|
+
platform: {
|
|
672
|
+
isWeb: (0, _isWebBrowser.isWebBrowser)(),
|
|
673
|
+
isNative: !(0, _isWebBrowser.isWebBrowser)()
|
|
674
|
+
},
|
|
675
|
+
onSession: async session => {
|
|
676
|
+
// Bound how long auth resolution waits for the handoff — the token is
|
|
677
|
+
// already planted, so on a slow backend we proceed and let the handoff
|
|
678
|
+
// land asynchronously (the socket subscription re-projects when it does).
|
|
679
|
+
const handoff = commitSessionRef.current({
|
|
680
|
+
sessionId: session.sessionId,
|
|
681
|
+
accessToken: session.accessToken,
|
|
682
|
+
userId: session.userId
|
|
683
|
+
}, {
|
|
684
|
+
activate: false
|
|
685
|
+
});
|
|
686
|
+
let handoffDeadlineId;
|
|
687
|
+
await Promise.race([handoff, new Promise(resolve => {
|
|
688
|
+
handoffDeadlineId = setTimeout(resolve, SESSION_HANDOFF_DEADLINE);
|
|
689
|
+
})]).finally(() => {
|
|
690
|
+
if (handoffDeadlineId !== undefined) {
|
|
691
|
+
clearTimeout(handoffDeadlineId);
|
|
1368
692
|
}
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
window.sessionStorage.setItem((0, _core.ssoGuardKey)(origin), String(Date.now()));
|
|
1376
|
-
window.sessionStorage.setItem((0, _core.ssoDestKey)(origin), window.location.href);
|
|
1377
|
-
// OUTCOME-INDEPENDENT once-guard: mark the probe attempted the instant we
|
|
1378
|
-
// commit to the bounce, so even if the callback never lands cleanly no
|
|
1379
|
-
// second bounce can ever fire this tab (the definitive loop breaker).
|
|
1380
|
-
window.sessionStorage.setItem((0, _core.ssoAttemptedKey)(origin), '1');
|
|
1381
|
-
const url = (0, _core.buildSsoBounceUrl)(origin, state, oxyServices.config?.authWebUrl);
|
|
1382
|
-
|
|
1383
|
-
// TERMINAL: the document is torn down by this navigation. The
|
|
1384
|
-
// `skip` below is only reached if `assign` is a no-op (e.g. the
|
|
1385
|
-
// navigation is blocked); in that case we fall through
|
|
1386
|
-
// unauthenticated, which is correct.
|
|
1387
|
-
(0, _core.ssoNavigate)(url);
|
|
1388
|
-
return {
|
|
1389
|
-
kind: 'skip'
|
|
1390
|
-
};
|
|
1391
|
-
}
|
|
1392
|
-
}],
|
|
693
|
+
});
|
|
694
|
+
markAuthResolvedRef.current();
|
|
695
|
+
},
|
|
696
|
+
onSignedOut: () => {
|
|
697
|
+
markAuthResolvedRef.current();
|
|
698
|
+
},
|
|
1393
699
|
onStepError: (id, error) => {
|
|
1394
700
|
if (__DEV__) {
|
|
1395
701
|
_core.logger.debug(`Cold-boot step "${id}" errored (non-fatal, falling through)`, {
|
|
1396
702
|
component: 'OxyContext',
|
|
1397
|
-
method: '
|
|
703
|
+
method: 'runColdBoot'
|
|
1398
704
|
}, error);
|
|
1399
705
|
}
|
|
1400
|
-
},
|
|
1401
|
-
// Defense-in-depth: a single step whose promise never settles can no
|
|
1402
|
-
// longer block the chain forever. On expiry the runner keeps iterating
|
|
1403
|
-
// to the terminal `sso-bounce`
|
|
1404
|
-
// step so a genuine no-local-session visit still reaches the
|
|
1405
|
-
// cross-domain `/sso` fallback; the `finally` backstop flips
|
|
1406
|
-
// `authResolved` regardless. See `COLD_BOOT_OVERALL_DEADLINE`.
|
|
1407
|
-
overallDeadlineMs: COLD_BOOT_OVERALL_DEADLINE,
|
|
1408
|
-
onStepDeadline: id => {
|
|
1409
|
-
if (__DEV__) {
|
|
1410
|
-
_core.logger.debug(`Cold-boot step "${id}" exceeded the overall deadline (abandoned, falling through)`, {
|
|
1411
|
-
component: 'OxyContext',
|
|
1412
|
-
method: 'restoreSessionsFromStorage'
|
|
1413
|
-
});
|
|
1414
|
-
}
|
|
1415
706
|
}
|
|
1416
707
|
});
|
|
1417
|
-
if (__DEV__ && outcome.kind === 'session') {
|
|
1418
|
-
_core.logger.debug(`Cold boot recovered a session via "${outcome.via}"`, {
|
|
1419
|
-
component: 'OxyContext',
|
|
1420
|
-
method: 'restoreSessionsFromStorage'
|
|
1421
|
-
});
|
|
1422
|
-
}
|
|
1423
|
-
|
|
1424
|
-
// TOKEN LADDER → SESSIONCLIENT AUTHORITY HANDOFF. The steps above are
|
|
1425
|
-
// ONLY a token-acquisition ladder — they mint the first per-domain
|
|
1426
|
-
// access token by whichever means recovers one fastest. Once a session
|
|
1427
|
-
// is known (either this cold boot committed one via the ladder, or an
|
|
1428
|
-
// access token is already held in memory — e.g. a prior render already
|
|
1429
|
-
// planted it), hand off to the server-authoritative `SessionClient`:
|
|
1430
|
-
// `addCurrentAccount` registers this recovered account+session into the
|
|
1431
|
-
// server `DeviceSession` (derives identity from the bearer), `start`
|
|
1432
|
-
// bootstraps the full device-session state (server `activeAccountId` +
|
|
1433
|
-
// `activeToken`) and connects the realtime socket, and `syncFromClient`
|
|
1434
|
-
// projects that state onto the exposed `sessions`/`activeSessionId`/
|
|
1435
|
-
// `user`. This is what lets a switched (managed/org) account survive
|
|
1436
|
-
// reload: the SERVER, not a client-persisted slot, owns which account is
|
|
1437
|
-
// active. Never call the client when no session was acquired — an
|
|
1438
|
-
// anonymous visitor must stay logged out. Failures are logged and
|
|
1439
|
-
// swallowed; they must never throw out of cold boot.
|
|
1440
|
-
//
|
|
1441
|
-
// `addCurrentAccount` is SKIPPED when `registeredDuringBootRef` is
|
|
1442
|
-
// already `true` — every ladder step except `stored-session` commits
|
|
1443
|
-
// through `handleWebSSOSession`, which registers the account itself
|
|
1444
|
-
// (see its own `sessionClient.addCurrentAccount()` call). Without this
|
|
1445
|
-
// guard a winning `sso-return` / `shared-key-signin` / `silent-iframe`
|
|
1446
|
-
// step would register the SAME account twice
|
|
1447
|
-
// (`POST /session/device/add` called back-to-back). `start()` and
|
|
1448
|
-
// `syncFromClient()` still always run — `start()` is idempotent
|
|
1449
|
-
// (no-ops once already started) and is what connects the realtime
|
|
1450
|
-
// socket for the first time.
|
|
1451
|
-
if (outcome.kind === 'session' || oxyServices.getAccessToken()) {
|
|
1452
|
-
// Self-contained: never throws (own try/catch), so it is safe to let it
|
|
1453
|
-
// outlive the race below and finish in the background if the deadline
|
|
1454
|
-
// trips — the socket subscription then projects the state when it lands.
|
|
1455
|
-
const handoff = (async () => {
|
|
1456
|
-
try {
|
|
1457
|
-
if (!registeredDuringBootRef.current) {
|
|
1458
|
-
// Nested guard: registering this account into the device set is
|
|
1459
|
-
// best-effort. If it throws, `start()` must STILL run so the
|
|
1460
|
-
// realtime socket connects and state projects — cold boot
|
|
1461
|
-
// re-registers the account into the device set on the next load.
|
|
1462
|
-
try {
|
|
1463
|
-
await sessionClient.addCurrentAccount();
|
|
1464
|
-
} catch (addAccountErr) {
|
|
1465
|
-
_core.logger.warn('cold-boot: addCurrentAccount failed; continuing to start()', {
|
|
1466
|
-
component: 'OxyContext',
|
|
1467
|
-
method: 'restoreSessionsFromStorage'
|
|
1468
|
-
}, addAccountErr);
|
|
1469
|
-
}
|
|
1470
|
-
}
|
|
1471
|
-
await sessionClient.start();
|
|
1472
|
-
await syncFromClient();
|
|
1473
|
-
} catch (startErr) {
|
|
1474
|
-
_core.logger.warn('cold-boot: SessionClient start failed', {
|
|
1475
|
-
component: 'OxyContext',
|
|
1476
|
-
method: 'restoreSessionsFromStorage'
|
|
1477
|
-
}, startErr);
|
|
1478
|
-
}
|
|
1479
|
-
})();
|
|
1480
|
-
// Bound how long auth resolution waits for the handoff (see
|
|
1481
|
-
// `SESSION_HANDOFF_DEADLINE`): the token is already planted, so on a
|
|
1482
|
-
// slow backend we proceed to `markAuthResolved` and let the handoff
|
|
1483
|
-
// complete asynchronously rather than stalling the spinner.
|
|
1484
|
-
let handoffDeadlineId;
|
|
1485
|
-
await Promise.race([handoff, new Promise(resolve => {
|
|
1486
|
-
handoffDeadlineId = setTimeout(resolve, SESSION_HANDOFF_DEADLINE);
|
|
1487
|
-
})]).finally(() => {
|
|
1488
|
-
if (handoffDeadlineId !== undefined) {
|
|
1489
|
-
clearTimeout(handoffDeadlineId);
|
|
1490
|
-
}
|
|
1491
|
-
});
|
|
1492
|
-
}
|
|
1493
708
|
} catch (error) {
|
|
1494
709
|
if (__DEV__) {
|
|
1495
|
-
_core.logger.error('
|
|
710
|
+
_core.logger.error('Cold boot error', error instanceof Error ? error : new Error(String(error)), {
|
|
1496
711
|
component: 'OxyContext',
|
|
1497
|
-
method: '
|
|
712
|
+
method: 'runColdBoot'
|
|
1498
713
|
});
|
|
1499
714
|
}
|
|
1500
|
-
await clearSessionStateRef.current();
|
|
1501
715
|
} finally {
|
|
1502
|
-
// Backstop:
|
|
1503
|
-
|
|
1504
|
-
// `stored-session` step), so the gate can never hang `false`. Idempotent
|
|
1505
|
-
// via `markAuthResolved`'s ref: when a commit site already flipped it
|
|
1506
|
-
// mid-chain (the common reload case), this is a no-op. When no session was
|
|
1507
|
-
// recovered (the unauthenticated/error path), this is where `tokenReady` +
|
|
1508
|
-
// `authResolved` finally flip. Monotonic — never reverts on later restores.
|
|
1509
|
-
markAuthResolved();
|
|
716
|
+
// Backstop: resolve on every exit path so the gate can never hang.
|
|
717
|
+
markAuthResolvedRef.current();
|
|
1510
718
|
}
|
|
1511
|
-
}, [oxyServices,
|
|
719
|
+
}, [oxyServices, authStore]);
|
|
1512
720
|
(0, _react.useEffect)(() => {
|
|
1513
721
|
if (!storage || initialized) {
|
|
1514
722
|
return;
|
|
1515
723
|
}
|
|
1516
724
|
setInitialized(true);
|
|
1517
|
-
|
|
1518
|
-
if (__DEV__) {
|
|
1519
|
-
logger('Failed to restore sessions from storage', error);
|
|
1520
|
-
}
|
|
1521
|
-
});
|
|
1522
|
-
}, [restoreSessionsFromStorage, storage, initialized, logger]);
|
|
1523
|
-
|
|
1524
|
-
// bfcache re-evaluation (web only, registered once). When a page is restored
|
|
1525
|
-
// from the back/forward cache (`e.persisted`) NO cold boot re-runs — React
|
|
1526
|
-
// state is resurrected as-is — yet the page may have been frozen mid-bounce
|
|
1527
|
-
// and resurrected ON the SSO callback with a fresh fragment in the URL. Re-run
|
|
1528
|
-
// the `sso-return` parse so the opaque code is still exchanged (and the
|
|
1529
|
-
// fragment stripped + NO_SESSION flag maintained) on a bfcache restore. Routed
|
|
1530
|
-
// through a ref so the listener registers exactly once and never churns with
|
|
1531
|
-
// `runSsoReturn`'s identity.
|
|
1532
|
-
const runSsoReturnRef = (0, _react.useRef)(runSsoReturn);
|
|
1533
|
-
runSsoReturnRef.current = runSsoReturn;
|
|
1534
|
-
(0, _react.useEffect)(() => {
|
|
1535
|
-
if (!(0, _useWebSSO.isWebBrowser)()) {
|
|
1536
|
-
return;
|
|
1537
|
-
}
|
|
1538
|
-
const onPageShow = event => {
|
|
1539
|
-
if (!event.persisted) {
|
|
1540
|
-
return;
|
|
1541
|
-
}
|
|
1542
|
-
runSsoReturnRef.current().catch(error => {
|
|
1543
|
-
if (__DEV__) {
|
|
1544
|
-
_core.logger.debug('bfcache SSO return re-evaluation failed (non-fatal)', {
|
|
1545
|
-
component: 'OxyContext',
|
|
1546
|
-
method: 'onPageShow'
|
|
1547
|
-
}, error);
|
|
1548
|
-
}
|
|
1549
|
-
});
|
|
1550
|
-
};
|
|
1551
|
-
window.addEventListener('pageshow', onPageShow);
|
|
1552
|
-
return () => window.removeEventListener('pageshow', onPageShow);
|
|
1553
|
-
}, []);
|
|
1554
|
-
|
|
1555
|
-
// EAGER, universal SSO-callback interception (web only, once on mount).
|
|
1556
|
-
//
|
|
1557
|
-
// When the central IdP redirects the RP back to the internal callback path
|
|
1558
|
-
// ({@link SSO_CALLBACK_PATH}), the app's own router would otherwise mount on
|
|
1559
|
-
// `/__oxy/sso-callback` — a route NO app declares — and briefly flash its
|
|
1560
|
-
// +not-found screen before the storage-gated cold-boot `sso-return` step gets
|
|
1561
|
-
// a chance to strip the fragment and restore the real destination.
|
|
1562
|
-
//
|
|
1563
|
-
// This effect fires the SAME `runSsoReturn` kernel the instant we hydrate ON
|
|
1564
|
-
// the callback path, BEFORE the cold boot (which awaits storage init). The
|
|
1565
|
-
// first render intentionally matches the app/router's static HTML; the
|
|
1566
|
-
// browser layout effect then hides the internal route and consumes the
|
|
1567
|
-
// callback before the first visible paint. That keeps SSR/SSG hydration stable
|
|
1568
|
-
// while still ensuring no app needs a `/__oxy/sso-callback` route.
|
|
1569
|
-
//
|
|
1570
|
-
// It is purely ADDITIVE. The later cold-boot `sso-return` step stays as
|
|
1571
|
-
// defense-in-depth for the non-callback-path case; `consumeSsoReturn` strips
|
|
1572
|
-
// the fragment first, so once this eager pass has run the cold-boot step is a
|
|
1573
|
-
// harmless idempotent no-op (a second parse of the now-fragment-less URL
|
|
1574
|
-
// returns `null`). The path guard scopes this strictly to the callback path,
|
|
1575
|
-
// so a normal page load is untouched. Routed through `runSsoReturnRef` (the
|
|
1576
|
-
// SAME ref the bfcache handler uses) so deps stay `[]` and it registers once.
|
|
1577
|
-
//
|
|
1578
|
-
// Timing: `handleWebSSOSessionRef.current` is assigned SYNCHRONOUSLY during
|
|
1579
|
-
// render (see below, around the `handleWebSSOSession` declaration), and effects
|
|
1580
|
-
// run only after the render commits, so on an `ok` outcome the commit path is
|
|
1581
|
-
// already wired when this fires at eager-mount time. If for any reason it were
|
|
1582
|
-
// not yet set, the later cold-boot `sso-return` step would commit it — but the
|
|
1583
|
-
// ref IS set during render, so the eager `ok` commit works.
|
|
1584
|
-
useBrowserLayoutEffect(() => {
|
|
1585
|
-
if (!isOnSsoCallbackPath()) {
|
|
1586
|
-
setSsoCallbackIntercepting(false);
|
|
1587
|
-
return;
|
|
1588
|
-
}
|
|
1589
|
-
let mounted = true;
|
|
1590
|
-
setSsoCallbackIntercepting(true);
|
|
1591
|
-
runSsoReturnRef.current().catch(error => {
|
|
1592
|
-
if (__DEV__) {
|
|
1593
|
-
_core.logger.debug('Eager SSO callback interception failed (non-fatal)', {
|
|
1594
|
-
component: 'OxyContext',
|
|
1595
|
-
method: 'eagerSsoCallbackIntercept'
|
|
1596
|
-
}, error);
|
|
1597
|
-
}
|
|
1598
|
-
}).finally(() => {
|
|
1599
|
-
if (mounted) {
|
|
1600
|
-
setSsoCallbackIntercepting(false);
|
|
1601
|
-
}
|
|
1602
|
-
});
|
|
1603
|
-
return () => {
|
|
1604
|
-
mounted = false;
|
|
1605
|
-
};
|
|
1606
|
-
}, []);
|
|
1607
|
-
|
|
1608
|
-
// Web SSO: automatically check for cross-domain session on web platforms.
|
|
1609
|
-
// Updates all state and persists session metadata.
|
|
1610
|
-
const handleWebSSOSession = (0, _react.useCallback)(async session => {
|
|
1611
|
-
if (!session?.user || !session?.sessionId) {
|
|
725
|
+
runColdBoot().catch(error => {
|
|
1612
726
|
if (__DEV__) {
|
|
1613
|
-
|
|
1614
|
-
component: 'OxyContext'
|
|
1615
|
-
});
|
|
727
|
+
logger('Cold boot failed', error);
|
|
1616
728
|
}
|
|
1617
|
-
return;
|
|
1618
|
-
}
|
|
1619
|
-
if (!session.accessToken) {
|
|
1620
|
-
throw new Error('Session response did not include an access token');
|
|
1621
|
-
}
|
|
1622
|
-
oxyServices.httpService.setTokens(session.accessToken);
|
|
1623
|
-
|
|
1624
|
-
// A committed web session re-enables automatic silent restore: clear the
|
|
1625
|
-
// durable "deliberately signed out" flag. This funnel is reached by deliberate
|
|
1626
|
-
// sign-ins (password, `/sso` return) AND by the silent
|
|
1627
|
-
// cold-boot steps — but those are GATED on the flag, so when it is set they
|
|
1628
|
-
// never run and never reach here; clearing is therefore only hit on a genuine
|
|
1629
|
-
// (re-)sign-in or when restore was already permitted, both correct.
|
|
1630
|
-
(0, _activeAuthuser.clearSignedOut)();
|
|
1631
|
-
|
|
1632
|
-
// Register this recovered account+session into the server-authoritative
|
|
1633
|
-
// device-session set. Every web primary-session restore funnels through
|
|
1634
|
-
// here — per-apex `/auth/silent` iframe,
|
|
1635
|
-
// central `/sso` return, and keyless password sign-in — and each of them
|
|
1636
|
-
// needs the resulting account added to the device's `DeviceSession` doc so
|
|
1637
|
-
// the sign-in persists across reload / other tabs / devices and the
|
|
1638
|
-
// realtime `device:<deviceId>` socket has an account to track.
|
|
1639
|
-
// `sessionClient.addCurrentAccount()` (`POST /session/device/add`) derives
|
|
1640
|
-
// identity from the bearer this function just planted above;
|
|
1641
|
-
// `syncFromClient()` reprojects the resulting server state onto the
|
|
1642
|
-
// exposed sessions/activeSessionId/user. Best-effort: a failure here must
|
|
1643
|
-
// NEVER fail the sign-in itself — cold boot re-registers this account into
|
|
1644
|
-
// the device set on the next load regardless. `registeredDuringBootRef` is
|
|
1645
|
-
// flipped on success so the cold-boot post-ladder handoff (which reaches
|
|
1646
|
-
// every step, including this one) does not redundantly re-register the
|
|
1647
|
-
// SAME account with a second `POST /session/device/add`.
|
|
1648
|
-
try {
|
|
1649
|
-
await sessionClient.addCurrentAccount();
|
|
1650
|
-
registeredDuringBootRef.current = true;
|
|
1651
|
-
await syncFromClient();
|
|
1652
|
-
} catch (registrationError) {
|
|
1653
|
-
_core.logger.warn('handleWebSSOSession: failed to register session into device set', {
|
|
1654
|
-
component: 'OxyContext',
|
|
1655
|
-
method: 'handleWebSSOSession'
|
|
1656
|
-
}, registrationError);
|
|
1657
|
-
}
|
|
1658
|
-
const clientSession = {
|
|
1659
|
-
sessionId: session.sessionId,
|
|
1660
|
-
deviceId: session.deviceId || '',
|
|
1661
|
-
expiresAt: session.expiresAt || new Date(Date.now() + DEFAULT_SESSION_VALIDITY_MS).toISOString(),
|
|
1662
|
-
lastActive: new Date().toISOString(),
|
|
1663
|
-
userId: session.user.id?.toString() ?? '',
|
|
1664
|
-
isCurrent: true
|
|
1665
|
-
};
|
|
1666
|
-
updateSessions([clientSession], {
|
|
1667
|
-
merge: true
|
|
1668
729
|
});
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
// Persist to storage BEFORE fetching the profile and BEFORE notifying the
|
|
1672
|
-
// auth-state-change callback. The token is planted and the in-memory store
|
|
1673
|
-
// is updated above; durably committing the session id here — ahead of
|
|
1674
|
-
// `getCurrentUser()` / `loginSuccess` / `onAuthStateChange` — is critical
|
|
1675
|
-
// because `onAuthStateChange` triggers a route navigation that can
|
|
1676
|
-
// interrupt/supersede a still-pending async write. Persisting first
|
|
1677
|
-
// guarantees the durable record lands; that is exactly what was missing
|
|
1678
|
-
// when `oxy_session_session_ids` came back empty after a successful SSO
|
|
1679
|
-
// sign-in (the user appeared logged in until reload, then had no session to
|
|
1680
|
-
// restore). `persistSessionDurably` awaits the READY storage instance rather
|
|
1681
|
-
// than reading the possibly-null `storage` state, so a sign-in fired the
|
|
1682
|
-
// instant the screen mounts (before storage-init populates state) is not
|
|
1683
|
-
// silently dropped.
|
|
1684
|
-
await persistSessionDurably(session.sessionId);
|
|
1685
|
-
|
|
1686
|
-
// Fetch the full user profile now that we have a valid access token and the
|
|
1687
|
-
// session is durably persisted. The session only carries MinimalUserData;
|
|
1688
|
-
// the store and callbacks expect a full User. The navigation kicked off by
|
|
1689
|
-
// `onAuthStateChange` now happens only after the durable write is committed.
|
|
1690
|
-
let fullUser;
|
|
1691
|
-
try {
|
|
1692
|
-
fullUser = await oxyServices.getCurrentUser();
|
|
1693
|
-
} catch (profileError) {
|
|
1694
|
-
if (__DEV__) {
|
|
1695
|
-
_core.logger.debug('Failed to fetch full user after web session; using session user fallback', {
|
|
1696
|
-
component: 'OxyContext',
|
|
1697
|
-
method: 'handleWebSSOSession'
|
|
1698
|
-
}, profileError);
|
|
1699
|
-
}
|
|
1700
|
-
// If the profile fetch fails, fall back to the minimal data from the session
|
|
1701
|
-
// so the user is still logged in (the store accepts User, but the shapes overlap at runtime).
|
|
1702
|
-
fullUser = session.user;
|
|
1703
|
-
}
|
|
1704
|
-
loginSuccess(fullUser);
|
|
1705
|
-
// A session is now committed (per-apex iframe /
|
|
1706
|
-
// SSO-return all funnel through here) — unblock the auth-resolution
|
|
1707
|
-
// gate immediately, ahead of the cold-boot chain returning (idempotent).
|
|
1708
|
-
markAuthResolvedRef.current();
|
|
1709
|
-
onAuthStateChange?.(fullUser);
|
|
1710
|
-
}, [oxyServices, updateSessions, setActiveSessionId, loginSuccess, onAuthStateChange, persistSessionDurably, sessionClient, syncFromClient]);
|
|
1711
|
-
|
|
1712
|
-
// Expose `handleWebSSOSession` to the cold-boot iframe/SSO steps,
|
|
1713
|
-
// which reference it through a ref because they are declared above this
|
|
1714
|
-
// callback. Assigned synchronously on every render so the ref is populated
|
|
1715
|
-
// before the cold-boot effect (gated on `storage`/`initialized`) can fire.
|
|
1716
|
-
handleWebSSOSessionRef.current = handleWebSSOSession;
|
|
1717
|
-
|
|
1718
|
-
// Keyless native sign-in: username/email + password. The slimmed Accounts app
|
|
1719
|
-
// (which no longer holds a local identity key) uses this; it commits a
|
|
1720
|
-
// successful session through the SAME `handleWebSSOSession` path SSO
|
|
1721
|
-
// use, so state, durable persistence, profile fetch, and `markAuthResolved`
|
|
1722
|
-
// all run identically. The API returns either a full session OR a two-factor
|
|
1723
|
-
// handoff (`{ twoFactorRequired, loginToken }`) — we surface the latter as a
|
|
1724
|
-
// discriminated result so the caller can run the 2FA challenge without any
|
|
1725
|
-
// session being committed.
|
|
1726
|
-
const signInWithPassword = (0, _react.useCallback)(async (identifier, password, opts) => {
|
|
1727
|
-
// On a cross-apex web RP a direct password sign-in mints a bearer against
|
|
1728
|
-
// the Oxy API but establishes no `fedcm_session`, so the session would be
|
|
1729
|
-
// lost on reload. Refuse it and direct the app to the durable IdP popup
|
|
1730
|
-
// ("Continue with Oxy"). Native and same-apex `*.oxy.so` are unaffected.
|
|
1731
|
-
if ((0, _crossApex.isCrossApexWeb)()) {
|
|
1732
|
-
throw new _crossApex.CrossApexDirectSignInError();
|
|
1733
|
-
}
|
|
1734
|
-
const response = await oxyServices.signIn(identifier, password, opts?.deviceName, opts?.deviceFingerprint);
|
|
1735
|
-
// Core types `signIn` as `SessionLoginResponse`, but the API may instead
|
|
1736
|
-
// return a 2FA challenge handoff. Widen with optional fields (no `any`) to
|
|
1737
|
-
// read them — the added members are optional, so the base type is
|
|
1738
|
-
// assignable to this intersection.
|
|
1739
|
-
const maybeTwoFactor = response;
|
|
1740
|
-
if (maybeTwoFactor.twoFactorRequired && maybeTwoFactor.loginToken) {
|
|
1741
|
-
return {
|
|
1742
|
-
status: '2fa_required',
|
|
1743
|
-
loginToken: maybeTwoFactor.loginToken
|
|
1744
|
-
};
|
|
1745
|
-
}
|
|
1746
|
-
// Full session — commit it through the shared web-session path.
|
|
1747
|
-
await handleWebSSOSession(response);
|
|
1748
|
-
return {
|
|
1749
|
-
status: 'ok'
|
|
1750
|
-
};
|
|
1751
|
-
}, [oxyServices, handleWebSSOSession]);
|
|
1752
|
-
|
|
1753
|
-
// Cross-domain silent SSO is owned ENTIRELY by the `silent-iframe` cold-boot
|
|
1754
|
-
// step above (the ordered `runColdBoot` sequence) plus the terminal
|
|
1755
|
-
// `sso-bounce` fallback. There is no post-boot safety net here: the FedCM
|
|
1756
|
-
// silent post-boot retry that used to run via `useWebSSO` was removed along
|
|
1757
|
-
// with the rest of the client FedCM surface (Chrome-only; see
|
|
1758
|
-
// `CrossDomainAuth`'s doc comment in `@oxyhq/core` for the production
|
|
1759
|
-
// incident that motivated the removal).
|
|
730
|
+
}, [runColdBoot, storage, initialized, logger]);
|
|
1760
731
|
|
|
1761
|
-
//
|
|
1762
|
-
//
|
|
1763
|
-
// If session is gone (cleared/logged out), clear local session too
|
|
1764
|
-
//
|
|
1765
|
-
// KEPT (session-sync cutover audit, Task 5): this is NOT redundant with the
|
|
1766
|
-
// `SessionClient` socket (`device:<deviceId>`, `session_state` push). That
|
|
1767
|
-
// socket only fires for revocations that flow through oxy-api's
|
|
1768
|
-
// `DeviceSession` authority (`POST /session/device/signout` and friends). A
|
|
1769
|
-
// sign-out at the CENTRAL IdP itself (`auth.oxy.so`'s own `AuthSession` /
|
|
1770
|
-
// `fedcm_session` — e.g. a direct IdP-side sign-out, or an admin/security
|
|
1771
|
-
// revocation there) does not currently touch `DeviceSession` at all, so no
|
|
1772
|
-
// socket push follows — this iframe poll is the ONLY mechanism that catches
|
|
1773
|
-
// it today. A future phase should move this server-side (IdP deactivation
|
|
1774
|
-
// -> `DeviceSession` update -> broadcast over the same socket), at which
|
|
1775
|
-
// point this client-side poll becomes redundant and can be deleted.
|
|
1776
|
-
const lastIdPCheckRef = (0, _react.useRef)(0);
|
|
1777
|
-
const pendingIdPCleanupRef = (0, _react.useRef)(null);
|
|
1778
|
-
|
|
1779
|
-
// Use the RESOLVED IdP origin (the auto-detected `auth.<rp-apex>` planted on
|
|
1780
|
-
// the instance config), not the raw `authWebUrl` prop — on a cross-domain RP
|
|
1781
|
-
// the prop is undefined but the instance was constructed with the detected
|
|
1782
|
-
// value, so the check must target the same first-party IdP the cold-boot
|
|
1783
|
-
// iframe used.
|
|
1784
|
-
const resolvedAuthWebUrl = oxyServices.config?.authWebUrl;
|
|
1785
|
-
(0, _react.useEffect)(() => {
|
|
1786
|
-
if (!(0, _useWebSSO.isWebBrowser)() || !user || !initialized) return;
|
|
1787
|
-
const idpOrigin = resolvedAuthWebUrl || 'https://auth.oxy.so';
|
|
1788
|
-
const checkIdPSession = () => {
|
|
1789
|
-
// Debounce: check at most once per cooldown window
|
|
1790
|
-
const now = Date.now();
|
|
1791
|
-
if (now - lastIdPCheckRef.current < IDP_SESSION_CHECK_COOLDOWN) return;
|
|
1792
|
-
lastIdPCheckRef.current = now;
|
|
1793
|
-
|
|
1794
|
-
// Clean up any in-flight check before starting a new one
|
|
1795
|
-
pendingIdPCleanupRef.current?.();
|
|
1796
|
-
|
|
1797
|
-
// Load hidden iframe to check IdP session via postMessage
|
|
1798
|
-
const iframe = document.createElement('iframe');
|
|
1799
|
-
iframe.style.cssText = 'display:none;width:0;height:0;border:0';
|
|
1800
|
-
iframe.src = `${idpOrigin}/auth/session-check?client_id=${encodeURIComponent(window.location.origin)}`;
|
|
1801
|
-
let cleaned = false;
|
|
1802
|
-
const cleanup = () => {
|
|
1803
|
-
if (cleaned) return;
|
|
1804
|
-
cleaned = true;
|
|
1805
|
-
window.removeEventListener('message', handleMessage);
|
|
1806
|
-
iframe.remove();
|
|
1807
|
-
};
|
|
1808
|
-
const handleMessage = async event => {
|
|
1809
|
-
if (event.origin !== idpOrigin) return;
|
|
1810
|
-
if (event.data?.type !== 'oxy-session-check') return;
|
|
1811
|
-
cleanup();
|
|
1812
|
-
if (!event.data.hasSession) {
|
|
1813
|
-
// Only a SAME-SITE, first-party IdP answer is authoritative enough to
|
|
1814
|
-
// force a local sign-out. On a cross-site / undetermined IdP the
|
|
1815
|
-
// "no session" answer must never clear local state (a third-party
|
|
1816
|
-
// can't be trusted to end this app's session). Surface the toast in
|
|
1817
|
-
// both cases, but gate the destructive `clearSessionState()`.
|
|
1818
|
-
if (isSameSiteIdP(idpOrigin)) {
|
|
1819
|
-
_bloom.toast.info('Your session has ended. Please sign in again.');
|
|
1820
|
-
await clearSessionState();
|
|
1821
|
-
}
|
|
1822
|
-
}
|
|
1823
|
-
};
|
|
1824
|
-
window.addEventListener('message', handleMessage);
|
|
1825
|
-
document.body.appendChild(iframe);
|
|
1826
|
-
setTimeout(cleanup, IDP_SESSION_CHECK_TIMEOUT);
|
|
1827
|
-
pendingIdPCleanupRef.current = cleanup;
|
|
1828
|
-
};
|
|
1829
|
-
const handleVisibilityChange = () => {
|
|
1830
|
-
if (document.visibilityState === 'visible') {
|
|
1831
|
-
checkIdPSession();
|
|
1832
|
-
}
|
|
1833
|
-
};
|
|
1834
|
-
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
1835
|
-
return () => {
|
|
1836
|
-
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
1837
|
-
pendingIdPCleanupRef.current?.();
|
|
1838
|
-
pendingIdPCleanupRef.current = null;
|
|
1839
|
-
};
|
|
1840
|
-
}, [user, initialized, clearSessionState, resolvedAuthWebUrl]);
|
|
1841
|
-
|
|
1842
|
-
// Exposed `refreshSessions`: re-bootstraps the server-authoritative device
|
|
1843
|
-
// state (`GET /session/device/state`, the SAME request `SessionClient.start()`
|
|
1844
|
-
// makes) and reprojects it via `syncFromClient` — a manual pull-to-refresh
|
|
1845
|
-
// counterpart to the realtime `device:<deviceId>` socket `client.start()`
|
|
1846
|
-
// already owns (there is no per-domain `useSessionSocket` anymore; a REMOTE
|
|
1847
|
-
// sign-out or account change arrives over that socket and is handled by
|
|
1848
|
-
// `syncFromClient`'s subscription below, including the full-sign-out gap fix
|
|
1849
|
-
// documented above it).
|
|
732
|
+
// Exposed `refreshSessions`: re-bootstrap the server-authoritative device
|
|
733
|
+
// state and reproject — the manual counterpart to the realtime socket.
|
|
1850
734
|
const refreshSessionsForContext = (0, _react.useCallback)(async () => {
|
|
1851
735
|
await sessionClient.bootstrap();
|
|
1852
736
|
await syncFromClient();
|
|
1853
737
|
}, [sessionClient, syncFromClient]);
|
|
1854
738
|
|
|
1855
|
-
// Exposed `switchSession`:
|
|
1856
|
-
// `SessionClient
|
|
1857
|
-
//
|
|
1858
|
-
// internal same-user duplicate-session dedup — a different, legacy
|
|
1859
|
-
// session-validate concept unrelated to switching the device's ACTIVE
|
|
1860
|
-
// account). Resolves the target account from the current device state,
|
|
1861
|
-
// asks the server to switch, reprojects, and returns the now-active user.
|
|
739
|
+
// Exposed `switchSession`: route through the server-authoritative
|
|
740
|
+
// `SessionClient`. Resolve the target account from the current device state,
|
|
741
|
+
// ask the server to switch, reproject, and return the now-active user.
|
|
1862
742
|
const switchSessionForContext = (0, _react.useCallback)(async sessionId => {
|
|
1863
743
|
const targetAccountId = sessionClient.getState()?.accounts.find(account => account.sessionId === sessionId)?.accountId;
|
|
1864
744
|
if (!targetAccountId) {
|
|
@@ -1872,21 +752,11 @@ const OxyProvider = ({
|
|
|
1872
752
|
}
|
|
1873
753
|
return activeUser;
|
|
1874
754
|
}, [sessionClient, syncFromClient]);
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
const hasIdentity = (0, _react.useCallback)(async () => {
|
|
1878
|
-
return _core.KeyManager.hasIdentity();
|
|
1879
|
-
}, []);
|
|
1880
|
-
const getPublicKey = (0, _react.useCallback)(async () => {
|
|
1881
|
-
return _core.KeyManager.getPublicKey();
|
|
1882
|
-
}, []);
|
|
1883
|
-
|
|
1884
|
-
// Create showBottomSheet function that uses the global function
|
|
755
|
+
const hasIdentity = (0, _react.useCallback)(async () => _core.KeyManager.hasIdentity(), []);
|
|
756
|
+
const getPublicKey = (0, _react.useCallback)(async () => _core.KeyManager.getPublicKey(), []);
|
|
1885
757
|
const showBottomSheetForContext = (0, _react.useCallback)(screenOrConfig => {
|
|
1886
758
|
(0, _bottomSheetManager.showBottomSheet)(screenOrConfig);
|
|
1887
759
|
}, []);
|
|
1888
|
-
|
|
1889
|
-
// Avatar picker extracted into dedicated hook
|
|
1890
760
|
const {
|
|
1891
761
|
openAvatarPicker
|
|
1892
762
|
} = (0, _useAvatarPicker.useAvatarPicker)({
|
|
@@ -1897,10 +767,8 @@ const OxyProvider = ({
|
|
|
1897
767
|
showBottomSheet: showBottomSheetForContext
|
|
1898
768
|
});
|
|
1899
769
|
|
|
1900
|
-
//
|
|
770
|
+
// ── Account graph ──────────────────────────────────────────────────────────
|
|
1901
771
|
const [accounts, setAccounts] = (0, _react.useState)([]);
|
|
1902
|
-
|
|
1903
|
-
// Load the unified account graph when authenticated
|
|
1904
772
|
const refreshAccounts = (0, _react.useCallback)(async () => {
|
|
1905
773
|
if (!isAuthenticated || !tokenReady || !oxyServices.getAccessToken()) {
|
|
1906
774
|
setAccounts([]);
|
|
@@ -1927,34 +795,13 @@ const OxyProvider = ({
|
|
|
1927
795
|
refreshAccounts();
|
|
1928
796
|
}
|
|
1929
797
|
}, [isAuthenticated, initialized, tokenReady, refreshAccounts]);
|
|
1930
|
-
|
|
1931
|
-
// Shared post-switch side effects, run identically regardless of which
|
|
1932
|
-
// `switchToAccount` branch handled the switch below: reload the switchable
|
|
1933
|
-
// account graph (the new active account's relationships differ) and
|
|
1934
|
-
// invalidate every query so all data refetches as the new account.
|
|
1935
798
|
const runPostAccountSwitchSideEffects = (0, _react.useCallback)(async () => {
|
|
1936
799
|
await refreshAccounts();
|
|
1937
800
|
queryClient.invalidateQueries();
|
|
1938
801
|
}, [refreshAccounts, queryClient]);
|
|
1939
802
|
|
|
1940
803
|
// Switch the active session INTO an account from the unified graph. In the
|
|
1941
|
-
//
|
|
1942
|
-
// whole app becomes that account. There is exactly ONE uniform path for
|
|
1943
|
-
// that — the device's server-authoritative `SessionClient.switchAccount()`
|
|
1944
|
-
// — used for EVERY switch, org/managed accounts included:
|
|
1945
|
-
//
|
|
1946
|
-
// - If `accountId` is ALREADY registered on this device's multi-account
|
|
1947
|
-
// set (a prior `switchToAccount`/sign-in already added it), switch
|
|
1948
|
-
// straight through `sessionClient.switchAccount()` — the SAME path
|
|
1949
|
-
// `switchSession` uses for ordinary device sessions. No new session is
|
|
1950
|
-
// minted, so no session churn / deactivation of the account's existing
|
|
1951
|
-
// session (H4).
|
|
1952
|
-
// - Only the FIRST time an account is switched into does it need minting:
|
|
1953
|
-
// `oxyServices.switchToAccount` mints+plants a real session, then
|
|
1954
|
-
// `sessionClient.addCurrentAccount()` registers it into the device set
|
|
1955
|
-
// (server-set httpOnly `oxy_rt_<authuser>` cookie, so the session joins
|
|
1956
|
-
// the device multi-account set and survives reload / `refresh-all`).
|
|
1957
|
-
// Every subsequent switch into that same account takes the branch above.
|
|
804
|
+
// real-session model this is identical to switching device sign-ins.
|
|
1958
805
|
const switchToAccount = (0, _react.useCallback)(async accountId => {
|
|
1959
806
|
const deviceState = sessionClient.getState();
|
|
1960
807
|
if (deviceState?.accounts.some(account => account.accountId === accountId)) {
|
|
@@ -1967,72 +814,22 @@ const OxyProvider = ({
|
|
|
1967
814
|
if (!result?.user || !result?.sessionId) {
|
|
1968
815
|
throw new Error('Account switch did not return a valid session');
|
|
1969
816
|
}
|
|
1970
|
-
|
|
1971
|
-
//
|
|
1972
|
-
//
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
// `oxyServices.switchToAccount` already planted `result.accessToken` as the
|
|
1976
|
-
// active token; mirror the minted session into the multi-account store and
|
|
1977
|
-
// mark it current. The device account SET (and its `authuser` slot
|
|
1978
|
-
// numbering) is server-authoritative via `SessionClient` — the
|
|
1979
|
-
// `addCurrentAccount` + `syncFromClient` reprojection below supersedes this
|
|
1980
|
-
// local mirror with the server's own state.
|
|
1981
|
-
const now = new Date();
|
|
1982
|
-
const clientSession = {
|
|
817
|
+
// `oxyServices.switchToAccount` already planted the access token; commit
|
|
818
|
+
// the minted session through the shared funnel (persist + register +
|
|
819
|
+
// hydrate) as a deliberate activation.
|
|
820
|
+
await commitSession({
|
|
1983
821
|
sessionId: result.sessionId,
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
822
|
+
accessToken: result.accessToken,
|
|
823
|
+
refreshToken: result.refreshToken,
|
|
824
|
+
deviceId: result.deviceId,
|
|
825
|
+
expiresAt: result.expiresAt,
|
|
826
|
+
userId: result.user.id,
|
|
827
|
+
user: result.user
|
|
828
|
+
}, {
|
|
829
|
+
activate: true
|
|
1992
830
|
});
|
|
1993
|
-
setActiveSessionId(result.sessionId);
|
|
1994
|
-
await persistSessionDurably(result.sessionId);
|
|
1995
|
-
|
|
1996
|
-
// Register the switched-to account into the device's server-authoritative
|
|
1997
|
-
// multi-account set (the SAME `SessionClient.addCurrentAccount()` the
|
|
1998
|
-
// cold-boot handoff uses — it derives identity from the bearer, which
|
|
1999
|
-
// `oxyServices.switchToAccount` already planted above) and reproject
|
|
2000
|
-
// state, so the switch persists across reload / other tabs / devices
|
|
2001
|
-
// instead of only living in this provider's local session-management
|
|
2002
|
-
// state. Best-effort: the token for the new account is already planted, so
|
|
2003
|
-
// a transient failure here must NEVER reject the switch — fall through to
|
|
2004
|
-
// `loginSuccess`/`onAuthStateChange` so the UI reflects the switched-to
|
|
2005
|
-
// account (cold boot re-registers into the device set on next load).
|
|
2006
|
-
// Mirrors the same guard in `handleWebSSOSession`.
|
|
2007
|
-
try {
|
|
2008
|
-
await sessionClient.addCurrentAccount();
|
|
2009
|
-
await syncFromClient();
|
|
2010
|
-
} catch (registrationError) {
|
|
2011
|
-
_core.logger.warn('switchToAccount: failed to register account into device set', {
|
|
2012
|
-
component: 'OxyContext',
|
|
2013
|
-
method: 'switchToAccount'
|
|
2014
|
-
}, registrationError);
|
|
2015
|
-
}
|
|
2016
|
-
|
|
2017
|
-
// Fetch the canonical User for the new account (the switch result carries
|
|
2018
|
-
// only MinimalUserData); fall back to that minimal shape if the profile
|
|
2019
|
-
// fetch fails so the app still reflects the switched identity.
|
|
2020
|
-
let fullUser;
|
|
2021
|
-
try {
|
|
2022
|
-
fullUser = await oxyServices.getCurrentUser();
|
|
2023
|
-
} catch (profileError) {
|
|
2024
|
-
if (__DEV__) {
|
|
2025
|
-
_core.logger.debug('Failed to fetch full user after account switch; using switch result user', {
|
|
2026
|
-
component: 'OxyContext',
|
|
2027
|
-
method: 'switchToAccount'
|
|
2028
|
-
}, profileError);
|
|
2029
|
-
}
|
|
2030
|
-
fullUser = result.user;
|
|
2031
|
-
}
|
|
2032
|
-
loginSuccess(fullUser);
|
|
2033
|
-
onAuthStateChange?.(fullUser);
|
|
2034
831
|
await runPostAccountSwitchSideEffects();
|
|
2035
|
-
}, [oxyServices,
|
|
832
|
+
}, [oxyServices, sessionClient, syncFromClient, commitSession, runPostAccountSwitchSideEffects]);
|
|
2036
833
|
const createAccountFn = (0, _react.useCallback)(async data => {
|
|
2037
834
|
const account = await oxyServices.createAccount(data);
|
|
2038
835
|
await refreshAccounts();
|
|
@@ -2061,7 +858,8 @@ const OxyProvider = ({
|
|
|
2061
858
|
getPublicKey,
|
|
2062
859
|
signIn,
|
|
2063
860
|
signInWithPassword,
|
|
2064
|
-
|
|
861
|
+
completeTwoFactorSignIn,
|
|
862
|
+
handleWebSession,
|
|
2065
863
|
logout,
|
|
2066
864
|
logoutAll,
|
|
2067
865
|
switchSession: switchSessionForContext,
|
|
@@ -2083,10 +881,10 @@ const OxyProvider = ({
|
|
|
2083
881
|
switchToAccount,
|
|
2084
882
|
refreshAccounts,
|
|
2085
883
|
createAccount: createAccountFn
|
|
2086
|
-
}), [activeSessionId,
|
|
884
|
+
}), [user, sessions, activeSessionId, isAuthenticated, isLoading, tokenReady, hasAccessToken, canUsePrivateApi, isPrivateApiPending, authResolved, storage, error, currentLanguage, currentLanguageMetadata, currentLanguageName, currentNativeLanguageName, hasIdentity, getPublicKey, signIn, signInWithPassword, completeTwoFactorSignIn, handleWebSession, logout, logoutAll, switchSessionForContext, refreshSessionsForContext, setLanguage, getDeviceSessions, logoutAllDeviceSessions, updateDeviceName, clearSessionState, clearAllAccountData, storageKeyPrefix, clientId, oxyServices, useFollowHook, showBottomSheetForContext, openAvatarPicker, accounts, switchToAccount, refreshAccounts, createAccountFn]);
|
|
2087
885
|
return /*#__PURE__*/(0, _jsxRuntime.jsx)(OxyContext.Provider, {
|
|
2088
886
|
value: contextValue,
|
|
2089
|
-
children:
|
|
887
|
+
children: children
|
|
2090
888
|
});
|
|
2091
889
|
};
|
|
2092
890
|
exports.OxyProvider = OxyProvider;
|
|
@@ -2094,15 +892,10 @@ const OxyContextProvider = exports.OxyContextProvider = OxyProvider;
|
|
|
2094
892
|
|
|
2095
893
|
/**
|
|
2096
894
|
* Loading-state stub used when `useOxy()` is called outside an OxyProvider.
|
|
2097
|
-
* All async methods reject with a clear error so misuse is caught early
|
|
2098
|
-
* instead of silently no-oping and leaving the UI in a bad state.
|
|
895
|
+
* All async methods reject with a clear error so misuse is caught early.
|
|
2099
896
|
*/
|
|
2100
897
|
const PROVIDER_MISSING_ERROR_MESSAGE = 'OxyProvider is not mounted. Wrap your app in <OxyProvider> before calling useOxy() methods.';
|
|
2101
898
|
const rejectMissingProvider = () => Promise.reject(new Error(PROVIDER_MISSING_ERROR_MESSAGE));
|
|
2102
|
-
|
|
2103
|
-
// A stub OxyServices instance so the public type contract is preserved.
|
|
2104
|
-
// Calling network methods on it before a provider mounts will fail with
|
|
2105
|
-
// a descriptive baseURL — preferable to a null-pointer crash at the call site.
|
|
2106
899
|
const LOADING_STATE_OXY_SERVICES = new _core.OxyServices({
|
|
2107
900
|
baseURL: 'about:blank'
|
|
2108
901
|
});
|
|
@@ -2127,6 +920,7 @@ const LOADING_STATE = {
|
|
|
2127
920
|
getPublicKey: () => Promise.resolve(null),
|
|
2128
921
|
signIn: () => rejectMissingProvider(),
|
|
2129
922
|
signInWithPassword: () => rejectMissingProvider(),
|
|
923
|
+
completeTwoFactorSignIn: () => rejectMissingProvider(),
|
|
2130
924
|
handleWebSession: () => rejectMissingProvider(),
|
|
2131
925
|
logout: () => rejectMissingProvider(),
|
|
2132
926
|
logoutAll: () => rejectMissingProvider(),
|
|
@@ -2156,5 +950,4 @@ const useOxy = () => {
|
|
|
2156
950
|
return context;
|
|
2157
951
|
};
|
|
2158
952
|
exports.useOxy = useOxy;
|
|
2159
|
-
var _default = exports.default = OxyContext;
|
|
2160
953
|
//# sourceMappingURL=OxyContext.js.map
|