@oxyhq/services 13.3.0 → 14.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 +11 -17
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/components/AccountMenu.js +103 -65
- package/lib/commonjs/ui/components/AccountMenu.js.map +1 -1
- package/lib/commonjs/ui/components/AccountSwitcher.js +84 -126
- package/lib/commonjs/ui/components/AccountSwitcher.js.map +1 -1
- package/lib/commonjs/ui/components/ProfileMenu.js +140 -153
- package/lib/commonjs/ui/components/ProfileMenu.js.map +1 -1
- package/lib/commonjs/ui/components/SignInModal.js +4 -4
- package/lib/commonjs/ui/components/SignInModal.js.map +1 -1
- package/lib/commonjs/ui/context/OxyContext.js +506 -466
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js +59 -57
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/commonjs/ui/context/inSessionTokenRefresh.js +23 -42
- package/lib/commonjs/ui/context/inSessionTokenRefresh.js.map +1 -1
- package/lib/commonjs/ui/context/silentSessionRestore.js +12 -21
- package/lib/commonjs/ui/context/silentSessionRestore.js.map +1 -1
- package/lib/commonjs/ui/hooks/useAuth.js +4 -3
- package/lib/commonjs/ui/hooks/useAuth.js.map +1 -1
- package/lib/commonjs/ui/hooks/useOxyAuthSession.js +12 -7
- package/lib/commonjs/ui/hooks/useOxyAuthSession.js.map +1 -1
- package/lib/commonjs/ui/hooks/useSessionManagement.js +17 -65
- package/lib/commonjs/ui/hooks/useSessionManagement.js.map +1 -1
- package/lib/commonjs/ui/hooks/useSwitchableAccounts.js +288 -0
- package/lib/commonjs/ui/hooks/useSwitchableAccounts.js.map +1 -0
- package/lib/commonjs/ui/hooks/useWebSSO.js +12 -191
- package/lib/commonjs/ui/hooks/useWebSSO.js.map +1 -1
- package/lib/commonjs/ui/index.js.map +1 -1
- package/lib/commonjs/ui/screens/OxyAuthScreen.js +2 -2
- package/lib/commonjs/ui/screens/OxyAuthScreen.js.map +1 -1
- package/lib/commonjs/ui/session/createSessionClient.js +28 -0
- package/lib/commonjs/ui/session/createSessionClient.js.map +1 -0
- package/lib/commonjs/ui/session/index.js +52 -0
- package/lib/commonjs/ui/session/index.js.map +1 -0
- package/lib/commonjs/ui/session/projectSessionState.js +86 -0
- package/lib/commonjs/ui/session/projectSessionState.js.map +1 -0
- package/lib/commonjs/ui/session/sessionClientHost.js +30 -0
- package/lib/commonjs/ui/session/sessionClientHost.js.map +1 -0
- package/lib/commonjs/ui/session/tokenTransport.js +77 -0
- package/lib/commonjs/ui/session/tokenTransport.js.map +1 -0
- package/lib/commonjs/ui/utils/activeAuthuser.js +35 -79
- package/lib/commonjs/ui/utils/activeAuthuser.js.map +1 -1
- package/lib/commonjs/ui/utils/sessionHelpers.js +1 -51
- package/lib/commonjs/ui/utils/sessionHelpers.js.map +1 -1
- package/lib/commonjs/utils/deviceFlowSignIn.js +70 -15
- package/lib/commonjs/utils/deviceFlowSignIn.js.map +1 -1
- package/lib/module/index.js +5 -5
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/components/AccountMenu.js +103 -65
- package/lib/module/ui/components/AccountMenu.js.map +1 -1
- package/lib/module/ui/components/AccountSwitcher.js +85 -127
- package/lib/module/ui/components/AccountSwitcher.js.map +1 -1
- package/lib/module/ui/components/ProfileMenu.js +143 -156
- package/lib/module/ui/components/ProfileMenu.js.map +1 -1
- package/lib/module/ui/components/SignInModal.js +4 -4
- package/lib/module/ui/components/SignInModal.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +510 -470
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/context/hooks/useAuthOperations.js +61 -60
- package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/module/ui/context/inSessionTokenRefresh.js +24 -43
- package/lib/module/ui/context/inSessionTokenRefresh.js.map +1 -1
- package/lib/module/ui/context/silentSessionRestore.js +12 -20
- package/lib/module/ui/context/silentSessionRestore.js.map +1 -1
- package/lib/module/ui/hooks/useAuth.js +4 -3
- package/lib/module/ui/hooks/useAuth.js.map +1 -1
- package/lib/module/ui/hooks/useOxyAuthSession.js +12 -7
- package/lib/module/ui/hooks/useOxyAuthSession.js.map +1 -1
- package/lib/module/ui/hooks/useSessionManagement.js +18 -66
- package/lib/module/ui/hooks/useSessionManagement.js.map +1 -1
- package/lib/module/ui/hooks/useSwitchableAccounts.js +283 -0
- package/lib/module/ui/hooks/useSwitchableAccounts.js.map +1 -0
- package/lib/module/ui/hooks/useWebSSO.js +13 -192
- package/lib/module/ui/hooks/useWebSSO.js.map +1 -1
- package/lib/module/ui/index.js +0 -1
- package/lib/module/ui/index.js.map +1 -1
- package/lib/module/ui/screens/OxyAuthScreen.js +2 -2
- package/lib/module/ui/screens/OxyAuthScreen.js.map +1 -1
- package/lib/module/ui/session/createSessionClient.js +25 -0
- package/lib/module/ui/session/createSessionClient.js.map +1 -0
- package/lib/module/ui/session/index.js +17 -0
- package/lib/module/ui/session/index.js.map +1 -0
- package/lib/module/ui/session/projectSessionState.js +79 -0
- package/lib/module/ui/session/projectSessionState.js.map +1 -0
- package/lib/module/ui/session/sessionClientHost.js +26 -0
- package/lib/module/ui/session/sessionClientHost.js.map +1 -0
- package/lib/module/ui/session/tokenTransport.js +74 -0
- package/lib/module/ui/session/tokenTransport.js.map +1 -0
- package/lib/module/ui/utils/activeAuthuser.js +34 -76
- package/lib/module/ui/utils/activeAuthuser.js.map +1 -1
- package/lib/module/ui/utils/sessionHelpers.js +0 -49
- package/lib/module/ui/utils/sessionHelpers.js.map +1 -1
- package/lib/module/utils/deviceFlowSignIn.js +71 -16
- 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/AccountMenu.d.ts +4 -2
- package/lib/typescript/commonjs/ui/components/AccountMenu.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/AccountSwitcher.d.ts +7 -8
- package/lib/typescript/commonjs/ui/components/AccountSwitcher.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/ProfileMenu.d.ts +12 -9
- package/lib/typescript/commonjs/ui/components/ProfileMenu.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts +16 -13
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts +27 -3
- package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/inSessionTokenRefresh.d.ts +22 -17
- package/lib/typescript/commonjs/ui/context/inSessionTokenRefresh.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/silentSessionRestore.d.ts +12 -18
- package/lib/typescript/commonjs/ui/context/silentSessionRestore.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts +1 -0
- package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useAuth.d.ts +1 -1
- package/lib/typescript/commonjs/ui/hooks/useAuth.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useOxyAuthSession.d.ts +2 -2
- package/lib/typescript/commonjs/ui/hooks/useOxyAuthSession.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useSessionManagement.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useSwitchableAccounts.d.ts +162 -0
- package/lib/typescript/commonjs/ui/hooks/useSwitchableAccounts.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/hooks/useWebSSO.d.ts +13 -52
- package/lib/typescript/commonjs/ui/hooks/useWebSSO.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/session/createSessionClient.d.ts +15 -0
- package/lib/typescript/commonjs/ui/session/createSessionClient.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/session/index.d.ts +15 -0
- package/lib/typescript/commonjs/ui/session/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/session/projectSessionState.d.ts +45 -0
- package/lib/typescript/commonjs/ui/session/projectSessionState.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/session/sessionClientHost.d.ts +14 -0
- package/lib/typescript/commonjs/ui/session/sessionClientHost.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/session/tokenTransport.d.ts +24 -0
- package/lib/typescript/commonjs/ui/session/tokenTransport.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/utils/activeAuthuser.d.ts +21 -29
- package/lib/typescript/commonjs/ui/utils/activeAuthuser.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/utils/sessionHelpers.d.ts +0 -19
- package/lib/typescript/commonjs/ui/utils/sessionHelpers.d.ts.map +1 -1
- package/lib/typescript/commonjs/utils/deviceFlowSignIn.d.ts +51 -19
- 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/AccountMenu.d.ts +4 -2
- package/lib/typescript/module/ui/components/AccountMenu.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/AccountSwitcher.d.ts +7 -8
- package/lib/typescript/module/ui/components/AccountSwitcher.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/ProfileMenu.d.ts +12 -9
- package/lib/typescript/module/ui/components/ProfileMenu.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/OxyContext.d.ts +16 -13
- package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts +27 -3
- package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/inSessionTokenRefresh.d.ts +22 -17
- package/lib/typescript/module/ui/context/inSessionTokenRefresh.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/silentSessionRestore.d.ts +12 -18
- package/lib/typescript/module/ui/context/silentSessionRestore.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts +1 -0
- package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useAuth.d.ts +1 -1
- package/lib/typescript/module/ui/hooks/useAuth.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useOxyAuthSession.d.ts +2 -2
- package/lib/typescript/module/ui/hooks/useOxyAuthSession.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useSessionManagement.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useSwitchableAccounts.d.ts +162 -0
- package/lib/typescript/module/ui/hooks/useSwitchableAccounts.d.ts.map +1 -0
- package/lib/typescript/module/ui/hooks/useWebSSO.d.ts +13 -52
- package/lib/typescript/module/ui/hooks/useWebSSO.d.ts.map +1 -1
- package/lib/typescript/module/ui/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/session/createSessionClient.d.ts +15 -0
- package/lib/typescript/module/ui/session/createSessionClient.d.ts.map +1 -0
- package/lib/typescript/module/ui/session/index.d.ts +15 -0
- package/lib/typescript/module/ui/session/index.d.ts.map +1 -0
- package/lib/typescript/module/ui/session/projectSessionState.d.ts +45 -0
- package/lib/typescript/module/ui/session/projectSessionState.d.ts.map +1 -0
- package/lib/typescript/module/ui/session/sessionClientHost.d.ts +14 -0
- package/lib/typescript/module/ui/session/sessionClientHost.d.ts.map +1 -0
- package/lib/typescript/module/ui/session/tokenTransport.d.ts +24 -0
- package/lib/typescript/module/ui/session/tokenTransport.d.ts.map +1 -0
- package/lib/typescript/module/ui/utils/activeAuthuser.d.ts +21 -29
- package/lib/typescript/module/ui/utils/activeAuthuser.d.ts.map +1 -1
- package/lib/typescript/module/ui/utils/sessionHelpers.d.ts +0 -19
- package/lib/typescript/module/ui/utils/sessionHelpers.d.ts.map +1 -1
- package/lib/typescript/module/utils/deviceFlowSignIn.d.ts +51 -19
- package/lib/typescript/module/utils/deviceFlowSignIn.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/index.ts +9 -9
- package/src/ui/components/AccountMenu.tsx +117 -70
- package/src/ui/components/AccountSwitcher.tsx +88 -134
- package/src/ui/components/ProfileMenu.tsx +158 -162
- package/src/ui/components/SignInModal.tsx +5 -5
- package/src/ui/context/OxyContext.tsx +560 -475
- package/src/ui/context/hooks/useAuthOperations.ts +72 -63
- package/src/ui/context/inSessionTokenRefresh.ts +23 -42
- package/src/ui/context/silentSessionRestore.ts +12 -26
- package/src/ui/hooks/useAuth.ts +4 -3
- package/src/ui/hooks/useOxyAuthSession.ts +13 -8
- package/src/ui/hooks/useSessionManagement.ts +16 -87
- package/src/ui/hooks/useSwitchableAccounts.ts +397 -0
- package/src/ui/hooks/useWebSSO.ts +13 -227
- package/src/ui/index.ts +0 -1
- package/src/ui/screens/OxyAuthScreen.tsx +2 -2
- package/src/ui/session/__tests__/createSessionClient.test.ts +41 -0
- package/src/ui/session/__tests__/projectSessionState.test.ts +114 -0
- package/src/ui/session/__tests__/sessionClientHost.test.ts +43 -0
- package/src/ui/session/__tests__/tokenTransport.test.ts +142 -0
- package/src/ui/session/createSessionClient.ts +21 -0
- package/src/ui/session/index.ts +19 -0
- package/src/ui/session/projectSessionState.ts +85 -0
- package/src/ui/session/sessionClientHost.ts +27 -0
- package/src/ui/session/tokenTransport.ts +76 -0
- package/src/ui/utils/activeAuthuser.ts +34 -76
- package/src/ui/utils/sessionHelpers.ts +0 -66
- package/src/utils/__tests__/deviceFlowSignIn.test.ts +243 -34
- package/src/utils/deviceFlowSignIn.ts +98 -21
- package/lib/commonjs/ui/components/accountMenuRows.js +0 -33
- package/lib/commonjs/ui/components/accountMenuRows.js.map +0 -1
- package/lib/commonjs/ui/hooks/useDeviceAccounts.js +0 -280
- package/lib/commonjs/ui/hooks/useDeviceAccounts.js.map +0 -1
- package/lib/commonjs/ui/hooks/useSessionSocket.js +0 -203
- package/lib/commonjs/ui/hooks/useSessionSocket.js.map +0 -1
- package/lib/commonjs/utils/silentGuardKey.js +0 -54
- package/lib/commonjs/utils/silentGuardKey.js.map +0 -1
- package/lib/module/ui/components/accountMenuRows.js +0 -29
- package/lib/module/ui/components/accountMenuRows.js.map +0 -1
- package/lib/module/ui/hooks/useDeviceAccounts.js +0 -276
- package/lib/module/ui/hooks/useDeviceAccounts.js.map +0 -1
- package/lib/module/ui/hooks/useSessionSocket.js +0 -199
- package/lib/module/ui/hooks/useSessionSocket.js.map +0 -1
- package/lib/module/utils/silentGuardKey.js +0 -49
- package/lib/module/utils/silentGuardKey.js.map +0 -1
- package/lib/typescript/commonjs/ui/components/accountMenuRows.d.ts +0 -34
- package/lib/typescript/commonjs/ui/components/accountMenuRows.d.ts.map +0 -1
- package/lib/typescript/commonjs/ui/hooks/useDeviceAccounts.d.ts +0 -133
- package/lib/typescript/commonjs/ui/hooks/useDeviceAccounts.d.ts.map +0 -1
- package/lib/typescript/commonjs/ui/hooks/useSessionSocket.d.ts +0 -14
- package/lib/typescript/commonjs/ui/hooks/useSessionSocket.d.ts.map +0 -1
- package/lib/typescript/commonjs/utils/silentGuardKey.d.ts +0 -31
- package/lib/typescript/commonjs/utils/silentGuardKey.d.ts.map +0 -1
- package/lib/typescript/module/ui/components/accountMenuRows.d.ts +0 -34
- package/lib/typescript/module/ui/components/accountMenuRows.d.ts.map +0 -1
- package/lib/typescript/module/ui/hooks/useDeviceAccounts.d.ts +0 -133
- package/lib/typescript/module/ui/hooks/useDeviceAccounts.d.ts.map +0 -1
- package/lib/typescript/module/ui/hooks/useSessionSocket.d.ts +0 -14
- package/lib/typescript/module/ui/hooks/useSessionSocket.d.ts.map +0 -1
- package/lib/typescript/module/utils/silentGuardKey.d.ts +0 -31
- package/lib/typescript/module/utils/silentGuardKey.d.ts.map +0 -1
- package/src/ui/components/accountMenuRows.ts +0 -48
- package/src/ui/hooks/useDeviceAccounts.ts +0 -344
- package/src/ui/hooks/useSessionSocket.ts +0 -233
- package/src/ui/utils/__tests__/activeAuthuser.test.ts +0 -75
- package/src/utils/__tests__/silentGuardKey.test.ts +0 -82
- package/src/utils/silentGuardKey.ts +0 -46
|
@@ -9,7 +9,6 @@ var _core = require("@oxyhq/core");
|
|
|
9
9
|
var _bloom = require("@oxyhq/bloom");
|
|
10
10
|
var _authStore = require("../stores/authStore.js");
|
|
11
11
|
var _shallow = require("zustand/react/shallow");
|
|
12
|
-
var _useSessionSocket = require("../hooks/useSessionSocket.js");
|
|
13
12
|
var _useLanguageManagement = require("../hooks/useLanguageManagement.js");
|
|
14
13
|
var _useSessionManagement = require("../hooks/useSessionManagement.js");
|
|
15
14
|
var _useAuthOperations = require("./hooks/useAuthOperations.js");
|
|
@@ -23,10 +22,10 @@ var _queryClient = require("../hooks/queryClient.js");
|
|
|
23
22
|
var _useAvatarPicker = require("../hooks/useAvatarPicker.js");
|
|
24
23
|
var _accountStore = require("../stores/accountStore.js");
|
|
25
24
|
var _useWebSSO = require("../hooks/useWebSSO.js");
|
|
26
|
-
var _silentGuardKey = require("../../utils/silentGuardKey.js");
|
|
27
25
|
var _crossApex = require("../../utils/crossApex.js");
|
|
28
26
|
var _inSessionTokenRefresh = require("./inSessionTokenRefresh.js");
|
|
29
27
|
var _silentSessionRestore = require("./silentSessionRestore.js");
|
|
28
|
+
var _index = require("../session/index.js");
|
|
30
29
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
31
30
|
const OxyContext = /*#__PURE__*/(0, _react.createContext)(null);
|
|
32
31
|
|
|
@@ -34,8 +33,8 @@ const OxyContext = /*#__PURE__*/(0, _react.createContext)(null);
|
|
|
34
33
|
* Result of {@link OxyContextState.signInWithPassword}.
|
|
35
34
|
*
|
|
36
35
|
* `'ok'` — the password was accepted and the resulting session has been
|
|
37
|
-
* committed into context state (the SAME path
|
|
38
|
-
* `isAuthenticated` / `user` are updated and the session is durably persisted;
|
|
36
|
+
* committed into context state (the SAME path SSO / silent-restore sessions
|
|
37
|
+
* use), so `isAuthenticated` / `user` are updated and the session is durably persisted;
|
|
39
38
|
* the caller can proceed (e.g. navigate into the app).
|
|
40
39
|
*
|
|
41
40
|
* `'2fa_required'` — the account has two-factor auth enabled, so NO session was
|
|
@@ -47,41 +46,6 @@ const OxyContext = /*#__PURE__*/(0, _react.createContext)(null);
|
|
|
47
46
|
// `../utils/activeAuthuser` so the session-management and auth-operations hooks
|
|
48
47
|
// can share them without re-importing this 1k-line context file.
|
|
49
48
|
|
|
50
|
-
/**
|
|
51
|
-
* Module-level run-once guard for the cold-boot `fedcm-silent` step.
|
|
52
|
-
*
|
|
53
|
-
* The FedCM silent step triggers a one-shot `navigator.credentials.get`
|
|
54
|
-
* handshake that must fire AT MOST ONCE per page load — otherwise a provider
|
|
55
|
-
* remount storm (route churn, StrictMode double-invoke, error-boundary
|
|
56
|
-
* recovery) becomes a credential request storm. A per-instance ref resets on
|
|
57
|
-
* every remount, so the guard must live at module scope. Keyed on
|
|
58
|
-
* `origin|baseURL` so two providers pointed at the same API from the same
|
|
59
|
-
* origin share one attempt; never cleared because only a fresh page load can
|
|
60
|
-
* change the central IdP session state, and a fresh page load starts a fresh
|
|
61
|
-
* module scope.
|
|
62
|
-
*
|
|
63
|
-
* This is a dedicated set — distinct from `useWebSSO`'s `silentSSOAttempted`
|
|
64
|
-
* (which guards the post-boot INTERACTIVE button path) and never a core
|
|
65
|
-
* module-level singleton (that re-evaluates under Metro web bundling and the
|
|
66
|
-
* guard would not hold).
|
|
67
|
-
*/
|
|
68
|
-
const servicesSilentAttempted = new Set();
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Build the `origin|baseURL` signature used as the silent-cold-boot guard key.
|
|
72
|
-
*/
|
|
73
|
-
function silentColdBootKey(oxyServices) {
|
|
74
|
-
// `buildSilentGuardKey` reads `window.location.origin` behind a guard that
|
|
75
|
-
// also verifies `window.location` exists. This is critical: it runs
|
|
76
|
-
// UNCONDITIONALLY at the top of `restoreSessionsFromStorage` (before the
|
|
77
|
-
// cold-boot try/catch) on EVERY platform, and React Native aliases a global
|
|
78
|
-
// `window` with NO `window.location`. Without that guard the read threw
|
|
79
|
-
// `Cannot read property 'origin' of undefined` on native, escaping the
|
|
80
|
-
// restore path so `markAuthResolved` never ran and stored-session restore was
|
|
81
|
-
// never reached.
|
|
82
|
-
return (0, _silentGuardKey.buildSilentGuardKey)(() => oxyServices.getBaseURL?.());
|
|
83
|
-
}
|
|
84
|
-
|
|
85
49
|
/**
|
|
86
50
|
* Per-step fail-fast budget for the cold-boot silent iframe (`silentSignIn`
|
|
87
51
|
* against the per-apex `/auth/silent` host).
|
|
@@ -96,18 +60,6 @@ function silentColdBootKey(oxyServices) {
|
|
|
96
60
|
*/
|
|
97
61
|
const SILENT_IFRAME_TIMEOUT = 2500;
|
|
98
62
|
|
|
99
|
-
/**
|
|
100
|
-
* Per-step fail-fast budget for the cold-boot refresh-cookie restore
|
|
101
|
-
* (`refreshAllSessions`).
|
|
102
|
-
*
|
|
103
|
-
* On a cross-domain RP the `Domain=oxy.so` refresh cookie never reaches
|
|
104
|
-
* `api.<apex>`, so this request returns no accounts (or stalls behind a slow
|
|
105
|
-
* endpoint) with no useful answer. As one cold-boot step it must not block the
|
|
106
|
-
* fall-through to the terminal `/sso` bounce. 3s bounds the wait while leaving
|
|
107
|
-
* ample headroom for a genuine first-party `*.oxy.so` rotation round-trip.
|
|
108
|
-
*/
|
|
109
|
-
const COOKIE_RESTORE_TIMEOUT = 3000;
|
|
110
|
-
|
|
111
63
|
/**
|
|
112
64
|
* Per-step fail-fast budget (ms) for the native shared-key cold-boot step
|
|
113
65
|
* (`signInWithSharedIdentity`).
|
|
@@ -125,15 +77,14 @@ const SHARED_KEY_SIGNIN_TIMEOUT = 8000;
|
|
|
125
77
|
/**
|
|
126
78
|
* HARD overall deadline (ms) for the entire cold-boot step loop —
|
|
127
79
|
* defense-in-depth so a single non-settling step can NEVER hang auth resolution
|
|
128
|
-
* forever (the production regression: a
|
|
129
|
-
* ignored its abort signal left
|
|
130
|
-
* `runColdBoot` never advanced to the terminal `/sso`
|
|
131
|
-
* indefinitely).
|
|
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).
|
|
132
84
|
*
|
|
133
85
|
* Every step ALREADY bounds its own network work (the stored-session bearer
|
|
134
|
-
* validation at 8s, the silent iframe at `SILENT_IFRAME_TIMEOUT
|
|
135
|
-
*
|
|
136
|
-
* plus its hard settle). On a healthy load the FIRST recovering step wins in a
|
|
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
|
|
137
88
|
* single round-trip (1–3s) and the chain short-circuits long before this fires.
|
|
138
89
|
* This budget only trips when one of those per-step bounds regresses.
|
|
139
90
|
*
|
|
@@ -150,6 +101,23 @@ const SHARED_KEY_SIGNIN_TIMEOUT = 8000;
|
|
|
150
101
|
*/
|
|
151
102
|
const COLD_BOOT_OVERALL_DEADLINE = 20000;
|
|
152
103
|
|
|
104
|
+
/**
|
|
105
|
+
* How long cold boot WAITS for the post-ladder SessionClient handoff
|
|
106
|
+
* (`addCurrentAccount` + `start` + `syncFromClient`) before it resolves auth
|
|
107
|
+
* and stops blocking. Once a ladder step planted a token the user is already
|
|
108
|
+
* authenticated — the handoff only populates the multi-account set and the
|
|
109
|
+
* server-authoritative active account, and those also arrive via the socket
|
|
110
|
+
* subscription wired in `useEffect`. So on a slow/unresponsive backend we stop
|
|
111
|
+
* AWAITING the handoff here (it keeps running in the background and projects
|
|
112
|
+
* when it lands) rather than delaying `markAuthResolved` — otherwise the two
|
|
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.
|
|
118
|
+
*/
|
|
119
|
+
const SESSION_HANDOFF_DEADLINE = 6000;
|
|
120
|
+
|
|
153
121
|
/**
|
|
154
122
|
* Per-session soft timeout (ms) for the parallel stored-session validation in
|
|
155
123
|
* `restoreStoredSession`. Each `validateSession` call races against this timer
|
|
@@ -408,9 +376,10 @@ const OxyProvider = ({
|
|
|
408
376
|
// hooks below. But it is `null` for a brief window after mount while
|
|
409
377
|
// `createPlatformStorage()` resolves (a microtask on web; a dynamic
|
|
410
378
|
// `import()` on native). Any persistence path that fires during that window
|
|
411
|
-
// — e.g. an interactive
|
|
412
|
-
// read `storage === null` and SILENTLY skip writing the session,
|
|
413
|
-
// user signed-in in-memory but with nothing to restore on
|
|
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.
|
|
414
383
|
//
|
|
415
384
|
// To make persistence robust regardless of timing we ALSO expose the storage
|
|
416
385
|
// as an awaitable promise (`getReadyStorage`). Persistence code awaits the
|
|
@@ -514,10 +483,8 @@ const OxyProvider = ({
|
|
|
514
483
|
setActiveSessionId,
|
|
515
484
|
updateSessions,
|
|
516
485
|
switchSession,
|
|
517
|
-
refreshSessions,
|
|
518
486
|
clearSessionState,
|
|
519
|
-
saveActiveSessionId
|
|
520
|
-
trackRemovedSession
|
|
487
|
+
saveActiveSessionId
|
|
521
488
|
} = (0, _useSessionManagement.useSessionManagement)({
|
|
522
489
|
oxyServices,
|
|
523
490
|
storage,
|
|
@@ -534,6 +501,139 @@ const OxyProvider = ({
|
|
|
534
501
|
setTokenReady,
|
|
535
502
|
queryClient
|
|
536
503
|
});
|
|
504
|
+
|
|
505
|
+
// Session-sync integration layer (Fase 3-A -> 3-B). Built ONCE per
|
|
506
|
+
// `oxyServices` instance via a lazy ref (mirrors the `oxyServicesRef`
|
|
507
|
+
// pattern above) so the underlying `SessionClient` — and its socket
|
|
508
|
+
// connection, once started — is never recreated across renders.
|
|
509
|
+
//
|
|
510
|
+
// ADDITIVE + INERT for this task: `client.start()` is NOT called here (that
|
|
511
|
+
// is Task 2's job, which also retires the 8-step cold boot below). Until
|
|
512
|
+
// then `client.getState()` never advances past `null`, so `syncFromClient`
|
|
513
|
+
// is a guaranteed no-op in production — the existing cold-boot-driven state
|
|
514
|
+
// remains the sole authority. The wiring exists so the projection can be
|
|
515
|
+
// exercised in isolation against a controlled client before the authority
|
|
516
|
+
// flip.
|
|
517
|
+
const sessionClientPairRef = (0, _react.useRef)(null);
|
|
518
|
+
if (!sessionClientPairRef.current) {
|
|
519
|
+
sessionClientPairRef.current = (0, _index.createSessionClient)(oxyServices);
|
|
520
|
+
}
|
|
521
|
+
const {
|
|
522
|
+
client: sessionClient,
|
|
523
|
+
host: sessionClientHost
|
|
524
|
+
} = sessionClientPairRef.current;
|
|
525
|
+
|
|
526
|
+
// Cold-boot registration dedup (Task 5). `handleWebSSOSession` (declared
|
|
527
|
+
// below) is the single commit funnel for every web ladder step that mints a
|
|
528
|
+
// session (`sso-return`, `shared-key-signin`, `silent-iframe`) — it registers
|
|
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.
|
|
579
|
+
const syncFromClient = (0, _react.useCallback)(async () => {
|
|
580
|
+
const state = sessionClient.getState();
|
|
581
|
+
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
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
if (state.accounts.length === 0) {
|
|
587
|
+
sessionClientHost.setCurrentAccountId(null);
|
|
588
|
+
if ((0, _useWebSSO.isWebBrowser)()) {
|
|
589
|
+
(0, _activeAuthuser.markSignedOut)();
|
|
590
|
+
}
|
|
591
|
+
(0, _activeAuthuser.clearSsoBounceState)();
|
|
592
|
+
(0, _useAuthOperations.clearPriorSessionHintSafe)(clearPriorSessionHint, logger);
|
|
593
|
+
await clearSessionState();
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
// LAST-WRITE-WINS GUARD (review I3): `syncFromClient` is called
|
|
597
|
+
// concurrently — once per socket `notify()` push AND once per direct
|
|
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.
|
|
607
|
+
const capturedRevision = state.revision;
|
|
608
|
+
const ids = (0, _index.accountIdsOf)(state);
|
|
609
|
+
let users = [];
|
|
610
|
+
try {
|
|
611
|
+
users = ids.length > 0 ? await oxyServices.getUsersByIds(ids) : [];
|
|
612
|
+
} 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
|
+
logger('Failed to resolve account profiles during syncFromClient', fetchError);
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
const latest = sessionClient.getState();
|
|
620
|
+
if (!latest || latest.revision !== capturedRevision) {
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
const usersById = new Map(users.map(resolvedUser => [resolvedUser.id, resolvedUser]));
|
|
624
|
+
updateSessions((0, _index.deviceStateToClientSessions)(latest, usersById));
|
|
625
|
+
setActiveSessionId((0, _index.activeSessionIdOf)(latest));
|
|
626
|
+
const activeUser = (0, _index.activeUserOf)(latest, usersById);
|
|
627
|
+
if (activeUser) {
|
|
628
|
+
loginSuccess(activeUser);
|
|
629
|
+
}
|
|
630
|
+
sessionClientHost.setCurrentAccountId(latest.activeAccountId);
|
|
631
|
+
}, [oxyServices, sessionClient, sessionClientHost, updateSessions, setActiveSessionId, loginSuccess, clearSessionState, clearPriorSessionHint, logger]);
|
|
632
|
+
(0, _react.useEffect)(() => {
|
|
633
|
+
return sessionClient.subscribe(() => {
|
|
634
|
+
void syncFromClient();
|
|
635
|
+
});
|
|
636
|
+
}, [sessionClient, syncFromClient]);
|
|
537
637
|
const {
|
|
538
638
|
signIn,
|
|
539
639
|
logout,
|
|
@@ -541,7 +641,6 @@ const OxyProvider = ({
|
|
|
541
641
|
} = (0, _useAuthOperations.useAuthOperations)({
|
|
542
642
|
oxyServices,
|
|
543
643
|
storage,
|
|
544
|
-
sessions,
|
|
545
644
|
activeSessionId,
|
|
546
645
|
setActiveSessionId,
|
|
547
646
|
updateSessions,
|
|
@@ -549,6 +648,8 @@ const OxyProvider = ({
|
|
|
549
648
|
clearSessionState,
|
|
550
649
|
clearPriorSessionHint,
|
|
551
650
|
switchSession,
|
|
651
|
+
sessionClient,
|
|
652
|
+
syncFromClient,
|
|
552
653
|
applyLanguagePreference,
|
|
553
654
|
onAuthStateChange,
|
|
554
655
|
onError,
|
|
@@ -676,7 +777,7 @@ const OxyProvider = ({
|
|
|
676
777
|
// init. Callers MUST invoke this BEFORE any work that can trigger a route
|
|
677
778
|
// navigation (`onAuthStateChange`) — navigation can interrupt a still-pending
|
|
678
779
|
// async write, which is exactly what once left `session_ids` empty after a
|
|
679
|
-
// successful sign-in. Shared by the
|
|
780
|
+
// successful sign-in. Shared by the SSO/silent-restore path and the cold-boot
|
|
680
781
|
// refresh-cookie restore so both land the same durable record.
|
|
681
782
|
const persistSessionDurably = (0, _react.useCallback)(async sessionId => {
|
|
682
783
|
const readyStorage = await getReadyStorage();
|
|
@@ -695,7 +796,7 @@ const OxyProvider = ({
|
|
|
695
796
|
// A session is now durably committed — set the returning-user hint so a
|
|
696
797
|
// future cold boot whose local session has lapsed still gets ONE `/sso`
|
|
697
798
|
// establish bounce (see `markPriorSessionHint`). Every web commit path
|
|
698
|
-
// (
|
|
799
|
+
// (silent iframe / SSO return / password / cookie restore) funnels
|
|
699
800
|
// through here, so this is the single chokepoint for the hint.
|
|
700
801
|
await readyStorage.setItem(storageKeys.priorSession, '1');
|
|
701
802
|
}, [getReadyStorage, logger, storageKeys.activeSessionId, storageKeys.sessionIds, storageKeys.priorSession]);
|
|
@@ -714,10 +815,10 @@ const OxyProvider = ({
|
|
|
714
815
|
// Idempotent and monotonic via `authResolvedRef`: the first call wins and the
|
|
715
816
|
// setters fire at most once, so the restore `finally` backstop becomes a no-op
|
|
716
817
|
// once a commit site has already marked resolution. Called from EVERY place a
|
|
717
|
-
// user is actually committed (the
|
|
718
|
-
// `handleWebSSOSession
|
|
719
|
-
//
|
|
720
|
-
//
|
|
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.
|
|
721
822
|
const markAuthResolved = (0, _react.useCallback)(() => {
|
|
722
823
|
if (authResolvedRef.current) {
|
|
723
824
|
return;
|
|
@@ -730,110 +831,13 @@ const OxyProvider = ({
|
|
|
730
831
|
markAuthResolvedRef.current = markAuthResolved;
|
|
731
832
|
|
|
732
833
|
// `handleWebSSOSession` is declared further down (it depends on values that
|
|
733
|
-
// are only available there). The
|
|
834
|
+
// are only available there). The iframe/SSO cold-boot steps need to commit
|
|
734
835
|
// a recovered session through it, so we route the call through a ref that is
|
|
735
836
|
// populated once the callback exists. The ref is assigned synchronously on
|
|
736
837
|
// every render before the cold-boot effect can fire (the effect is gated on
|
|
737
838
|
// `storage` + `initialized`, both of which settle after first render).
|
|
738
839
|
const handleWebSSOSessionRef = (0, _react.useRef)(null);
|
|
739
840
|
|
|
740
|
-
// Cold-boot session restore via the secure refresh cookies (web only).
|
|
741
|
-
//
|
|
742
|
-
// Calls `oxyServices.refreshAllSessions()` → `POST /auth/refresh-all` with
|
|
743
|
-
// `credentials: 'include'`. The server rotates every device-local
|
|
744
|
-
// `oxy_rt_${authuser}` cookie in parallel and returns one entry per valid
|
|
745
|
-
// account (Google-style multi-account).
|
|
746
|
-
//
|
|
747
|
-
// Active-account selection: the persisted `oxy_active_authuser` slot index
|
|
748
|
-
// wins when it matches a returned account; otherwise the lowest `authuser`
|
|
749
|
-
// is picked. JS never sees the refresh cookies (httpOnly).
|
|
750
|
-
//
|
|
751
|
-
// Returns `true` when at least one session was restored (caller short-
|
|
752
|
-
// circuits the bearer path); `false` on no signed-in accounts / any failure
|
|
753
|
-
// (caller proceeds unauthenticated through the existing flow — nothing is
|
|
754
|
-
// cleared).
|
|
755
|
-
const restoreViaRefreshCookie = (0, _react.useCallback)(async () => {
|
|
756
|
-
if (!(0, _useWebSSO.isWebBrowser)()) {
|
|
757
|
-
return false;
|
|
758
|
-
}
|
|
759
|
-
let snapshot;
|
|
760
|
-
try {
|
|
761
|
-
// Bound the refresh so a cross-domain/stalled call cannot hang the cold
|
|
762
|
-
// boot in front of the terminal `/sso` bounce (see COOKIE_RESTORE_TIMEOUT).
|
|
763
|
-
snapshot = await oxyServices.refreshAllSessions({
|
|
764
|
-
timeout: COOKIE_RESTORE_TIMEOUT
|
|
765
|
-
});
|
|
766
|
-
} catch (fetchError) {
|
|
767
|
-
// Offline / network error — fall through to the cached/stored-session flow.
|
|
768
|
-
if (__DEV__) {
|
|
769
|
-
_core.logger.debug('Refresh-all cookie restore network error (expected when offline)', {
|
|
770
|
-
component: 'OxyContext',
|
|
771
|
-
method: 'restoreViaRefreshCookie'
|
|
772
|
-
}, fetchError);
|
|
773
|
-
}
|
|
774
|
-
return false;
|
|
775
|
-
}
|
|
776
|
-
if (snapshot.accounts.length === 0) {
|
|
777
|
-
return false;
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
// Pick the active account: persisted authuser if it still matches a returned
|
|
781
|
-
// account, otherwise the lowest authuser (deterministic). The server has
|
|
782
|
-
// already sorted ascending so [0] is the lowest.
|
|
783
|
-
const activeAccount = (0, _silentSessionRestore.selectActiveRefreshAccount)(snapshot.accounts, (0, _activeAuthuser.readActiveAuthuser)());
|
|
784
|
-
|
|
785
|
-
// Plant the active access token. Sibling accounts' access tokens stay in
|
|
786
|
-
// the snapshot (the chooser can drive a per-account refresh via
|
|
787
|
-
// `refreshTokenViaCookie({authuser})` on switch).
|
|
788
|
-
oxyServices.httpService.setTokens(activeAccount.accessToken);
|
|
789
|
-
|
|
790
|
-
// Fetch the full user with the freshly planted token. The refresh-all
|
|
791
|
-
// payload includes a minimal user shape (id, username, name, avatar,
|
|
792
|
-
// email, color) — sufficient for the chooser but the auth store wants the
|
|
793
|
-
// canonical User document for downstream rendering.
|
|
794
|
-
let fullUser;
|
|
795
|
-
try {
|
|
796
|
-
fullUser = await oxyServices.getCurrentUser();
|
|
797
|
-
} catch (userError) {
|
|
798
|
-
// Token planted but profile fetch failed (e.g. transient network). Do
|
|
799
|
-
// not claim a restored session; fall through so the stored-session flow
|
|
800
|
-
// can retry. Leave the planted token in place — it is valid and harmless.
|
|
801
|
-
if (__DEV__) {
|
|
802
|
-
_core.logger.debug('Refresh-all cookie restore: getCurrentUser failed', {
|
|
803
|
-
component: 'OxyContext',
|
|
804
|
-
method: 'restoreViaRefreshCookie'
|
|
805
|
-
}, userError);
|
|
806
|
-
}
|
|
807
|
-
return false;
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
// Build a ClientSession per returned account so the multi-session store
|
|
811
|
-
// reflects every device-local slot, not just the active one. The active
|
|
812
|
-
// account is flagged `isCurrent: true`.
|
|
813
|
-
const now = new Date();
|
|
814
|
-
const clientSessions = snapshot.accounts.map(account => ({
|
|
815
|
-
sessionId: account.sessionId,
|
|
816
|
-
deviceId: '',
|
|
817
|
-
expiresAt: account.expiresAt || new Date(now.getTime() + DEFAULT_SESSION_VALIDITY_MS).toISOString(),
|
|
818
|
-
lastActive: now.toISOString(),
|
|
819
|
-
userId: account.user?.id,
|
|
820
|
-
isCurrent: account.sessionId === activeAccount.sessionId,
|
|
821
|
-
authuser: account.authuser
|
|
822
|
-
}));
|
|
823
|
-
updateSessionsRef.current(clientSessions, {
|
|
824
|
-
merge: true
|
|
825
|
-
});
|
|
826
|
-
setActiveSessionIdRef.current(activeAccount.sessionId);
|
|
827
|
-
(0, _activeAuthuser.writeActiveAuthuser)(activeAccount.authuser);
|
|
828
|
-
await persistSessionDurably(activeAccount.sessionId);
|
|
829
|
-
loginSuccessRef.current(fullUser);
|
|
830
|
-
// A session is now committed — unblock the auth-resolution gate immediately
|
|
831
|
-
// rather than waiting for `runColdBoot` to return (idempotent).
|
|
832
|
-
markAuthResolvedRef.current();
|
|
833
|
-
onAuthStateChangeRef.current?.(fullUser);
|
|
834
|
-
return true;
|
|
835
|
-
}, [oxyServices, persistSessionDurably]);
|
|
836
|
-
|
|
837
841
|
// Native (and offline) stored-session restore — the ONLY restore path that
|
|
838
842
|
// runs on React Native, and the web fallback when no cross-domain step won.
|
|
839
843
|
//
|
|
@@ -848,8 +852,23 @@ const OxyProvider = ({
|
|
|
848
852
|
const storedSessionIdsJson = await storage.getItem(storageKeys.sessionIds);
|
|
849
853
|
const storedSessionIdsFromStorage = storedSessionIdsJson ? JSON.parse(storedSessionIdsJson) : [];
|
|
850
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.
|
|
851
860
|
const storedActiveAuthuser = (0, _useWebSSO.isWebBrowser)() ? (0, _activeAuthuser.readActiveAuthuser)() : null;
|
|
852
|
-
|
|
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) {
|
|
853
872
|
return false;
|
|
854
873
|
}
|
|
855
874
|
const nativeSharedSession = !(0, _useWebSSO.isWebBrowser)() ? await _core.KeyManager.getSharedSession().catch(() => null) : null;
|
|
@@ -946,23 +965,59 @@ const OxyProvider = ({
|
|
|
946
965
|
} catch (switchError) {
|
|
947
966
|
// Silently handle expected errors (invalid sessions, timeouts, network issues)
|
|
948
967
|
if ((0, _errorHandlers.isInvalidSessionError)(switchError)) {
|
|
968
|
+
// The stored active id was rejected server-side. Clear it and drop the
|
|
969
|
+
// dead session from the local view, then FALL THROUGH to the election
|
|
970
|
+
// below to promote a surviving valid session — a stale/removed active
|
|
971
|
+
// marker must not strand the other still-valid accounts.
|
|
949
972
|
await storage.removeItem(storageKeys.activeSessionId);
|
|
950
|
-
|
|
973
|
+
validSessions = validSessions.filter(session => session.sessionId !== storedActiveSessionId);
|
|
974
|
+
updateSessionsRef.current(validSessions, {
|
|
951
975
|
merge: false,
|
|
952
976
|
preserveSessionIds: unvalidatedSessionIds
|
|
953
977
|
});
|
|
954
978
|
// Don't log expected session errors during restoration
|
|
955
979
|
} else if ((0, _errorHandlers.isTimeoutOrNetworkError)(switchError)) {
|
|
956
|
-
// Timeout/network
|
|
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.
|
|
957
984
|
if (__DEV__) {
|
|
958
985
|
_core.logger.debug('Active session validation timeout (expected when offline)', {
|
|
959
986
|
component: 'OxyContext',
|
|
960
987
|
method: 'restoreStoredSession'
|
|
961
988
|
}, switchError);
|
|
962
989
|
}
|
|
990
|
+
return false;
|
|
963
991
|
} else {
|
|
964
|
-
// Only log unexpected errors
|
|
992
|
+
// Only log unexpected errors, and do not elect on an ambiguous failure.
|
|
965
993
|
logger('Active session validation error', switchError);
|
|
994
|
+
return false;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// Election fallback. Reached when there is no usable stored active id —
|
|
1000
|
+
// either it was never persisted (e.g. removed after a server-side revocation
|
|
1001
|
+
// while sibling sessions survived) or it was just rejected as invalid above.
|
|
1002
|
+
// Promote the FIRST validated session to active and commit it through the
|
|
1003
|
+
// same `switchSession` path. The server owns which account is truly active,
|
|
1004
|
+
// so any valid session is a safe provisional active — the post-restore
|
|
1005
|
+
// SessionClient handoff reconciles the real active account immediately after.
|
|
1006
|
+
const electedSession = validSessions[0];
|
|
1007
|
+
if (electedSession) {
|
|
1008
|
+
try {
|
|
1009
|
+
await switchSessionRef.current(electedSession.sessionId);
|
|
1010
|
+
await storage.setItem(storageKeys.activeSessionId, electedSession.sessionId);
|
|
1011
|
+
await storage.setItem(storageKeys.priorSession, '1');
|
|
1012
|
+
markAuthResolvedRef.current();
|
|
1013
|
+
return true;
|
|
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);
|
|
966
1021
|
}
|
|
967
1022
|
}
|
|
968
1023
|
}
|
|
@@ -1037,21 +1092,31 @@ const OxyProvider = ({
|
|
|
1037
1092
|
// web-only step is gated by `isWebBrowser()`, so on native ONLY
|
|
1038
1093
|
// `stored-session` runs.
|
|
1039
1094
|
//
|
|
1040
|
-
// Order (web): SSO return → stored session →
|
|
1041
|
-
//
|
|
1042
|
-
//
|
|
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`).
|
|
1043
1109
|
//
|
|
1044
|
-
// LATENCY (FIX A): `stored-session` runs BEFORE the slow no-redirect
|
|
1045
|
-
//
|
|
1046
|
-
//
|
|
1047
|
-
//
|
|
1048
|
-
//
|
|
1049
|
-
//
|
|
1050
|
-
//
|
|
1051
|
-
//
|
|
1052
|
-
//
|
|
1053
|
-
//
|
|
1054
|
-
// `sso-bounce` never fires (no flash, no loop).
|
|
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).
|
|
1055
1120
|
// Order (native): stored session only (every web-only step is disabled
|
|
1056
1121
|
// off-browser).
|
|
1057
1122
|
const restoreSessionsFromStorage = (0, _react.useCallback)(async () => {
|
|
@@ -1059,18 +1124,18 @@ const OxyProvider = ({
|
|
|
1059
1124
|
return;
|
|
1060
1125
|
}
|
|
1061
1126
|
setTokenReady(false);
|
|
1127
|
+
// Fresh per-boot flag — see the declaration comment above `sessionClient`.
|
|
1128
|
+
registeredDuringBootRef.current = false;
|
|
1062
1129
|
const commitWebSession = handleWebSSOSessionRef.current;
|
|
1063
|
-
const silentKey = silentColdBootKey(oxyServices);
|
|
1064
|
-
const fedcmSupported = (0, _useWebSSO.isWebBrowser)() && oxyServices.isFedCMSupported?.() === true;
|
|
1065
1130
|
|
|
1066
1131
|
// FIX-B precondition flag: set true the instant the (now-earlier)
|
|
1067
|
-
// `stored-session` step recovers a local bearer session. The slow
|
|
1068
|
-
//
|
|
1069
|
-
// so
|
|
1132
|
+
// `stored-session` step recovers a local bearer session. The slow
|
|
1133
|
+
// web-only `silent-iframe` probe is `enabled` on `!storedSessionRestored`
|
|
1134
|
+
// so it is explicitly skipped once a local session won. `runColdBoot`
|
|
1070
1135
|
// already short-circuits on the first `{kind:'session'}`, so on a winning
|
|
1071
|
-
// reload
|
|
1136
|
+
// reload that `enabled` body is never even reached — this flag makes the
|
|
1072
1137
|
// intent explicit and is redundant-safe. On a first-visit-no-local-session,
|
|
1073
|
-
// `stored-session` skips, this stays false, and the
|
|
1138
|
+
// `stored-session` skips, this stays false, and the probe runs as before.
|
|
1074
1139
|
let storedSessionRestored = false;
|
|
1075
1140
|
|
|
1076
1141
|
// FIX-B smart-gate input: has this device/app EVER had a signed-in Oxy
|
|
@@ -1093,52 +1158,16 @@ const OxyProvider = ({
|
|
|
1093
1158
|
}
|
|
1094
1159
|
}
|
|
1095
1160
|
|
|
1096
|
-
//
|
|
1097
|
-
//
|
|
1098
|
-
//
|
|
1099
|
-
//
|
|
1100
|
-
//
|
|
1101
|
-
//
|
|
1102
|
-
// `
|
|
1103
|
-
//
|
|
1104
|
-
//
|
|
1105
|
-
//
|
|
1106
|
-
// run cookie-restore BEFORE those probes (and disable the later duplicate).
|
|
1107
|
-
//
|
|
1108
|
-
// Read ONCE here (synchronously usable by the step `enabled` gates below).
|
|
1109
|
-
// It is non-null ONLY on first-party web apps that persist the slot. Both the
|
|
1110
|
-
// device sign-in/switch paths AND `handleWebSSOSession` (FedCM/SSO/credentials
|
|
1111
|
-
// commit funnel) now persist it — but only when the session genuinely joined
|
|
1112
|
-
// the device set: `establishDeviceRefreshSlot` returns an `authuser` ONLY on
|
|
1113
|
-
// first-party web (same registrable apex as the API), so a cross-apex RP never
|
|
1114
|
-
// writes a phantom slot and native has no localStorage. Both therefore keep
|
|
1115
|
-
// their existing order untouched. The earlier `stored-session` step still runs
|
|
1116
|
-
// first (FIX-A latency), and when a slot exists but its cookies have all
|
|
1117
|
-
// lapsed, cookie-restore simply returns no accounts and the chain falls through
|
|
1118
|
-
// to the cross-domain fallbacks exactly as before.
|
|
1119
|
-
const prioritizeMultiAccount = (0, _useWebSSO.isWebBrowser)() && (0, _activeAuthuser.readActiveAuthuser)() !== null;
|
|
1120
|
-
|
|
1121
|
-
// DELIBERATELY-SIGNED-OUT gate (web): when the user pressed "Sign out", any
|
|
1122
|
-
// credential that can silently re-mint a session may still be live on the
|
|
1123
|
-
// next reload — the central IdP session (FedCM credential association /
|
|
1124
|
-
// per-apex `fedcm_session` cookie) AND the device refresh cookies. Since
|
|
1125
|
-
// PR #455 the PRIMARY web session also joins the `oxy_rt_<authuser>` device
|
|
1126
|
-
// set, so the un-gated cookie-restore steps would `refresh-all` that still-
|
|
1127
|
-
// present cookie and sign the user back in without intent — exactly the
|
|
1128
|
-
// "sign-out is silently undone" regression. So this flag gates EVERY
|
|
1129
|
-
// AUTOMATIC silent-restore cold-boot step: `stored-session`,
|
|
1130
|
-
// `cookie-restore-active`, `fedcm-silent`, `silent-iframe`, and
|
|
1131
|
-
// `cookie-restore`. The gate — not a cookie wipe — is the authority: the
|
|
1132
|
-
// account may stay "known" for a fast deliberate re-sign-in, but no step
|
|
1133
|
-
// restores it silently while the flag is set. Read the durable flag ONCE here
|
|
1134
|
-
// (synchronously usable by every step `enabled` gate). Any deliberate sign-in
|
|
1135
|
-
// (password, FedCM, account switch, device claim) clears it, so there is no
|
|
1136
|
-
// "stuck signed out" state. The `sso-bounce` step needs no extra gate: it is
|
|
1137
|
-
// already self-suppressed after sign-out (its `hasPriorSession` hint is
|
|
1138
|
-
// cleared). `sso-return` is NOT gated — it commits the result of a deliberate
|
|
1139
|
-
// top-level `/sso` bounce the user just initiated. `shared-key-signin` is
|
|
1140
|
-
// native-only and the flag never sets on native (`markSignedOut` no-ops
|
|
1141
|
-
// off-web), so its gate would be moot; it is left untouched.
|
|
1161
|
+
// DELIBERATELY-SIGNED-OUT gate (web): when the user pressed "Sign out", the
|
|
1162
|
+
// per-apex `fedcm_session` cookie can still be live, so the AUTOMATIC
|
|
1163
|
+
// `silent-iframe` step below would re-mint a session on the very next cold
|
|
1164
|
+
// boot and sign the user back in without intent. Read the durable flag
|
|
1165
|
+
// ONCE here (synchronously usable by the step `enabled` gate) and skip
|
|
1166
|
+
// that step while it is set. Any deliberate sign-in clears it. The
|
|
1167
|
+
// `sso-bounce` step is already self-suppressed after sign-out (its
|
|
1168
|
+
// `hasPriorSession` hint is cleared), and the local `stored-session`
|
|
1169
|
+
// restore step is unaffected — a deliberate sign-out clears those
|
|
1170
|
+
// credentials anyway.
|
|
1142
1171
|
const silentRestoreBlocked = (0, _activeAuthuser.isSilentRestoreSuppressed)();
|
|
1143
1172
|
try {
|
|
1144
1173
|
const outcome = await (0, _core.runColdBoot)({
|
|
@@ -1160,28 +1189,22 @@ const OxyProvider = ({
|
|
|
1160
1189
|
};
|
|
1161
1190
|
}
|
|
1162
1191
|
}, {
|
|
1163
|
-
// 2) Stored-session bearer restore.
|
|
1164
|
-
//
|
|
1165
|
-
//
|
|
1166
|
-
//
|
|
1192
|
+
// 2) Stored-session bearer restore. NO `enabled` gate — runs on ALL
|
|
1193
|
+
// platforms. This is native's ONLY restore path (every web-only step
|
|
1194
|
+
// is disabled off-browser, so native reaches exactly this) AND the
|
|
1195
|
+
// common WEB reload winner.
|
|
1167
1196
|
//
|
|
1168
|
-
//
|
|
1169
|
-
//
|
|
1170
|
-
//
|
|
1171
|
-
//
|
|
1172
|
-
//
|
|
1173
|
-
//
|
|
1174
|
-
//
|
|
1175
|
-
//
|
|
1176
|
-
//
|
|
1177
|
-
//
|
|
1178
|
-
// ~20-30s serial stall). The `sso-return` step stays AHEAD of this
|
|
1179
|
-
// one — it must consume the URL fragment before any
|
|
1180
|
-
// later step (or anything else) strips it. On a first visit with no
|
|
1181
|
-
// local session this step skips and the cross-domain fallback chain
|
|
1182
|
-
// (fedcm → iframe → cookie → sso-bounce) runs exactly as before.
|
|
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.
|
|
1183
1207
|
id: 'stored-session',
|
|
1184
|
-
enabled: () => !silentRestoreBlocked,
|
|
1185
1208
|
run: async () => {
|
|
1186
1209
|
const restored = await restoreStoredSession();
|
|
1187
1210
|
if (restored) {
|
|
@@ -1251,66 +1274,7 @@ const OxyProvider = ({
|
|
|
1251
1274
|
};
|
|
1252
1275
|
}
|
|
1253
1276
|
}, {
|
|
1254
|
-
//
|
|
1255
|
-
// ONLY when an explicit persisted slot selection exists
|
|
1256
|
-
// (`prioritizeMultiAccount`) — i.e. the user previously signed in or
|
|
1257
|
-
// SWITCHED accounts on this first-party app. It runs BEFORE the
|
|
1258
|
-
// `fedcm-silent` / `silent-iframe` probes (which only ever recover the
|
|
1259
|
-
// PRIMARY central session) so a switched managed/org account survives
|
|
1260
|
-
// reload instead of being clobbered back to the primary. The restore
|
|
1261
|
-
// itself is the SAME `restoreViaRefreshCookie` as the terminal-tier
|
|
1262
|
-
// step below: it rotates every device-local `oxy_rt_<authuser>` cookie
|
|
1263
|
-
// and picks the persisted slot via `selectActiveRefreshAccount`,
|
|
1264
|
-
// committing it (token + state + durable persistence + `markAuthResolved`).
|
|
1265
|
-
// When no slot is persisted this is disabled and the original order is
|
|
1266
|
-
// unchanged; when the slot's cookies have lapsed it returns no accounts
|
|
1267
|
-
// and the chain falls through to the cross-domain fallbacks below.
|
|
1268
|
-
//
|
|
1269
|
-
// SIGNED-OUT GATE: since PR #455 the primary's `oxy_rt` slot survives a
|
|
1270
|
-
// deliberate sign-out, so without this gate `refresh-all` would re-mint
|
|
1271
|
-
// and silently re-log-in the user on reload. The flag is cleared by any
|
|
1272
|
-
// deliberate sign-in (including an account SWITCH, which also writes the
|
|
1273
|
-
// new active slot), so the switch-survives-reload path is unaffected.
|
|
1274
|
-
id: 'cookie-restore-active',
|
|
1275
|
-
enabled: () => prioritizeMultiAccount && !silentRestoreBlocked,
|
|
1276
|
-
run: async () => {
|
|
1277
|
-
const restored = await restoreViaRefreshCookie();
|
|
1278
|
-
return restored ? {
|
|
1279
|
-
kind: 'session',
|
|
1280
|
-
session: true
|
|
1281
|
-
} : {
|
|
1282
|
-
kind: 'skip'
|
|
1283
|
-
};
|
|
1284
|
-
}
|
|
1285
|
-
}, {
|
|
1286
|
-
// 3) FedCM silent reauthn (Chrome) against the CENTRAL IdP
|
|
1287
|
-
// (auth.oxy.so). `silentSignInWithFedCM` plants the access token
|
|
1288
|
-
// internally; we commit the returned session via
|
|
1289
|
-
// `handleWebSSOSession`. Guarded so it fires at most once per page
|
|
1290
|
-
// load across remounts. This is an enhancement layered above the
|
|
1291
|
-
// opaque-code bounce: when it succeeds the bounce never fires.
|
|
1292
|
-
//
|
|
1293
|
-
// FIX-B: additionally skipped when the earlier `stored-session` step
|
|
1294
|
-
// already recovered a local session — the probe cannot improve on a
|
|
1295
|
-
// valid local bearer, and skipping it avoids the silent round-trip.
|
|
1296
|
-
id: 'fedcm-silent',
|
|
1297
|
-
enabled: () => !storedSessionRestored && !silentRestoreBlocked && fedcmSupported && !servicesSilentAttempted.has(silentKey),
|
|
1298
|
-
run: async () => {
|
|
1299
|
-
servicesSilentAttempted.add(silentKey);
|
|
1300
|
-
const session = await oxyServices.silentSignInWithFedCM?.();
|
|
1301
|
-
if (!session || !commitWebSession) {
|
|
1302
|
-
return {
|
|
1303
|
-
kind: 'skip'
|
|
1304
|
-
};
|
|
1305
|
-
}
|
|
1306
|
-
await commitWebSession(session);
|
|
1307
|
-
return {
|
|
1308
|
-
kind: 'session',
|
|
1309
|
-
session: true
|
|
1310
|
-
};
|
|
1311
|
-
}
|
|
1312
|
-
}, {
|
|
1313
|
-
// 4) First-party silent iframe at the PER-APEX IdP — the DURABLE
|
|
1277
|
+
// 3) First-party silent iframe at the PER-APEX IdP — the DURABLE
|
|
1314
1278
|
// cross-domain reload-restore path. The durable session lives as a
|
|
1315
1279
|
// first-party `fedcm_session` cookie on `auth.<rp-apex>` (e.g.
|
|
1316
1280
|
// `auth.mention.earth`), established during the `/sso` bounce's
|
|
@@ -1357,39 +1321,7 @@ const OxyProvider = ({
|
|
|
1357
1321
|
};
|
|
1358
1322
|
}
|
|
1359
1323
|
}, {
|
|
1360
|
-
//
|
|
1361
|
-
// httpOnly `oxy_rt_${n}` cookies ride along and resurrect every
|
|
1362
|
-
// device-local slot. On a cross-domain RP (mention.earth, …) the
|
|
1363
|
-
// cookie is `Domain=oxy.so` so it never reaches `api.<apex>` —
|
|
1364
|
-
// `refreshAllSessions` returns `{accounts:[]}` and this skips. That
|
|
1365
|
-
// is correct; cross-domain restore is handled by the SSO bounce.
|
|
1366
|
-
// FIX-D: `restoreViaRefreshCookie` bounds the request with
|
|
1367
|
-
// `COOKIE_RESTORE_TIMEOUT` so a cross-domain stall cannot hang here.
|
|
1368
|
-
//
|
|
1369
|
-
// Disabled when `prioritizeMultiAccount` already ran the
|
|
1370
|
-
// `cookie-restore-active` step above (an explicit persisted slot) — that
|
|
1371
|
-
// earlier step is the identical restore, so this terminal-tier copy
|
|
1372
|
-
// would be a redundant second `refreshAllSessions`. It still runs in the
|
|
1373
|
-
// common no-persisted-slot case (e.g. first visit to a first-party app
|
|
1374
|
-
// whose central refresh cookies already exist).
|
|
1375
|
-
//
|
|
1376
|
-
// SIGNED-OUT GATE (same rationale as `cookie-restore-active`): the
|
|
1377
|
-
// primary's `oxy_rt` slot survives a deliberate sign-out (PR #455), so
|
|
1378
|
-
// this must be skipped while the signed-out flag is set or it would
|
|
1379
|
-
// silently re-restore the primary on reload.
|
|
1380
|
-
id: 'cookie-restore',
|
|
1381
|
-
enabled: () => (0, _useWebSSO.isWebBrowser)() && !prioritizeMultiAccount && !silentRestoreBlocked,
|
|
1382
|
-
run: async () => {
|
|
1383
|
-
const restored = await restoreViaRefreshCookie();
|
|
1384
|
-
return restored ? {
|
|
1385
|
-
kind: 'session',
|
|
1386
|
-
session: true
|
|
1387
|
-
} : {
|
|
1388
|
-
kind: 'skip'
|
|
1389
|
-
};
|
|
1390
|
-
}
|
|
1391
|
-
}, {
|
|
1392
|
-
// 6) SSO bounce (TERMINAL, web only, at most once). No local session
|
|
1324
|
+
// 4) SSO bounce (TERMINAL, web only, at most once). No local session
|
|
1393
1325
|
// was found by any step above. Top-level navigate to the central
|
|
1394
1326
|
// `auth.oxy.so/sso?prompt=none` so the IdP can either mint a session
|
|
1395
1327
|
// (returning an opaque code we exchange on the callback) or report
|
|
@@ -1466,9 +1398,9 @@ const OxyProvider = ({
|
|
|
1466
1398
|
}, error);
|
|
1467
1399
|
}
|
|
1468
1400
|
},
|
|
1469
|
-
// Defense-in-depth: a single step whose promise never settles
|
|
1470
|
-
//
|
|
1471
|
-
//
|
|
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`
|
|
1472
1404
|
// step so a genuine no-local-session visit still reaches the
|
|
1473
1405
|
// cross-domain `/sso` fallback; the `finally` backstop flips
|
|
1474
1406
|
// `authResolved` regardless. See `COLD_BOOT_OVERALL_DEADLINE`.
|
|
@@ -1488,6 +1420,76 @@ const OxyProvider = ({
|
|
|
1488
1420
|
method: 'restoreSessionsFromStorage'
|
|
1489
1421
|
});
|
|
1490
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
|
+
}
|
|
1491
1493
|
} catch (error) {
|
|
1492
1494
|
if (__DEV__) {
|
|
1493
1495
|
_core.logger.error('Auth init error', error instanceof Error ? error : new Error(String(error)), {
|
|
@@ -1506,7 +1508,7 @@ const OxyProvider = ({
|
|
|
1506
1508
|
// `authResolved` finally flip. Monotonic — never reverts on later restores.
|
|
1507
1509
|
markAuthResolved();
|
|
1508
1510
|
}
|
|
1509
|
-
}, [oxyServices, storage, storageKeys.priorSession,
|
|
1511
|
+
}, [oxyServices, storage, storageKeys.priorSession, restoreStoredSession, runSsoReturn, markAuthResolved, sessionClient, syncFromClient]);
|
|
1510
1512
|
(0, _react.useEffect)(() => {
|
|
1511
1513
|
if (!storage || initialized) {
|
|
1512
1514
|
return;
|
|
@@ -1621,34 +1623,37 @@ const OxyProvider = ({
|
|
|
1621
1623
|
|
|
1622
1624
|
// A committed web session re-enables automatic silent restore: clear the
|
|
1623
1625
|
// durable "deliberately signed out" flag. This funnel is reached by deliberate
|
|
1624
|
-
// sign-ins (password,
|
|
1626
|
+
// sign-ins (password, `/sso` return) AND by the silent
|
|
1625
1627
|
// cold-boot steps — but those are GATED on the flag, so when it is set they
|
|
1626
1628
|
// never run and never reach here; clearing is therefore only hit on a genuine
|
|
1627
1629
|
// (re-)sign-in or when restore was already permitted, both correct.
|
|
1628
1630
|
(0, _activeAuthuser.clearSignedOut)();
|
|
1629
1631
|
|
|
1630
|
-
// Register this
|
|
1631
|
-
//
|
|
1632
|
-
//
|
|
1633
|
-
//
|
|
1634
|
-
//
|
|
1635
|
-
//
|
|
1636
|
-
//
|
|
1637
|
-
//
|
|
1638
|
-
//
|
|
1639
|
-
// `
|
|
1640
|
-
//
|
|
1641
|
-
//
|
|
1642
|
-
// next
|
|
1643
|
-
//
|
|
1644
|
-
//
|
|
1645
|
-
//
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
(
|
|
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);
|
|
1652
1657
|
}
|
|
1653
1658
|
const clientSession = {
|
|
1654
1659
|
sessionId: session.sessionId,
|
|
@@ -1656,10 +1661,7 @@ const OxyProvider = ({
|
|
|
1656
1661
|
expiresAt: session.expiresAt || new Date(Date.now() + DEFAULT_SESSION_VALIDITY_MS).toISOString(),
|
|
1657
1662
|
lastActive: new Date().toISOString(),
|
|
1658
1663
|
userId: session.user.id?.toString() ?? '',
|
|
1659
|
-
isCurrent: true
|
|
1660
|
-
...(typeof primaryAuthuser === 'number' ? {
|
|
1661
|
-
authuser: primaryAuthuser
|
|
1662
|
-
} : null)
|
|
1664
|
+
isCurrent: true
|
|
1663
1665
|
};
|
|
1664
1666
|
updateSessions([clientSession], {
|
|
1665
1667
|
merge: true
|
|
@@ -1673,7 +1675,7 @@ const OxyProvider = ({
|
|
|
1673
1675
|
// because `onAuthStateChange` triggers a route navigation that can
|
|
1674
1676
|
// interrupt/supersede a still-pending async write. Persisting first
|
|
1675
1677
|
// guarantees the durable record lands; that is exactly what was missing
|
|
1676
|
-
// when `oxy_session_session_ids` came back empty after a successful
|
|
1678
|
+
// when `oxy_session_session_ids` came back empty after a successful SSO
|
|
1677
1679
|
// sign-in (the user appeared logged in until reload, then had no session to
|
|
1678
1680
|
// restore). `persistSessionDurably` awaits the READY storage instance rather
|
|
1679
1681
|
// than reading the possibly-null `storage` state, so a sign-in fired the
|
|
@@ -1700,14 +1702,14 @@ const OxyProvider = ({
|
|
|
1700
1702
|
fullUser = session.user;
|
|
1701
1703
|
}
|
|
1702
1704
|
loginSuccess(fullUser);
|
|
1703
|
-
// A session is now committed (
|
|
1705
|
+
// A session is now committed (per-apex iframe /
|
|
1704
1706
|
// SSO-return all funnel through here) — unblock the auth-resolution
|
|
1705
1707
|
// gate immediately, ahead of the cold-boot chain returning (idempotent).
|
|
1706
1708
|
markAuthResolvedRef.current();
|
|
1707
1709
|
onAuthStateChange?.(fullUser);
|
|
1708
|
-
}, [oxyServices, updateSessions, setActiveSessionId, loginSuccess, onAuthStateChange, persistSessionDurably]);
|
|
1710
|
+
}, [oxyServices, updateSessions, setActiveSessionId, loginSuccess, onAuthStateChange, persistSessionDurably, sessionClient, syncFromClient]);
|
|
1709
1711
|
|
|
1710
|
-
// Expose `handleWebSSOSession` to the cold-boot
|
|
1712
|
+
// Expose `handleWebSSOSession` to the cold-boot iframe/SSO steps,
|
|
1711
1713
|
// which reference it through a ref because they are declared above this
|
|
1712
1714
|
// callback. Assigned synchronously on every render so the ref is populated
|
|
1713
1715
|
// before the cold-boot effect (gated on `storage`/`initialized`) can fire.
|
|
@@ -1715,7 +1717,7 @@ const OxyProvider = ({
|
|
|
1715
1717
|
|
|
1716
1718
|
// Keyless native sign-in: username/email + password. The slimmed Accounts app
|
|
1717
1719
|
// (which no longer holds a local identity key) uses this; it commits a
|
|
1718
|
-
// successful session through the SAME `handleWebSSOSession` path
|
|
1720
|
+
// successful session through the SAME `handleWebSSOSession` path SSO
|
|
1719
1721
|
// use, so state, durable persistence, profile fetch, and `markAuthResolved`
|
|
1720
1722
|
// all run identically. The API returns either a full session OR a two-factor
|
|
1721
1723
|
// handoff (`{ twoFactorRequired, loginToken }`) — we surface the latter as a
|
|
@@ -1748,32 +1750,29 @@ const OxyProvider = ({
|
|
|
1748
1750
|
};
|
|
1749
1751
|
}, [oxyServices, handleWebSSOSession]);
|
|
1750
1752
|
|
|
1751
|
-
// Cross-domain silent SSO is
|
|
1752
|
-
//
|
|
1753
|
-
//
|
|
1754
|
-
//
|
|
1755
|
-
//
|
|
1756
|
-
//
|
|
1757
|
-
//
|
|
1758
|
-
// loosened; cold boot runs while `tokenReady` is false, so this never races
|
|
1759
|
-
// the cold-boot silent step.
|
|
1760
|
-
const shouldTryWebSSO = (0, _useWebSSO.isWebBrowser)() && tokenReady && !user && initialized;
|
|
1761
|
-
(0, _useWebSSO.useWebSSO)({
|
|
1762
|
-
oxyServices,
|
|
1763
|
-
onSessionFound: handleWebSSOSession,
|
|
1764
|
-
onError: error => {
|
|
1765
|
-
if (__DEV__) {
|
|
1766
|
-
_core.logger.debug('Web SSO check failed (non-critical)', {
|
|
1767
|
-
component: 'OxyContext'
|
|
1768
|
-
}, error);
|
|
1769
|
-
}
|
|
1770
|
-
},
|
|
1771
|
-
enabled: shouldTryWebSSO
|
|
1772
|
-
});
|
|
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).
|
|
1773
1760
|
|
|
1774
1761
|
// IdP session validation via lightweight iframe check
|
|
1775
1762
|
// When user returns to tab, verify auth.oxy.so still has their session
|
|
1776
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.
|
|
1777
1776
|
const lastIdPCheckRef = (0, _react.useRef)(0);
|
|
1778
1777
|
const pendingIdPCleanupRef = (0, _react.useRef)(null);
|
|
1779
1778
|
|
|
@@ -1839,35 +1838,40 @@ const OxyProvider = ({
|
|
|
1839
1838
|
pendingIdPCleanupRef.current = null;
|
|
1840
1839
|
};
|
|
1841
1840
|
}, [user, initialized, clearSessionState, resolvedAuthWebUrl]);
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
});
|
|
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).
|
|
1850
|
+
const refreshSessionsForContext = (0, _react.useCallback)(async () => {
|
|
1851
|
+
await sessionClient.bootstrap();
|
|
1852
|
+
await syncFromClient();
|
|
1853
|
+
}, [sessionClient, syncFromClient]);
|
|
1854
|
+
|
|
1855
|
+
// Exposed `switchSession`: routes through the server-authoritative
|
|
1856
|
+
// `SessionClient` (Fase 3-B) rather than `useSessionManagement`'s own
|
|
1857
|
+
// `switchSession` (which remains, unchanged, for `useAuthOperations`'
|
|
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.
|
|
1864
1862
|
const switchSessionForContext = (0, _react.useCallback)(async sessionId => {
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1863
|
+
const targetAccountId = sessionClient.getState()?.accounts.find(account => account.sessionId === sessionId)?.accountId;
|
|
1864
|
+
if (!targetAccountId) {
|
|
1865
|
+
throw new Error(`No device account found for session "${sessionId}"`);
|
|
1866
|
+
}
|
|
1867
|
+
await sessionClient.switchAccount(targetAccountId);
|
|
1868
|
+
await syncFromClient();
|
|
1869
|
+
const activeUser = _authStore.useAuthStore.getState().user;
|
|
1870
|
+
if (!activeUser) {
|
|
1871
|
+
throw new Error('Active account profile could not be resolved after switch');
|
|
1872
|
+
}
|
|
1873
|
+
return activeUser;
|
|
1874
|
+
}, [sessionClient, syncFromClient]);
|
|
1871
1875
|
|
|
1872
1876
|
// Identity management wrappers (delegate to KeyManager)
|
|
1873
1877
|
const hasIdentity = (0, _react.useCallback)(async () => {
|
|
@@ -1924,16 +1928,41 @@ const OxyProvider = ({
|
|
|
1924
1928
|
}
|
|
1925
1929
|
}, [isAuthenticated, initialized, tokenReady, refreshAccounts]);
|
|
1926
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
|
+
const runPostAccountSwitchSideEffects = (0, _react.useCallback)(async () => {
|
|
1936
|
+
await refreshAccounts();
|
|
1937
|
+
queryClient.invalidateQueries();
|
|
1938
|
+
}, [refreshAccounts, queryClient]);
|
|
1939
|
+
|
|
1927
1940
|
// Switch the active session INTO an account from the unified graph. In the
|
|
1928
|
-
// REAL-SESSION model this is identical to switching device sign-ins:
|
|
1929
|
-
//
|
|
1930
|
-
//
|
|
1931
|
-
//
|
|
1932
|
-
//
|
|
1933
|
-
//
|
|
1934
|
-
//
|
|
1935
|
-
//
|
|
1941
|
+
// REAL-SESSION model this is identical to switching device sign-ins: the
|
|
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.
|
|
1936
1958
|
const switchToAccount = (0, _react.useCallback)(async accountId => {
|
|
1959
|
+
const deviceState = sessionClient.getState();
|
|
1960
|
+
if (deviceState?.accounts.some(account => account.accountId === accountId)) {
|
|
1961
|
+
await sessionClient.switchAccount(accountId);
|
|
1962
|
+
await syncFromClient();
|
|
1963
|
+
await runPostAccountSwitchSideEffects();
|
|
1964
|
+
return;
|
|
1965
|
+
}
|
|
1937
1966
|
const result = await oxyServices.switchToAccount(accountId);
|
|
1938
1967
|
if (!result?.user || !result?.sessionId) {
|
|
1939
1968
|
throw new Error('Account switch did not return a valid session');
|
|
@@ -1945,8 +1974,10 @@ const OxyProvider = ({
|
|
|
1945
1974
|
|
|
1946
1975
|
// `oxyServices.switchToAccount` already planted `result.accessToken` as the
|
|
1947
1976
|
// active token; mirror the minted session into the multi-account store and
|
|
1948
|
-
// mark it current
|
|
1949
|
-
//
|
|
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.
|
|
1950
1981
|
const now = new Date();
|
|
1951
1982
|
const clientSession = {
|
|
1952
1983
|
sessionId: result.sessionId,
|
|
@@ -1954,20 +1985,35 @@ const OxyProvider = ({
|
|
|
1954
1985
|
expiresAt: result.expiresAt || new Date(now.getTime() + DEFAULT_SESSION_VALIDITY_MS).toISOString(),
|
|
1955
1986
|
lastActive: now.toISOString(),
|
|
1956
1987
|
userId: result.user.id?.toString() ?? '',
|
|
1957
|
-
isCurrent: true
|
|
1958
|
-
...(typeof result.authuser === 'number' ? {
|
|
1959
|
-
authuser: result.authuser
|
|
1960
|
-
} : null)
|
|
1988
|
+
isCurrent: true
|
|
1961
1989
|
};
|
|
1962
1990
|
updateSessions([clientSession], {
|
|
1963
1991
|
merge: true
|
|
1964
1992
|
});
|
|
1965
1993
|
setActiveSessionId(result.sessionId);
|
|
1966
|
-
if ((0, _useWebSSO.isWebBrowser)() && typeof result.authuser === 'number') {
|
|
1967
|
-
(0, _activeAuthuser.writeActiveAuthuser)(result.authuser);
|
|
1968
|
-
}
|
|
1969
1994
|
await persistSessionDurably(result.sessionId);
|
|
1970
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
|
+
|
|
1971
2017
|
// Fetch the canonical User for the new account (the switch result carries
|
|
1972
2018
|
// only MinimalUserData); fall back to that minimal shape if the profile
|
|
1973
2019
|
// fetch fails so the app still reflects the switched identity.
|
|
@@ -1985,14 +2031,8 @@ const OxyProvider = ({
|
|
|
1985
2031
|
}
|
|
1986
2032
|
loginSuccess(fullUser);
|
|
1987
2033
|
onAuthStateChange?.(fullUser);
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
// differ) and invalidate every query so all data refetches as the new
|
|
1991
|
-
// account — the deviceAccounts probe re-enumerates the multi-account set so
|
|
1992
|
-
// the switched account now appears as a device session.
|
|
1993
|
-
await refreshAccounts();
|
|
1994
|
-
queryClient.invalidateQueries();
|
|
1995
|
-
}, [oxyServices, updateSessions, setActiveSessionId, persistSessionDurably, loginSuccess, onAuthStateChange, refreshAccounts, queryClient]);
|
|
2034
|
+
await runPostAccountSwitchSideEffects();
|
|
2035
|
+
}, [oxyServices, updateSessions, setActiveSessionId, persistSessionDurably, sessionClient, syncFromClient, loginSuccess, onAuthStateChange, runPostAccountSwitchSideEffects]);
|
|
1996
2036
|
const createAccountFn = (0, _react.useCallback)(async data => {
|
|
1997
2037
|
const account = await oxyServices.createAccount(data);
|
|
1998
2038
|
await refreshAccounts();
|
|
@@ -2026,7 +2066,7 @@ const OxyProvider = ({
|
|
|
2026
2066
|
logoutAll,
|
|
2027
2067
|
switchSession: switchSessionForContext,
|
|
2028
2068
|
removeSession: logout,
|
|
2029
|
-
refreshSessions:
|
|
2069
|
+
refreshSessions: refreshSessionsForContext,
|
|
2030
2070
|
setLanguage,
|
|
2031
2071
|
getDeviceSessions,
|
|
2032
2072
|
logoutAllDeviceSessions,
|
|
@@ -2043,7 +2083,7 @@ const OxyProvider = ({
|
|
|
2043
2083
|
switchToAccount,
|
|
2044
2084
|
refreshAccounts,
|
|
2045
2085
|
createAccount: createAccountFn
|
|
2046
|
-
}), [activeSessionId, signIn, signInWithPassword, handleWebSSOSession, currentLanguage, currentLanguageMetadata, currentLanguageName, currentNativeLanguageName, error, getDeviceSessions, hasAccessToken, canUsePrivateApi, isPrivateApiPending, getPublicKey, hasIdentity, isAuthenticated, isLoading, logout, logoutAll, logoutAllDeviceSessions, oxyServices, storageKeyPrefix, clientId,
|
|
2086
|
+
}), [activeSessionId, signIn, signInWithPassword, handleWebSSOSession, currentLanguage, currentLanguageMetadata, currentLanguageName, currentNativeLanguageName, error, getDeviceSessions, hasAccessToken, canUsePrivateApi, isPrivateApiPending, getPublicKey, hasIdentity, isAuthenticated, isLoading, logout, logoutAll, logoutAllDeviceSessions, oxyServices, storageKeyPrefix, clientId, refreshSessionsForContext, sessions, setLanguage, storage, switchSessionForContext, tokenReady, hasAccessToken, canUsePrivateApi, isPrivateApiPending, authResolved, updateDeviceName, clearAllAccountData, useFollowHook, user, showBottomSheetForContext, openAvatarPicker, accounts, switchToAccount, refreshAccounts, createAccountFn]);
|
|
2047
2087
|
return /*#__PURE__*/(0, _jsxRuntime.jsx)(OxyContext.Provider, {
|
|
2048
2088
|
value: contextValue,
|
|
2049
2089
|
children: ssoCallbackIntercepting ? null : children
|