@oxyhq/services 13.3.0 → 14.0.1

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.
Files changed (245) hide show
  1. package/lib/commonjs/index.js +11 -16
  2. package/lib/commonjs/index.js.map +1 -1
  3. package/lib/commonjs/ui/components/AccountMenu.js +103 -65
  4. package/lib/commonjs/ui/components/AccountMenu.js.map +1 -1
  5. package/lib/commonjs/ui/components/AccountSwitcher.js +84 -126
  6. package/lib/commonjs/ui/components/AccountSwitcher.js.map +1 -1
  7. package/lib/commonjs/ui/components/ProfileMenu.js +140 -153
  8. package/lib/commonjs/ui/components/ProfileMenu.js.map +1 -1
  9. package/lib/commonjs/ui/components/SignInModal.js +4 -4
  10. package/lib/commonjs/ui/components/SignInModal.js.map +1 -1
  11. package/lib/commonjs/ui/context/OxyContext.js +506 -466
  12. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  13. package/lib/commonjs/ui/context/hooks/useAuthOperations.js +59 -57
  14. package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
  15. package/lib/commonjs/ui/context/inSessionTokenRefresh.js +23 -42
  16. package/lib/commonjs/ui/context/inSessionTokenRefresh.js.map +1 -1
  17. package/lib/commonjs/ui/context/silentSessionRestore.js +12 -21
  18. package/lib/commonjs/ui/context/silentSessionRestore.js.map +1 -1
  19. package/lib/commonjs/ui/hooks/useAuth.js +4 -3
  20. package/lib/commonjs/ui/hooks/useAuth.js.map +1 -1
  21. package/lib/commonjs/ui/hooks/useOxyAuthSession.js +12 -7
  22. package/lib/commonjs/ui/hooks/useOxyAuthSession.js.map +1 -1
  23. package/lib/commonjs/ui/hooks/useSessionManagement.js +17 -65
  24. package/lib/commonjs/ui/hooks/useSessionManagement.js.map +1 -1
  25. package/lib/commonjs/ui/hooks/useSwitchableAccounts.js +288 -0
  26. package/lib/commonjs/ui/hooks/useSwitchableAccounts.js.map +1 -0
  27. package/lib/commonjs/ui/hooks/useWebSSO.js +12 -191
  28. package/lib/commonjs/ui/hooks/useWebSSO.js.map +1 -1
  29. package/lib/commonjs/ui/screens/OxyAuthScreen.js +2 -2
  30. package/lib/commonjs/ui/screens/OxyAuthScreen.js.map +1 -1
  31. package/lib/commonjs/ui/session/createSessionClient.js +28 -0
  32. package/lib/commonjs/ui/session/createSessionClient.js.map +1 -0
  33. package/lib/commonjs/ui/session/index.js +52 -0
  34. package/lib/commonjs/ui/session/index.js.map +1 -0
  35. package/lib/commonjs/ui/session/projectSessionState.js +86 -0
  36. package/lib/commonjs/ui/session/projectSessionState.js.map +1 -0
  37. package/lib/commonjs/ui/session/sessionClientHost.js +30 -0
  38. package/lib/commonjs/ui/session/sessionClientHost.js.map +1 -0
  39. package/lib/commonjs/ui/session/tokenTransport.js +77 -0
  40. package/lib/commonjs/ui/session/tokenTransport.js.map +1 -0
  41. package/lib/commonjs/ui/utils/activeAuthuser.js +35 -79
  42. package/lib/commonjs/ui/utils/activeAuthuser.js.map +1 -1
  43. package/lib/commonjs/ui/utils/sessionHelpers.js +1 -51
  44. package/lib/commonjs/ui/utils/sessionHelpers.js.map +1 -1
  45. package/lib/commonjs/utils/deviceFlowSignIn.js +70 -15
  46. package/lib/commonjs/utils/deviceFlowSignIn.js.map +1 -1
  47. package/lib/module/index.js +5 -4
  48. package/lib/module/index.js.map +1 -1
  49. package/lib/module/ui/components/AccountMenu.js +103 -65
  50. package/lib/module/ui/components/AccountMenu.js.map +1 -1
  51. package/lib/module/ui/components/AccountSwitcher.js +85 -127
  52. package/lib/module/ui/components/AccountSwitcher.js.map +1 -1
  53. package/lib/module/ui/components/ProfileMenu.js +143 -156
  54. package/lib/module/ui/components/ProfileMenu.js.map +1 -1
  55. package/lib/module/ui/components/SignInModal.js +4 -4
  56. package/lib/module/ui/components/SignInModal.js.map +1 -1
  57. package/lib/module/ui/context/OxyContext.js +510 -470
  58. package/lib/module/ui/context/OxyContext.js.map +1 -1
  59. package/lib/module/ui/context/hooks/useAuthOperations.js +61 -60
  60. package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
  61. package/lib/module/ui/context/inSessionTokenRefresh.js +24 -43
  62. package/lib/module/ui/context/inSessionTokenRefresh.js.map +1 -1
  63. package/lib/module/ui/context/silentSessionRestore.js +12 -20
  64. package/lib/module/ui/context/silentSessionRestore.js.map +1 -1
  65. package/lib/module/ui/hooks/useAuth.js +4 -3
  66. package/lib/module/ui/hooks/useAuth.js.map +1 -1
  67. package/lib/module/ui/hooks/useOxyAuthSession.js +12 -7
  68. package/lib/module/ui/hooks/useOxyAuthSession.js.map +1 -1
  69. package/lib/module/ui/hooks/useSessionManagement.js +18 -66
  70. package/lib/module/ui/hooks/useSessionManagement.js.map +1 -1
  71. package/lib/module/ui/hooks/useSwitchableAccounts.js +283 -0
  72. package/lib/module/ui/hooks/useSwitchableAccounts.js.map +1 -0
  73. package/lib/module/ui/hooks/useWebSSO.js +13 -192
  74. package/lib/module/ui/hooks/useWebSSO.js.map +1 -1
  75. package/lib/module/ui/screens/OxyAuthScreen.js +2 -2
  76. package/lib/module/ui/screens/OxyAuthScreen.js.map +1 -1
  77. package/lib/module/ui/session/createSessionClient.js +25 -0
  78. package/lib/module/ui/session/createSessionClient.js.map +1 -0
  79. package/lib/module/ui/session/index.js +17 -0
  80. package/lib/module/ui/session/index.js.map +1 -0
  81. package/lib/module/ui/session/projectSessionState.js +79 -0
  82. package/lib/module/ui/session/projectSessionState.js.map +1 -0
  83. package/lib/module/ui/session/sessionClientHost.js +26 -0
  84. package/lib/module/ui/session/sessionClientHost.js.map +1 -0
  85. package/lib/module/ui/session/tokenTransport.js +74 -0
  86. package/lib/module/ui/session/tokenTransport.js.map +1 -0
  87. package/lib/module/ui/utils/activeAuthuser.js +34 -76
  88. package/lib/module/ui/utils/activeAuthuser.js.map +1 -1
  89. package/lib/module/ui/utils/sessionHelpers.js +0 -49
  90. package/lib/module/ui/utils/sessionHelpers.js.map +1 -1
  91. package/lib/module/utils/deviceFlowSignIn.js +71 -16
  92. package/lib/module/utils/deviceFlowSignIn.js.map +1 -1
  93. package/lib/typescript/commonjs/index.d.ts +2 -3
  94. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  95. package/lib/typescript/commonjs/ui/components/AccountMenu.d.ts +4 -2
  96. package/lib/typescript/commonjs/ui/components/AccountMenu.d.ts.map +1 -1
  97. package/lib/typescript/commonjs/ui/components/AccountSwitcher.d.ts +7 -8
  98. package/lib/typescript/commonjs/ui/components/AccountSwitcher.d.ts.map +1 -1
  99. package/lib/typescript/commonjs/ui/components/ProfileMenu.d.ts +12 -9
  100. package/lib/typescript/commonjs/ui/components/ProfileMenu.d.ts.map +1 -1
  101. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts +16 -13
  102. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
  103. package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts +27 -3
  104. package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  105. package/lib/typescript/commonjs/ui/context/inSessionTokenRefresh.d.ts +22 -17
  106. package/lib/typescript/commonjs/ui/context/inSessionTokenRefresh.d.ts.map +1 -1
  107. package/lib/typescript/commonjs/ui/context/silentSessionRestore.d.ts +12 -18
  108. package/lib/typescript/commonjs/ui/context/silentSessionRestore.d.ts.map +1 -1
  109. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts +1 -0
  110. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  111. package/lib/typescript/commonjs/ui/hooks/useAuth.d.ts +1 -1
  112. package/lib/typescript/commonjs/ui/hooks/useAuth.d.ts.map +1 -1
  113. package/lib/typescript/commonjs/ui/hooks/useOxyAuthSession.d.ts +2 -2
  114. package/lib/typescript/commonjs/ui/hooks/useOxyAuthSession.d.ts.map +1 -1
  115. package/lib/typescript/commonjs/ui/hooks/useSessionManagement.d.ts.map +1 -1
  116. package/lib/typescript/commonjs/ui/hooks/useSwitchableAccounts.d.ts +162 -0
  117. package/lib/typescript/commonjs/ui/hooks/useSwitchableAccounts.d.ts.map +1 -0
  118. package/lib/typescript/commonjs/ui/hooks/useWebSSO.d.ts +13 -52
  119. package/lib/typescript/commonjs/ui/hooks/useWebSSO.d.ts.map +1 -1
  120. package/lib/typescript/commonjs/ui/session/createSessionClient.d.ts +15 -0
  121. package/lib/typescript/commonjs/ui/session/createSessionClient.d.ts.map +1 -0
  122. package/lib/typescript/commonjs/ui/session/index.d.ts +15 -0
  123. package/lib/typescript/commonjs/ui/session/index.d.ts.map +1 -0
  124. package/lib/typescript/commonjs/ui/session/projectSessionState.d.ts +45 -0
  125. package/lib/typescript/commonjs/ui/session/projectSessionState.d.ts.map +1 -0
  126. package/lib/typescript/commonjs/ui/session/sessionClientHost.d.ts +14 -0
  127. package/lib/typescript/commonjs/ui/session/sessionClientHost.d.ts.map +1 -0
  128. package/lib/typescript/commonjs/ui/session/tokenTransport.d.ts +24 -0
  129. package/lib/typescript/commonjs/ui/session/tokenTransport.d.ts.map +1 -0
  130. package/lib/typescript/commonjs/ui/utils/activeAuthuser.d.ts +21 -29
  131. package/lib/typescript/commonjs/ui/utils/activeAuthuser.d.ts.map +1 -1
  132. package/lib/typescript/commonjs/ui/utils/sessionHelpers.d.ts +0 -19
  133. package/lib/typescript/commonjs/ui/utils/sessionHelpers.d.ts.map +1 -1
  134. package/lib/typescript/commonjs/utils/deviceFlowSignIn.d.ts +51 -19
  135. package/lib/typescript/commonjs/utils/deviceFlowSignIn.d.ts.map +1 -1
  136. package/lib/typescript/module/index.d.ts +2 -3
  137. package/lib/typescript/module/index.d.ts.map +1 -1
  138. package/lib/typescript/module/ui/components/AccountMenu.d.ts +4 -2
  139. package/lib/typescript/module/ui/components/AccountMenu.d.ts.map +1 -1
  140. package/lib/typescript/module/ui/components/AccountSwitcher.d.ts +7 -8
  141. package/lib/typescript/module/ui/components/AccountSwitcher.d.ts.map +1 -1
  142. package/lib/typescript/module/ui/components/ProfileMenu.d.ts +12 -9
  143. package/lib/typescript/module/ui/components/ProfileMenu.d.ts.map +1 -1
  144. package/lib/typescript/module/ui/context/OxyContext.d.ts +16 -13
  145. package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
  146. package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts +27 -3
  147. package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  148. package/lib/typescript/module/ui/context/inSessionTokenRefresh.d.ts +22 -17
  149. package/lib/typescript/module/ui/context/inSessionTokenRefresh.d.ts.map +1 -1
  150. package/lib/typescript/module/ui/context/silentSessionRestore.d.ts +12 -18
  151. package/lib/typescript/module/ui/context/silentSessionRestore.d.ts.map +1 -1
  152. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts +1 -0
  153. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  154. package/lib/typescript/module/ui/hooks/useAuth.d.ts +1 -1
  155. package/lib/typescript/module/ui/hooks/useAuth.d.ts.map +1 -1
  156. package/lib/typescript/module/ui/hooks/useOxyAuthSession.d.ts +2 -2
  157. package/lib/typescript/module/ui/hooks/useOxyAuthSession.d.ts.map +1 -1
  158. package/lib/typescript/module/ui/hooks/useSessionManagement.d.ts.map +1 -1
  159. package/lib/typescript/module/ui/hooks/useSwitchableAccounts.d.ts +162 -0
  160. package/lib/typescript/module/ui/hooks/useSwitchableAccounts.d.ts.map +1 -0
  161. package/lib/typescript/module/ui/hooks/useWebSSO.d.ts +13 -52
  162. package/lib/typescript/module/ui/hooks/useWebSSO.d.ts.map +1 -1
  163. package/lib/typescript/module/ui/session/createSessionClient.d.ts +15 -0
  164. package/lib/typescript/module/ui/session/createSessionClient.d.ts.map +1 -0
  165. package/lib/typescript/module/ui/session/index.d.ts +15 -0
  166. package/lib/typescript/module/ui/session/index.d.ts.map +1 -0
  167. package/lib/typescript/module/ui/session/projectSessionState.d.ts +45 -0
  168. package/lib/typescript/module/ui/session/projectSessionState.d.ts.map +1 -0
  169. package/lib/typescript/module/ui/session/sessionClientHost.d.ts +14 -0
  170. package/lib/typescript/module/ui/session/sessionClientHost.d.ts.map +1 -0
  171. package/lib/typescript/module/ui/session/tokenTransport.d.ts +24 -0
  172. package/lib/typescript/module/ui/session/tokenTransport.d.ts.map +1 -0
  173. package/lib/typescript/module/ui/utils/activeAuthuser.d.ts +21 -29
  174. package/lib/typescript/module/ui/utils/activeAuthuser.d.ts.map +1 -1
  175. package/lib/typescript/module/ui/utils/sessionHelpers.d.ts +0 -19
  176. package/lib/typescript/module/ui/utils/sessionHelpers.d.ts.map +1 -1
  177. package/lib/typescript/module/utils/deviceFlowSignIn.d.ts +51 -19
  178. package/lib/typescript/module/utils/deviceFlowSignIn.d.ts.map +1 -1
  179. package/package.json +4 -4
  180. package/src/index.ts +9 -8
  181. package/src/ui/components/AccountMenu.tsx +117 -70
  182. package/src/ui/components/AccountSwitcher.tsx +88 -134
  183. package/src/ui/components/ProfileMenu.tsx +158 -162
  184. package/src/ui/components/SignInModal.tsx +5 -5
  185. package/src/ui/context/OxyContext.tsx +560 -475
  186. package/src/ui/context/hooks/useAuthOperations.ts +72 -63
  187. package/src/ui/context/inSessionTokenRefresh.ts +23 -42
  188. package/src/ui/context/silentSessionRestore.ts +12 -26
  189. package/src/ui/hooks/useAuth.ts +4 -3
  190. package/src/ui/hooks/useOxyAuthSession.ts +13 -8
  191. package/src/ui/hooks/useSessionManagement.ts +16 -87
  192. package/src/ui/hooks/useSwitchableAccounts.ts +397 -0
  193. package/src/ui/hooks/useWebSSO.ts +13 -227
  194. package/src/ui/screens/OxyAuthScreen.tsx +2 -2
  195. package/src/ui/session/__tests__/createSessionClient.test.ts +41 -0
  196. package/src/ui/session/__tests__/projectSessionState.test.ts +114 -0
  197. package/src/ui/session/__tests__/sessionClientHost.test.ts +43 -0
  198. package/src/ui/session/__tests__/tokenTransport.test.ts +142 -0
  199. package/src/ui/session/createSessionClient.ts +21 -0
  200. package/src/ui/session/index.ts +19 -0
  201. package/src/ui/session/projectSessionState.ts +85 -0
  202. package/src/ui/session/sessionClientHost.ts +27 -0
  203. package/src/ui/session/tokenTransport.ts +76 -0
  204. package/src/ui/utils/activeAuthuser.ts +34 -76
  205. package/src/ui/utils/sessionHelpers.ts +0 -66
  206. package/src/utils/__tests__/deviceFlowSignIn.test.ts +243 -34
  207. package/src/utils/deviceFlowSignIn.ts +98 -21
  208. package/lib/commonjs/ui/components/accountMenuRows.js +0 -33
  209. package/lib/commonjs/ui/components/accountMenuRows.js.map +0 -1
  210. package/lib/commonjs/ui/hooks/useDeviceAccounts.js +0 -280
  211. package/lib/commonjs/ui/hooks/useDeviceAccounts.js.map +0 -1
  212. package/lib/commonjs/ui/hooks/useSessionSocket.js +0 -203
  213. package/lib/commonjs/ui/hooks/useSessionSocket.js.map +0 -1
  214. package/lib/commonjs/utils/silentGuardKey.js +0 -54
  215. package/lib/commonjs/utils/silentGuardKey.js.map +0 -1
  216. package/lib/module/ui/components/accountMenuRows.js +0 -29
  217. package/lib/module/ui/components/accountMenuRows.js.map +0 -1
  218. package/lib/module/ui/hooks/useDeviceAccounts.js +0 -276
  219. package/lib/module/ui/hooks/useDeviceAccounts.js.map +0 -1
  220. package/lib/module/ui/hooks/useSessionSocket.js +0 -199
  221. package/lib/module/ui/hooks/useSessionSocket.js.map +0 -1
  222. package/lib/module/utils/silentGuardKey.js +0 -49
  223. package/lib/module/utils/silentGuardKey.js.map +0 -1
  224. package/lib/typescript/commonjs/ui/components/accountMenuRows.d.ts +0 -34
  225. package/lib/typescript/commonjs/ui/components/accountMenuRows.d.ts.map +0 -1
  226. package/lib/typescript/commonjs/ui/hooks/useDeviceAccounts.d.ts +0 -133
  227. package/lib/typescript/commonjs/ui/hooks/useDeviceAccounts.d.ts.map +0 -1
  228. package/lib/typescript/commonjs/ui/hooks/useSessionSocket.d.ts +0 -14
  229. package/lib/typescript/commonjs/ui/hooks/useSessionSocket.d.ts.map +0 -1
  230. package/lib/typescript/commonjs/utils/silentGuardKey.d.ts +0 -31
  231. package/lib/typescript/commonjs/utils/silentGuardKey.d.ts.map +0 -1
  232. package/lib/typescript/module/ui/components/accountMenuRows.d.ts +0 -34
  233. package/lib/typescript/module/ui/components/accountMenuRows.d.ts.map +0 -1
  234. package/lib/typescript/module/ui/hooks/useDeviceAccounts.d.ts +0 -133
  235. package/lib/typescript/module/ui/hooks/useDeviceAccounts.d.ts.map +0 -1
  236. package/lib/typescript/module/ui/hooks/useSessionSocket.d.ts +0 -14
  237. package/lib/typescript/module/ui/hooks/useSessionSocket.d.ts.map +0 -1
  238. package/lib/typescript/module/utils/silentGuardKey.d.ts +0 -31
  239. package/lib/typescript/module/utils/silentGuardKey.d.ts.map +0 -1
  240. package/src/ui/components/accountMenuRows.ts +0 -48
  241. package/src/ui/hooks/useDeviceAccounts.ts +0 -344
  242. package/src/ui/hooks/useSessionSocket.ts +0 -233
  243. package/src/ui/utils/__tests__/activeAuthuser.test.ts +0 -75
  244. package/src/utils/__tests__/silentGuardKey.test.ts +0 -82
  245. 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 FedCM / SSO sessions use), so
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 `navigator.credentials.get()` that
129
- * ignored its abort signal left the `fedcm-silent` step's promise unsettled, so
130
- * `runColdBoot` never advanced to the terminal `/sso` bounce and auth hung
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`, the refresh
135
- * cookie at `COOKIE_RESTORE_TIMEOUT`, FedCM silent at `FEDCM_SILENT_TIMEOUT`
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 FedCM sign-in the instant the screen mounts — would
412
- // read `storage === null` and SILENTLY skip writing the session, leaving the
413
- // user signed-in in-memory but with nothing to restore on reload.
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 FedCM/SSO path and the cold-boot
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
- // (FedCM / silent iframe / SSO return / password / cookie restore) funnels
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 FedCM/iframe/SSO path
718
- // `handleWebSSOSession`, the cookie-restore path, and the stored-session path)
719
- // so the common reload case unblocks the loading gate without sitting behind
720
- // the remaining (now-skipped) cold-boot steps.
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 FedCM/iframe cold-boot steps need to commit
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
- if ((0, _useWebSSO.isWebBrowser)() && !oxyServices.getAccessToken() && (storedActiveSessionId === null || storedActiveAuthuser === null)) {
861
+
862
+ // On web with no in-memory bearer, bail ONLY when there is genuinely nothing
863
+ // to restore — neither a stored active id NOR any stored session ids. A
864
+ // surviving `sessionIds` list (e.g. the active-id key was removed after a
865
+ // server-side revocation while sibling sessions remain) MUST still be
866
+ // validated over the network and have a valid session elected as active
867
+ // below. The old guard also required the deleted `activeAuthuser` marker,
868
+ // which since the cutover is never written — that made EVERY web reload
869
+ // whose local bearer had lapsed bail before any validation. Native always
870
+ // proceeds (it plants the shared KeyManager bearer next).
871
+ if ((0, _useWebSSO.isWebBrowser)() && !oxyServices.getAccessToken() && storedActiveSessionId === null && storedSessionIdsFromStorage.length === 0) {
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
- updateSessionsRef.current(validSessions.filter(session => session.sessionId !== storedActiveSessionId), {
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 error - non-critical, don't block
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 → FedCM silent
1041
- // (central) → silent iframe (per-apex, the durable reload path) → cookie
1042
- // restore → SSO bounce (terminal).
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 probes
1045
- // (`fedcm-silent`, `silent-iframe`, `cookie-restore`). On a normal reload the
1046
- // local bearer validates in one round-trip and wins, so `runColdBoot`
1047
- // short-circuits and never sits through those probes' timeouts (the prior
1048
- // serial sum was a ~20-30s stall). `sso-return` MUST stay first it consumes
1049
- // the URL fragment before anything can strip it. On a
1050
- // first visit with no local session, `stored-session` skips and the
1051
- // cross-domain fallback chain (fedcm iframe cookie sso-bounce) runs
1052
- // exactly as before; the per-apex silent iframe still restores a durable
1053
- // cross-domain session on reload WITHOUT a top-level bounce, so when it wins
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 web-only
1068
- // probes (`fedcm-silent`, `silent-iframe`) AND `enabled` on `!storedSessionRestored`
1069
- // so they are explicitly skipped once a local session won. `runColdBoot`
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 those `enabled` bodies are never even reached — this flag makes the
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 probes run as before.
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
- // LAST-ACTIVE-ACCOUNT priority (web multi-account). When the user has an
1097
- // explicit persisted active slot (`oxy_active_authuser`, written on every
1098
- // device sign-in / account SWITCH / cookie restore), the multi-account
1099
- // refresh-cookie restore is the ONLY cold-boot step that honors WHICH slot
1100
- // was last active `restoreViaRefreshCookie` selects it via
1101
- // `selectActiveRefreshAccount(accounts, readActiveAuthuser())`. The
1102
- // `fedcm-silent` and per-apex `silent-iframe` steps only ever recover the
1103
- // PRIMARY central IdP session, so if either ran first they would clobber a
1104
- // switched (managed/org) account back to the primary on reload — the exact
1105
- // "switch is lost on reload" regression. So when a slot selection exists we
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. Runs on ALL platforms EXCEPT when
1164
- // the deliberately-signed-out flag is set (web only it never sets on
1165
- // native, so native still always reaches exactly this step, its ONLY
1166
- // restore path). This is also the common WEB reload winner.
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
- // SIGNED-OUT GATE: after a deliberate full sign-out the stored session
1169
- // state is cleared, so this would normally skip anyway — but the gate
1170
- // makes that authoritative rather than incidental, so no residual
1171
- // stored token can silently restore while the user is signed out.
1172
- //
1173
- // ORDERING (FIX A): this step runs BEFORE the slow web-only
1174
- // probes (`fedcm-silent`, `silent-iframe`, `cookie-restore`). On a
1175
- // normal reload the local bearer validates in one round-trip and
1176
- // wins; `runColdBoot` then short-circuits and never even evaluates
1177
- // the slow no-redirect probes that would otherwise time out (the
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
- // 2.75) LAST-ACTIVE multi-account restore (WEB, prioritized). Runs
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
- // 5) Refresh-cookie restore (first-party only). On `*.oxy.so` the
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 (the
1470
- // production FedCM-silent hang) can no longer block the chain forever.
1471
- // On expiry the runner keeps iterating to the terminal `sso-bounce`
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, restoreViaRefreshCookie, restoreStoredSession, runSsoReturn, markAuthResolved]);
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, interactive FedCM, `/sso` return) AND by the silent
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 primary session in the device's first-party multi-account
1631
- // refresh-cookie set (web only). Every web primary-session restore funnels
1632
- // through here — FedCM silent (`/fedcm/exchange`), per-apex `/auth/silent`
1633
- // iframe, central `/sso` return, and keyless password sign-in. Of those, only
1634
- // a same-apex `/fedcm/exchange` plants an `oxy_rt_<authuser>` slot as a side
1635
- // effect; the cross-origin/credential-less restores (`/sso/exchange`,
1636
- // `/auth/silent` postMessage) cannot set an `api.oxy.so` cookie at all. So
1637
- // WITHOUT this call a web primary never joins the device set: `refresh-all`
1638
- // returns zero accounts, and account-switch persistence + the
1639
- // `cookie-restore-active` cold-boot step have no foundation. Calling the shared
1640
- // `POST /auth/session` primitive here makes EVERY primary participate in the
1641
- // set, re-plants the rotated token, and records the active `authuser` so the
1642
- // next cold boot's `cookie-restore-active` reconciles the persisted active
1643
- // (possibly switched) account instead of `fedcm-silent` clobbering it back to
1644
- // the primary. Best-effort: a `null` result (native / transient failure)
1645
- // leaves the in-session token untouched.
1646
- const primaryAuthuser = await oxyServices.establishDeviceRefreshSlot();
1647
- // A non-null slot is returned ONLY on first-party web (the helper gates on
1648
- // platform + same registrable apex), so recording the active slot here is safe
1649
- // and never writes a phantom authuser for a cross-apex RP or native.
1650
- if (typeof primaryAuthuser === 'number') {
1651
- (0, _activeAuthuser.writeActiveAuthuser)(primaryAuthuser);
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 FedCM
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 (FedCM silent / per-apex iframe /
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 FedCM/iframe/SSO steps,
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 FedCM / SSO
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 now owned by the `fedcm-silent` / `silent-iframe`
1752
- // cold-boot steps above (the ordered `runColdBoot` sequence). `useWebSSO`
1753
- // remains mounted for its module-level run-once guard and its interactive
1754
- // FedCM helpers, and as a bounded post-boot safety net: it can fire at most
1755
- // once per page load (its own module guard), and only AFTER cold boot has
1756
- // finished (`tokenReady`) with no user recovered. We deliberately keep
1757
- // `shouldTryWebSSO` as `tokenReady && !user && initialized` — it is NOT
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
- const activeSession = activeSessionId ? sessions.find(session => session.sessionId === activeSessionId) : undefined;
1843
- const currentDeviceId = activeSession?.deviceId ?? null;
1844
- const userId = user?.id;
1845
- const refreshSessionsWithUser = (0, _react.useCallback)(() => refreshSessions(userId), [refreshSessions, userId]);
1846
- const handleSessionRemoved = (0, _react.useCallback)(sessionId => {
1847
- trackRemovedSession(sessionId);
1848
- }, [trackRemovedSession]);
1849
- const handleRemoteSignOut = (0, _react.useCallback)(() => {
1850
- _bloom.toast.info('You have been signed out remotely.');
1851
- logout().catch(remoteError => logger('Failed to process remote sign out', remoteError));
1852
- }, [logger, logout]);
1853
- (0, _useSessionSocket.useSessionSocket)({
1854
- userId,
1855
- activeSessionId,
1856
- currentDeviceId,
1857
- refreshSessions: refreshSessionsWithUser,
1858
- clearSessionState,
1859
- baseURL: oxyServices.getBaseURL(),
1860
- getAccessToken: () => oxyServices.getAccessToken(),
1861
- onRemoteSignOut: handleRemoteSignOut,
1862
- onSessionRemoved: handleSessionRemoved
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
- // Propagate the activated user so callers (the device-flow sign-in,
1866
- // `useSwitchSession`'s cache write, account chooser) receive it. The
1867
- // underlying session-management `switchSession` already resolves the
1868
- // `User`; the previous `Promise<void>` wrapper discarded it.
1869
- return switchSession(sessionId);
1870
- }, [switchSession]);
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: mint a
1929
- // real session for the target and make the whole app that account. The removed
1930
- // `X-Acting-As` delegation header is gone `oxyServices.switchToAccount`
1931
- // plants the minted access token (the refresh token is the server-set httpOnly
1932
- // `oxy_rt_<authuser>` cookie, so the session joins the device multi-account set
1933
- // and survives reload / `refresh-all`). We then commit the session into context
1934
- // state the SAME way sign-in / `switchSession` do, refresh the account graph,
1935
- // and invalidate every query so all data reloads as the new account.
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, recording the device `authuser` slot so web silent-switch
1949
- // and the device account chooser can address it.
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
- // Reload the switchable account graph (the new active account's relationships
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: refreshSessionsWithUser,
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, refreshSessionsWithUser, sessions, setLanguage, storage, switchSessionForContext, tokenReady, hasAccessToken, canUsePrivateApi, isPrivateApiPending, authResolved, updateDeviceName, clearAllAccountData, useFollowHook, user, showBottomSheetForContext, openAvatarPicker, accounts, switchToAccount, refreshAccounts, createAccountFn]);
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