@oxyhq/services 13.2.0 → 14.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (260) hide show
  1. package/lib/commonjs/index.js +11 -17
  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/ProfileButton.js +81 -22
  8. package/lib/commonjs/ui/components/ProfileButton.js.map +1 -1
  9. package/lib/commonjs/ui/components/ProfileMenu.js +244 -142
  10. package/lib/commonjs/ui/components/ProfileMenu.js.map +1 -1
  11. package/lib/commonjs/ui/components/SignInModal.js +4 -4
  12. package/lib/commonjs/ui/components/SignInModal.js.map +1 -1
  13. package/lib/commonjs/ui/context/OxyContext.js +506 -466
  14. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  15. package/lib/commonjs/ui/context/hooks/useAuthOperations.js +59 -57
  16. package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
  17. package/lib/commonjs/ui/context/inSessionTokenRefresh.js +23 -42
  18. package/lib/commonjs/ui/context/inSessionTokenRefresh.js.map +1 -1
  19. package/lib/commonjs/ui/context/silentSessionRestore.js +12 -21
  20. package/lib/commonjs/ui/context/silentSessionRestore.js.map +1 -1
  21. package/lib/commonjs/ui/hooks/useAuth.js +4 -3
  22. package/lib/commonjs/ui/hooks/useAuth.js.map +1 -1
  23. package/lib/commonjs/ui/hooks/useOxyAuthSession.js +12 -7
  24. package/lib/commonjs/ui/hooks/useOxyAuthSession.js.map +1 -1
  25. package/lib/commonjs/ui/hooks/useSessionManagement.js +17 -65
  26. package/lib/commonjs/ui/hooks/useSessionManagement.js.map +1 -1
  27. package/lib/commonjs/ui/hooks/useSwitchableAccounts.js +288 -0
  28. package/lib/commonjs/ui/hooks/useSwitchableAccounts.js.map +1 -0
  29. package/lib/commonjs/ui/hooks/useWebSSO.js +12 -191
  30. package/lib/commonjs/ui/hooks/useWebSSO.js.map +1 -1
  31. package/lib/commonjs/ui/index.js.map +1 -1
  32. package/lib/commonjs/ui/screens/OxyAuthScreen.js +2 -2
  33. package/lib/commonjs/ui/screens/OxyAuthScreen.js.map +1 -1
  34. package/lib/commonjs/ui/session/createSessionClient.js +28 -0
  35. package/lib/commonjs/ui/session/createSessionClient.js.map +1 -0
  36. package/lib/commonjs/ui/session/index.js +52 -0
  37. package/lib/commonjs/ui/session/index.js.map +1 -0
  38. package/lib/commonjs/ui/session/projectSessionState.js +86 -0
  39. package/lib/commonjs/ui/session/projectSessionState.js.map +1 -0
  40. package/lib/commonjs/ui/session/sessionClientHost.js +30 -0
  41. package/lib/commonjs/ui/session/sessionClientHost.js.map +1 -0
  42. package/lib/commonjs/ui/session/tokenTransport.js +77 -0
  43. package/lib/commonjs/ui/session/tokenTransport.js.map +1 -0
  44. package/lib/commonjs/ui/utils/activeAuthuser.js +35 -79
  45. package/lib/commonjs/ui/utils/activeAuthuser.js.map +1 -1
  46. package/lib/commonjs/ui/utils/sessionHelpers.js +1 -51
  47. package/lib/commonjs/ui/utils/sessionHelpers.js.map +1 -1
  48. package/lib/commonjs/utils/deviceFlowSignIn.js +70 -15
  49. package/lib/commonjs/utils/deviceFlowSignIn.js.map +1 -1
  50. package/lib/module/index.js +5 -5
  51. package/lib/module/index.js.map +1 -1
  52. package/lib/module/ui/components/AccountMenu.js +103 -65
  53. package/lib/module/ui/components/AccountMenu.js.map +1 -1
  54. package/lib/module/ui/components/AccountSwitcher.js +85 -127
  55. package/lib/module/ui/components/AccountSwitcher.js.map +1 -1
  56. package/lib/module/ui/components/ProfileButton.js +82 -23
  57. package/lib/module/ui/components/ProfileButton.js.map +1 -1
  58. package/lib/module/ui/components/ProfileMenu.js +247 -145
  59. package/lib/module/ui/components/ProfileMenu.js.map +1 -1
  60. package/lib/module/ui/components/SignInModal.js +4 -4
  61. package/lib/module/ui/components/SignInModal.js.map +1 -1
  62. package/lib/module/ui/context/OxyContext.js +510 -470
  63. package/lib/module/ui/context/OxyContext.js.map +1 -1
  64. package/lib/module/ui/context/hooks/useAuthOperations.js +61 -60
  65. package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
  66. package/lib/module/ui/context/inSessionTokenRefresh.js +24 -43
  67. package/lib/module/ui/context/inSessionTokenRefresh.js.map +1 -1
  68. package/lib/module/ui/context/silentSessionRestore.js +12 -20
  69. package/lib/module/ui/context/silentSessionRestore.js.map +1 -1
  70. package/lib/module/ui/hooks/useAuth.js +4 -3
  71. package/lib/module/ui/hooks/useAuth.js.map +1 -1
  72. package/lib/module/ui/hooks/useOxyAuthSession.js +12 -7
  73. package/lib/module/ui/hooks/useOxyAuthSession.js.map +1 -1
  74. package/lib/module/ui/hooks/useSessionManagement.js +18 -66
  75. package/lib/module/ui/hooks/useSessionManagement.js.map +1 -1
  76. package/lib/module/ui/hooks/useSwitchableAccounts.js +283 -0
  77. package/lib/module/ui/hooks/useSwitchableAccounts.js.map +1 -0
  78. package/lib/module/ui/hooks/useWebSSO.js +13 -192
  79. package/lib/module/ui/hooks/useWebSSO.js.map +1 -1
  80. package/lib/module/ui/index.js +0 -1
  81. package/lib/module/ui/index.js.map +1 -1
  82. package/lib/module/ui/screens/OxyAuthScreen.js +2 -2
  83. package/lib/module/ui/screens/OxyAuthScreen.js.map +1 -1
  84. package/lib/module/ui/session/createSessionClient.js +25 -0
  85. package/lib/module/ui/session/createSessionClient.js.map +1 -0
  86. package/lib/module/ui/session/index.js +17 -0
  87. package/lib/module/ui/session/index.js.map +1 -0
  88. package/lib/module/ui/session/projectSessionState.js +79 -0
  89. package/lib/module/ui/session/projectSessionState.js.map +1 -0
  90. package/lib/module/ui/session/sessionClientHost.js +26 -0
  91. package/lib/module/ui/session/sessionClientHost.js.map +1 -0
  92. package/lib/module/ui/session/tokenTransport.js +74 -0
  93. package/lib/module/ui/session/tokenTransport.js.map +1 -0
  94. package/lib/module/ui/utils/activeAuthuser.js +34 -76
  95. package/lib/module/ui/utils/activeAuthuser.js.map +1 -1
  96. package/lib/module/ui/utils/sessionHelpers.js +0 -49
  97. package/lib/module/ui/utils/sessionHelpers.js.map +1 -1
  98. package/lib/module/utils/deviceFlowSignIn.js +71 -16
  99. package/lib/module/utils/deviceFlowSignIn.js.map +1 -1
  100. package/lib/typescript/commonjs/index.d.ts +2 -3
  101. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  102. package/lib/typescript/commonjs/ui/components/AccountMenu.d.ts +4 -2
  103. package/lib/typescript/commonjs/ui/components/AccountMenu.d.ts.map +1 -1
  104. package/lib/typescript/commonjs/ui/components/AccountSwitcher.d.ts +7 -8
  105. package/lib/typescript/commonjs/ui/components/AccountSwitcher.d.ts.map +1 -1
  106. package/lib/typescript/commonjs/ui/components/ProfileButton.d.ts +11 -1
  107. package/lib/typescript/commonjs/ui/components/ProfileButton.d.ts.map +1 -1
  108. package/lib/typescript/commonjs/ui/components/ProfileMenu.d.ts +12 -9
  109. package/lib/typescript/commonjs/ui/components/ProfileMenu.d.ts.map +1 -1
  110. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts +16 -13
  111. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
  112. package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts +27 -3
  113. package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  114. package/lib/typescript/commonjs/ui/context/inSessionTokenRefresh.d.ts +22 -17
  115. package/lib/typescript/commonjs/ui/context/inSessionTokenRefresh.d.ts.map +1 -1
  116. package/lib/typescript/commonjs/ui/context/silentSessionRestore.d.ts +12 -18
  117. package/lib/typescript/commonjs/ui/context/silentSessionRestore.d.ts.map +1 -1
  118. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts +1 -0
  119. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  120. package/lib/typescript/commonjs/ui/hooks/useAuth.d.ts +1 -1
  121. package/lib/typescript/commonjs/ui/hooks/useAuth.d.ts.map +1 -1
  122. package/lib/typescript/commonjs/ui/hooks/useOxyAuthSession.d.ts +2 -2
  123. package/lib/typescript/commonjs/ui/hooks/useOxyAuthSession.d.ts.map +1 -1
  124. package/lib/typescript/commonjs/ui/hooks/useSessionManagement.d.ts.map +1 -1
  125. package/lib/typescript/commonjs/ui/hooks/useSwitchableAccounts.d.ts +162 -0
  126. package/lib/typescript/commonjs/ui/hooks/useSwitchableAccounts.d.ts.map +1 -0
  127. package/lib/typescript/commonjs/ui/hooks/useWebSSO.d.ts +13 -52
  128. package/lib/typescript/commonjs/ui/hooks/useWebSSO.d.ts.map +1 -1
  129. package/lib/typescript/commonjs/ui/index.d.ts.map +1 -1
  130. package/lib/typescript/commonjs/ui/session/createSessionClient.d.ts +15 -0
  131. package/lib/typescript/commonjs/ui/session/createSessionClient.d.ts.map +1 -0
  132. package/lib/typescript/commonjs/ui/session/index.d.ts +15 -0
  133. package/lib/typescript/commonjs/ui/session/index.d.ts.map +1 -0
  134. package/lib/typescript/commonjs/ui/session/projectSessionState.d.ts +45 -0
  135. package/lib/typescript/commonjs/ui/session/projectSessionState.d.ts.map +1 -0
  136. package/lib/typescript/commonjs/ui/session/sessionClientHost.d.ts +14 -0
  137. package/lib/typescript/commonjs/ui/session/sessionClientHost.d.ts.map +1 -0
  138. package/lib/typescript/commonjs/ui/session/tokenTransport.d.ts +24 -0
  139. package/lib/typescript/commonjs/ui/session/tokenTransport.d.ts.map +1 -0
  140. package/lib/typescript/commonjs/ui/utils/activeAuthuser.d.ts +21 -29
  141. package/lib/typescript/commonjs/ui/utils/activeAuthuser.d.ts.map +1 -1
  142. package/lib/typescript/commonjs/ui/utils/sessionHelpers.d.ts +0 -19
  143. package/lib/typescript/commonjs/ui/utils/sessionHelpers.d.ts.map +1 -1
  144. package/lib/typescript/commonjs/utils/deviceFlowSignIn.d.ts +51 -19
  145. package/lib/typescript/commonjs/utils/deviceFlowSignIn.d.ts.map +1 -1
  146. package/lib/typescript/module/index.d.ts +2 -3
  147. package/lib/typescript/module/index.d.ts.map +1 -1
  148. package/lib/typescript/module/ui/components/AccountMenu.d.ts +4 -2
  149. package/lib/typescript/module/ui/components/AccountMenu.d.ts.map +1 -1
  150. package/lib/typescript/module/ui/components/AccountSwitcher.d.ts +7 -8
  151. package/lib/typescript/module/ui/components/AccountSwitcher.d.ts.map +1 -1
  152. package/lib/typescript/module/ui/components/ProfileButton.d.ts +11 -1
  153. package/lib/typescript/module/ui/components/ProfileButton.d.ts.map +1 -1
  154. package/lib/typescript/module/ui/components/ProfileMenu.d.ts +12 -9
  155. package/lib/typescript/module/ui/components/ProfileMenu.d.ts.map +1 -1
  156. package/lib/typescript/module/ui/context/OxyContext.d.ts +16 -13
  157. package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
  158. package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts +27 -3
  159. package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  160. package/lib/typescript/module/ui/context/inSessionTokenRefresh.d.ts +22 -17
  161. package/lib/typescript/module/ui/context/inSessionTokenRefresh.d.ts.map +1 -1
  162. package/lib/typescript/module/ui/context/silentSessionRestore.d.ts +12 -18
  163. package/lib/typescript/module/ui/context/silentSessionRestore.d.ts.map +1 -1
  164. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts +1 -0
  165. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  166. package/lib/typescript/module/ui/hooks/useAuth.d.ts +1 -1
  167. package/lib/typescript/module/ui/hooks/useAuth.d.ts.map +1 -1
  168. package/lib/typescript/module/ui/hooks/useOxyAuthSession.d.ts +2 -2
  169. package/lib/typescript/module/ui/hooks/useOxyAuthSession.d.ts.map +1 -1
  170. package/lib/typescript/module/ui/hooks/useSessionManagement.d.ts.map +1 -1
  171. package/lib/typescript/module/ui/hooks/useSwitchableAccounts.d.ts +162 -0
  172. package/lib/typescript/module/ui/hooks/useSwitchableAccounts.d.ts.map +1 -0
  173. package/lib/typescript/module/ui/hooks/useWebSSO.d.ts +13 -52
  174. package/lib/typescript/module/ui/hooks/useWebSSO.d.ts.map +1 -1
  175. package/lib/typescript/module/ui/index.d.ts.map +1 -1
  176. package/lib/typescript/module/ui/session/createSessionClient.d.ts +15 -0
  177. package/lib/typescript/module/ui/session/createSessionClient.d.ts.map +1 -0
  178. package/lib/typescript/module/ui/session/index.d.ts +15 -0
  179. package/lib/typescript/module/ui/session/index.d.ts.map +1 -0
  180. package/lib/typescript/module/ui/session/projectSessionState.d.ts +45 -0
  181. package/lib/typescript/module/ui/session/projectSessionState.d.ts.map +1 -0
  182. package/lib/typescript/module/ui/session/sessionClientHost.d.ts +14 -0
  183. package/lib/typescript/module/ui/session/sessionClientHost.d.ts.map +1 -0
  184. package/lib/typescript/module/ui/session/tokenTransport.d.ts +24 -0
  185. package/lib/typescript/module/ui/session/tokenTransport.d.ts.map +1 -0
  186. package/lib/typescript/module/ui/utils/activeAuthuser.d.ts +21 -29
  187. package/lib/typescript/module/ui/utils/activeAuthuser.d.ts.map +1 -1
  188. package/lib/typescript/module/ui/utils/sessionHelpers.d.ts +0 -19
  189. package/lib/typescript/module/ui/utils/sessionHelpers.d.ts.map +1 -1
  190. package/lib/typescript/module/utils/deviceFlowSignIn.d.ts +51 -19
  191. package/lib/typescript/module/utils/deviceFlowSignIn.d.ts.map +1 -1
  192. package/package.json +4 -4
  193. package/src/index.ts +9 -9
  194. package/src/ui/components/AccountMenu.tsx +117 -70
  195. package/src/ui/components/AccountSwitcher.tsx +88 -134
  196. package/src/ui/components/ProfileButton.tsx +95 -17
  197. package/src/ui/components/ProfileMenu.tsx +263 -155
  198. package/src/ui/components/SignInModal.tsx +5 -5
  199. package/src/ui/context/OxyContext.tsx +560 -475
  200. package/src/ui/context/hooks/useAuthOperations.ts +72 -63
  201. package/src/ui/context/inSessionTokenRefresh.ts +23 -42
  202. package/src/ui/context/silentSessionRestore.ts +12 -26
  203. package/src/ui/hooks/useAuth.ts +4 -3
  204. package/src/ui/hooks/useOxyAuthSession.ts +13 -8
  205. package/src/ui/hooks/useSessionManagement.ts +16 -87
  206. package/src/ui/hooks/useSwitchableAccounts.ts +397 -0
  207. package/src/ui/hooks/useWebSSO.ts +13 -227
  208. package/src/ui/index.ts +0 -1
  209. package/src/ui/screens/OxyAuthScreen.tsx +2 -2
  210. package/src/ui/session/__tests__/createSessionClient.test.ts +41 -0
  211. package/src/ui/session/__tests__/projectSessionState.test.ts +114 -0
  212. package/src/ui/session/__tests__/sessionClientHost.test.ts +43 -0
  213. package/src/ui/session/__tests__/tokenTransport.test.ts +142 -0
  214. package/src/ui/session/createSessionClient.ts +21 -0
  215. package/src/ui/session/index.ts +19 -0
  216. package/src/ui/session/projectSessionState.ts +85 -0
  217. package/src/ui/session/sessionClientHost.ts +27 -0
  218. package/src/ui/session/tokenTransport.ts +76 -0
  219. package/src/ui/utils/activeAuthuser.ts +34 -76
  220. package/src/ui/utils/sessionHelpers.ts +0 -66
  221. package/src/utils/__tests__/deviceFlowSignIn.test.ts +243 -34
  222. package/src/utils/deviceFlowSignIn.ts +98 -21
  223. package/lib/commonjs/ui/components/accountMenuRows.js +0 -33
  224. package/lib/commonjs/ui/components/accountMenuRows.js.map +0 -1
  225. package/lib/commonjs/ui/hooks/useDeviceAccounts.js +0 -280
  226. package/lib/commonjs/ui/hooks/useDeviceAccounts.js.map +0 -1
  227. package/lib/commonjs/ui/hooks/useSessionSocket.js +0 -203
  228. package/lib/commonjs/ui/hooks/useSessionSocket.js.map +0 -1
  229. package/lib/commonjs/utils/silentGuardKey.js +0 -54
  230. package/lib/commonjs/utils/silentGuardKey.js.map +0 -1
  231. package/lib/module/ui/components/accountMenuRows.js +0 -29
  232. package/lib/module/ui/components/accountMenuRows.js.map +0 -1
  233. package/lib/module/ui/hooks/useDeviceAccounts.js +0 -276
  234. package/lib/module/ui/hooks/useDeviceAccounts.js.map +0 -1
  235. package/lib/module/ui/hooks/useSessionSocket.js +0 -199
  236. package/lib/module/ui/hooks/useSessionSocket.js.map +0 -1
  237. package/lib/module/utils/silentGuardKey.js +0 -49
  238. package/lib/module/utils/silentGuardKey.js.map +0 -1
  239. package/lib/typescript/commonjs/ui/components/accountMenuRows.d.ts +0 -34
  240. package/lib/typescript/commonjs/ui/components/accountMenuRows.d.ts.map +0 -1
  241. package/lib/typescript/commonjs/ui/hooks/useDeviceAccounts.d.ts +0 -133
  242. package/lib/typescript/commonjs/ui/hooks/useDeviceAccounts.d.ts.map +0 -1
  243. package/lib/typescript/commonjs/ui/hooks/useSessionSocket.d.ts +0 -14
  244. package/lib/typescript/commonjs/ui/hooks/useSessionSocket.d.ts.map +0 -1
  245. package/lib/typescript/commonjs/utils/silentGuardKey.d.ts +0 -31
  246. package/lib/typescript/commonjs/utils/silentGuardKey.d.ts.map +0 -1
  247. package/lib/typescript/module/ui/components/accountMenuRows.d.ts +0 -34
  248. package/lib/typescript/module/ui/components/accountMenuRows.d.ts.map +0 -1
  249. package/lib/typescript/module/ui/hooks/useDeviceAccounts.d.ts +0 -133
  250. package/lib/typescript/module/ui/hooks/useDeviceAccounts.d.ts.map +0 -1
  251. package/lib/typescript/module/ui/hooks/useSessionSocket.d.ts +0 -14
  252. package/lib/typescript/module/ui/hooks/useSessionSocket.d.ts.map +0 -1
  253. package/lib/typescript/module/utils/silentGuardKey.d.ts +0 -31
  254. package/lib/typescript/module/utils/silentGuardKey.d.ts.map +0 -1
  255. package/src/ui/components/accountMenuRows.ts +0 -48
  256. package/src/ui/hooks/useDeviceAccounts.ts +0 -344
  257. package/src/ui/hooks/useSessionSocket.ts +0 -233
  258. package/src/ui/utils/__tests__/activeAuthuser.test.ts +0 -75
  259. package/src/utils/__tests__/silentGuardKey.test.ts +0 -82
  260. package/src/utils/silentGuardKey.ts +0 -46
@@ -7,25 +7,24 @@ import { runColdBoot, resolveCentralAuthUrl, registrableApex, SSO_CALLBACK_PATH,
7
7
  import { toast } from '@oxyhq/bloom';
8
8
  import { useAuthStore } from "../stores/authStore.js";
9
9
  import { useShallow } from 'zustand/react/shallow';
10
- import { useSessionSocket } from "../hooks/useSessionSocket.js";
11
10
  import { useLanguageManagement } from "../hooks/useLanguageManagement.js";
12
11
  import { useSessionManagement } from "../hooks/useSessionManagement.js";
13
- import { useAuthOperations } from "./hooks/useAuthOperations.js";
12
+ import { useAuthOperations, clearPriorSessionHintSafe } from "./hooks/useAuthOperations.js";
14
13
  import { useDeviceManagement } from "../hooks/useDeviceManagement.js";
15
14
  import { getStorageKeys, createPlatformStorage } from "../utils/storageHelpers.js";
16
15
  import { isInvalidSessionError, isTimeoutOrNetworkError } from "../utils/errorHandlers.js";
17
- import { readActiveAuthuser, writeActiveAuthuser, clearSignedOut, isSilentRestoreSuppressed } from "../utils/activeAuthuser.js";
16
+ import { readActiveAuthuser, clearSignedOut, isSilentRestoreSuppressed, markSignedOut, clearSsoBounceState } from "../utils/activeAuthuser.js";
18
17
  import { showBottomSheet as globalShowBottomSheet } from "../navigation/bottomSheetManager.js";
19
18
  import { useQueryClient } from '@tanstack/react-query';
20
19
  import { clearQueryCache } from "../hooks/queryClient.js";
21
20
  import { useAvatarPicker } from "../hooks/useAvatarPicker.js";
22
21
  import { useAccountStore } from "../stores/accountStore.js";
23
22
  import { logger as loggerUtil } from '@oxyhq/core';
24
- import { useWebSSO, isWebBrowser } from "../hooks/useWebSSO.js";
25
- import { buildSilentGuardKey } from "../../utils/silentGuardKey.js";
23
+ import { isWebBrowser } from "../hooks/useWebSSO.js";
26
24
  import { isCrossApexWeb, CrossApexDirectSignInError } from "../../utils/crossApex.js";
27
25
  import { createInSessionRefreshHandler, startTokenRefreshScheduler } from "./inSessionTokenRefresh.js";
28
- import { mintSessionViaPerApexIframe, selectActiveRefreshAccount } from "./silentSessionRestore.js";
26
+ import { mintSessionViaPerApexIframe } from "./silentSessionRestore.js";
27
+ import { createSessionClient, deviceStateToClientSessions, activeSessionIdOf, activeUserOf, accountIdsOf } from "../session/index.js";
29
28
  import { jsx as _jsx } from "react/jsx-runtime";
30
29
  const OxyContext = /*#__PURE__*/createContext(null);
31
30
 
@@ -33,8 +32,8 @@ const OxyContext = /*#__PURE__*/createContext(null);
33
32
  * Result of {@link OxyContextState.signInWithPassword}.
34
33
  *
35
34
  * `'ok'` — the password was accepted and the resulting session has been
36
- * committed into context state (the SAME path FedCM / SSO sessions use), so
37
- * `isAuthenticated` / `user` are updated and the session is durably persisted;
35
+ * committed into context state (the SAME path SSO / silent-restore sessions
36
+ * use), so `isAuthenticated` / `user` are updated and the session is durably persisted;
38
37
  * the caller can proceed (e.g. navigate into the app).
39
38
  *
40
39
  * `'2fa_required'` — the account has two-factor auth enabled, so NO session was
@@ -46,41 +45,6 @@ const OxyContext = /*#__PURE__*/createContext(null);
46
45
  // `../utils/activeAuthuser` so the session-management and auth-operations hooks
47
46
  // can share them without re-importing this 1k-line context file.
48
47
 
49
- /**
50
- * Module-level run-once guard for the cold-boot `fedcm-silent` step.
51
- *
52
- * The FedCM silent step triggers a one-shot `navigator.credentials.get`
53
- * handshake that must fire AT MOST ONCE per page load — otherwise a provider
54
- * remount storm (route churn, StrictMode double-invoke, error-boundary
55
- * recovery) becomes a credential request storm. A per-instance ref resets on
56
- * every remount, so the guard must live at module scope. Keyed on
57
- * `origin|baseURL` so two providers pointed at the same API from the same
58
- * origin share one attempt; never cleared because only a fresh page load can
59
- * change the central IdP session state, and a fresh page load starts a fresh
60
- * module scope.
61
- *
62
- * This is a dedicated set — distinct from `useWebSSO`'s `silentSSOAttempted`
63
- * (which guards the post-boot INTERACTIVE button path) and never a core
64
- * module-level singleton (that re-evaluates under Metro web bundling and the
65
- * guard would not hold).
66
- */
67
- const servicesSilentAttempted = new Set();
68
-
69
- /**
70
- * Build the `origin|baseURL` signature used as the silent-cold-boot guard key.
71
- */
72
- function silentColdBootKey(oxyServices) {
73
- // `buildSilentGuardKey` reads `window.location.origin` behind a guard that
74
- // also verifies `window.location` exists. This is critical: it runs
75
- // UNCONDITIONALLY at the top of `restoreSessionsFromStorage` (before the
76
- // cold-boot try/catch) on EVERY platform, and React Native aliases a global
77
- // `window` with NO `window.location`. Without that guard the read threw
78
- // `Cannot read property 'origin' of undefined` on native, escaping the
79
- // restore path so `markAuthResolved` never ran and stored-session restore was
80
- // never reached.
81
- return buildSilentGuardKey(() => oxyServices.getBaseURL?.());
82
- }
83
-
84
48
  /**
85
49
  * Per-step fail-fast budget for the cold-boot silent iframe (`silentSignIn`
86
50
  * against the per-apex `/auth/silent` host).
@@ -95,18 +59,6 @@ function silentColdBootKey(oxyServices) {
95
59
  */
96
60
  const SILENT_IFRAME_TIMEOUT = 2500;
97
61
 
98
- /**
99
- * Per-step fail-fast budget for the cold-boot refresh-cookie restore
100
- * (`refreshAllSessions`).
101
- *
102
- * On a cross-domain RP the `Domain=oxy.so` refresh cookie never reaches
103
- * `api.<apex>`, so this request returns no accounts (or stalls behind a slow
104
- * endpoint) with no useful answer. As one cold-boot step it must not block the
105
- * fall-through to the terminal `/sso` bounce. 3s bounds the wait while leaving
106
- * ample headroom for a genuine first-party `*.oxy.so` rotation round-trip.
107
- */
108
- const COOKIE_RESTORE_TIMEOUT = 3000;
109
-
110
62
  /**
111
63
  * Per-step fail-fast budget (ms) for the native shared-key cold-boot step
112
64
  * (`signInWithSharedIdentity`).
@@ -124,15 +76,14 @@ const SHARED_KEY_SIGNIN_TIMEOUT = 8000;
124
76
  /**
125
77
  * HARD overall deadline (ms) for the entire cold-boot step loop —
126
78
  * defense-in-depth so a single non-settling step can NEVER hang auth resolution
127
- * forever (the production regression: a `navigator.credentials.get()` that
128
- * ignored its abort signal left the `fedcm-silent` step's promise unsettled, so
129
- * `runColdBoot` never advanced to the terminal `/sso` bounce and auth hung
130
- * indefinitely).
79
+ * forever (the production regression this guards against: a browser silent
80
+ * credential/iframe probe that ignored its own abort signal left a step's
81
+ * promise unsettled, so `runColdBoot` never advanced to the terminal `/sso`
82
+ * bounce and auth hung indefinitely).
131
83
  *
132
84
  * Every step ALREADY bounds its own network work (the stored-session bearer
133
- * validation at 8s, the silent iframe at `SILENT_IFRAME_TIMEOUT`, the refresh
134
- * cookie at `COOKIE_RESTORE_TIMEOUT`, FedCM silent at `FEDCM_SILENT_TIMEOUT`
135
- * plus its hard settle). On a healthy load the FIRST recovering step wins in a
85
+ * validation at 8s, the silent iframe at `SILENT_IFRAME_TIMEOUT` plus its hard
86
+ * settle). On a healthy load the FIRST recovering step wins in a
136
87
  * single round-trip (1–3s) and the chain short-circuits long before this fires.
137
88
  * This budget only trips when one of those per-step bounds regresses.
138
89
  *
@@ -149,6 +100,23 @@ const SHARED_KEY_SIGNIN_TIMEOUT = 8000;
149
100
  */
150
101
  const COLD_BOOT_OVERALL_DEADLINE = 20000;
151
102
 
103
+ /**
104
+ * How long cold boot WAITS for the post-ladder SessionClient handoff
105
+ * (`addCurrentAccount` + `start` + `syncFromClient`) before it resolves auth
106
+ * and stops blocking. Once a ladder step planted a token the user is already
107
+ * authenticated — the handoff only populates the multi-account set and the
108
+ * server-authoritative active account, and those also arrive via the socket
109
+ * subscription wired in `useEffect`. So on a slow/unresponsive backend we stop
110
+ * AWAITING the handoff here (it keeps running in the background and projects
111
+ * when it lands) rather than delaying `markAuthResolved` — otherwise the two
112
+ * sequential `HttpService`-bounded REST calls could push auth resolution up to
113
+ * ~10s past the ladder's own budget, reintroducing the exact spinner-stall
114
+ * `COLD_BOOT_OVERALL_DEADLINE` exists to prevent. On the normal fast path the
115
+ * handoff completes well within this, so the correct account shows with no
116
+ * flash.
117
+ */
118
+ const SESSION_HANDOFF_DEADLINE = 6000;
119
+
152
120
  /**
153
121
  * Per-session soft timeout (ms) for the parallel stored-session validation in
154
122
  * `restoreStoredSession`. Each `validateSession` call races against this timer
@@ -407,9 +375,10 @@ export const OxyProvider = ({
407
375
  // hooks below. But it is `null` for a brief window after mount while
408
376
  // `createPlatformStorage()` resolves (a microtask on web; a dynamic
409
377
  // `import()` on native). Any persistence path that fires during that window
410
- // — e.g. an interactive FedCM sign-in the instant the screen mounts — would
411
- // read `storage === null` and SILENTLY skip writing the session, leaving the
412
- // user signed-in in-memory but with nothing to restore on reload.
378
+ // — e.g. an interactive password sign-in the instant the screen mounts —
379
+ // would read `storage === null` and SILENTLY skip writing the session,
380
+ // leaving the user signed-in in-memory but with nothing to restore on
381
+ // reload.
413
382
  //
414
383
  // To make persistence robust regardless of timing we ALSO expose the storage
415
384
  // as an awaitable promise (`getReadyStorage`). Persistence code awaits the
@@ -513,10 +482,8 @@ export const OxyProvider = ({
513
482
  setActiveSessionId,
514
483
  updateSessions,
515
484
  switchSession,
516
- refreshSessions,
517
485
  clearSessionState,
518
- saveActiveSessionId,
519
- trackRemovedSession
486
+ saveActiveSessionId
520
487
  } = useSessionManagement({
521
488
  oxyServices,
522
489
  storage,
@@ -533,6 +500,139 @@ export const OxyProvider = ({
533
500
  setTokenReady,
534
501
  queryClient
535
502
  });
503
+
504
+ // Session-sync integration layer (Fase 3-A -> 3-B). Built ONCE per
505
+ // `oxyServices` instance via a lazy ref (mirrors the `oxyServicesRef`
506
+ // pattern above) so the underlying `SessionClient` — and its socket
507
+ // connection, once started — is never recreated across renders.
508
+ //
509
+ // ADDITIVE + INERT for this task: `client.start()` is NOT called here (that
510
+ // is Task 2's job, which also retires the 8-step cold boot below). Until
511
+ // then `client.getState()` never advances past `null`, so `syncFromClient`
512
+ // is a guaranteed no-op in production — the existing cold-boot-driven state
513
+ // remains the sole authority. The wiring exists so the projection can be
514
+ // exercised in isolation against a controlled client before the authority
515
+ // flip.
516
+ const sessionClientPairRef = useRef(null);
517
+ if (!sessionClientPairRef.current) {
518
+ sessionClientPairRef.current = createSessionClient(oxyServices);
519
+ }
520
+ const {
521
+ client: sessionClient,
522
+ host: sessionClientHost
523
+ } = sessionClientPairRef.current;
524
+
525
+ // Cold-boot registration dedup (Task 5). `handleWebSSOSession` (declared
526
+ // below) is the single commit funnel for every web ladder step that mints a
527
+ // session (`sso-return`, `shared-key-signin`, `silent-iframe`) — it registers
528
+ // the recovered account into the device set
529
+ // itself (`sessionClient.addCurrentAccount()`). The cold-boot post-ladder
530
+ // handoff ALSO registers, but only as a FALLBACK for the `stored-session`
531
+ // step, which commits through a completely different path
532
+ // (`switchSessionRef`) and never touches `sessionClient` on its own. This
533
+ // ref lets the handoff detect "did a ladder step already register this
534
+ // boot?" and skip a redundant second `POST /session/device/add`. Reset at
535
+ // the start of every cold-boot pass.
536
+ const registeredDuringBootRef = useRef(false);
537
+
538
+ // Projects `client.getState()` onto the exposed `sessions` / `activeSessionId`
539
+ // / `user` via the SAME setters the existing cold-boot/session-management
540
+ // paths use. This is the SOLE authority for both a locally-initiated mutation
541
+ // (switch/logout resolving here) AND a REMOTELY-pushed `session_state` (the
542
+ // `device:<deviceId>` socket owned by `client.start()`, subscribed to below) —
543
+ // there is no per-domain `useSessionSocket` anymore.
544
+ //
545
+ // GAP FIX (Task 4): when the pushed/bootstrapped state reports ZERO device
546
+ // accounts, this device has been fully signed out — either by the LOCAL
547
+ // `logout()`/`logoutAll()` mutation (which also runs its own explicit
548
+ // cleanup) or, just as importantly, by a REMOTE actor (another tab/device
549
+ // removing the last session, or an admin revoking every device account)
550
+ // pushing that same empty state over the socket. The prior per-domain
551
+ // `useSessionSocket` forced a local sign-out in this situation
552
+ // (`triggerLocalSignOut`); without an equivalent here a remote full sign-out
553
+ // would leave `user`/`isAuthenticated`/the bearer token stale until the next
554
+ // 401. Route through the SAME `clearSessionState()` a local full sign-out
555
+ // uses (authStore reset, token clear, query-cache clear, storage clear) —
556
+ // idempotent with the local-logout caller's own explicit call.
557
+ //
558
+ // DELIBERATE-SIGN-OUT GUARD (Task 5): a zero-account state — however it
559
+ // arrived — is just as terminal as a local `logout()`/`logoutAll()`, so it
560
+ // sets the SAME durable "deliberately signed out" flag (web-only,
561
+ // best-effort) that those two callers set directly. Without this, a remote
562
+ // full wipe would leave the flag unset, and the `silent-iframe` cold-boot
563
+ // step could silently re-mint a session from a still-live per-apex
564
+ // `fedcm_session` cookie right after this device was authoritatively signed
565
+ // out everywhere.
566
+ //
567
+ // REMOTE-SIGN-OUT SUPPRESSION FIX (review H1): the terminal `sso-bounce`
568
+ // cold-boot step is gated on the durable prior-session hint
569
+ // (`allowSsoBounce` = `hasPriorSession || hasLocalSession`), NOT on the
570
+ // deliberately-signed-out flag set above. Leaving that hint in place after a
571
+ // REMOTE full sign-out means the next reload still performs one terminal
572
+ // `/sso` establish bounce, which can silently re-mint a session from a
573
+ // still-live central `fedcm_session` — signing the user back in right after
574
+ // they were signed out everywhere. Clear the SAME cleanup set the LOCAL
575
+ // `logout()`/`logoutAll()` paths use (`useAuthOperations.ts`) — SSO bounce
576
+ // state + the prior-session hint — so a remote sign-out is indistinguishable
577
+ // from a local one to the next cold boot.
578
+ const syncFromClient = useCallback(async () => {
579
+ const state = sessionClient.getState();
580
+ if (state === null) {
581
+ // INERT: no session state has been bootstrapped (client.start() has not
582
+ // run). Never overwrite the existing cold-boot-driven state.
583
+ return;
584
+ }
585
+ if (state.accounts.length === 0) {
586
+ sessionClientHost.setCurrentAccountId(null);
587
+ if (isWebBrowser()) {
588
+ markSignedOut();
589
+ }
590
+ clearSsoBounceState();
591
+ clearPriorSessionHintSafe(clearPriorSessionHint, logger);
592
+ await clearSessionState();
593
+ return;
594
+ }
595
+ // LAST-WRITE-WINS GUARD (review I3): `syncFromClient` is called
596
+ // concurrently — once per socket `notify()` push AND once per direct
597
+ // mutation call — and each invocation captures `state` before an async
598
+ // profile fetch (`getUsersByIds`). `SessionClient`'s own state is
599
+ // monotonic by revision, but this projection previously was not: a
600
+ // SLOWER, OLDER fetch resolving AFTER a newer one would still apply its
601
+ // now-stale captured `state`, clobbering `user`/`sessions`/
602
+ // `activeSessionId` back to an outdated account. Capture the revision
603
+ // this fetch is FOR, then after the await, re-read the client's current
604
+ // state and bail if it has since moved past the captured revision — a
605
+ // fresher call has already applied (or will apply) the current truth.
606
+ const capturedRevision = state.revision;
607
+ const ids = accountIdsOf(state);
608
+ let users = [];
609
+ try {
610
+ users = ids.length > 0 ? await oxyServices.getUsersByIds(ids) : [];
611
+ } catch (fetchError) {
612
+ // Invoked fire-and-forget from the socket `notify()` subscription, so a
613
+ // rejected profile fetch would surface as an unhandled rejection. Log and
614
+ // bail; the next `notify()`/mutation will re-project the current truth.
615
+ logger('Failed to resolve account profiles during syncFromClient', fetchError);
616
+ return;
617
+ }
618
+ const latest = sessionClient.getState();
619
+ if (!latest || latest.revision !== capturedRevision) {
620
+ return;
621
+ }
622
+ const usersById = new Map(users.map(resolvedUser => [resolvedUser.id, resolvedUser]));
623
+ updateSessions(deviceStateToClientSessions(latest, usersById));
624
+ setActiveSessionId(activeSessionIdOf(latest));
625
+ const activeUser = activeUserOf(latest, usersById);
626
+ if (activeUser) {
627
+ loginSuccess(activeUser);
628
+ }
629
+ sessionClientHost.setCurrentAccountId(latest.activeAccountId);
630
+ }, [oxyServices, sessionClient, sessionClientHost, updateSessions, setActiveSessionId, loginSuccess, clearSessionState, clearPriorSessionHint, logger]);
631
+ useEffect(() => {
632
+ return sessionClient.subscribe(() => {
633
+ void syncFromClient();
634
+ });
635
+ }, [sessionClient, syncFromClient]);
536
636
  const {
537
637
  signIn,
538
638
  logout,
@@ -540,7 +640,6 @@ export const OxyProvider = ({
540
640
  } = useAuthOperations({
541
641
  oxyServices,
542
642
  storage,
543
- sessions,
544
643
  activeSessionId,
545
644
  setActiveSessionId,
546
645
  updateSessions,
@@ -548,6 +647,8 @@ export const OxyProvider = ({
548
647
  clearSessionState,
549
648
  clearPriorSessionHint,
550
649
  switchSession,
650
+ sessionClient,
651
+ syncFromClient,
551
652
  applyLanguagePreference,
552
653
  onAuthStateChange,
553
654
  onError,
@@ -675,7 +776,7 @@ export const OxyProvider = ({
675
776
  // init. Callers MUST invoke this BEFORE any work that can trigger a route
676
777
  // navigation (`onAuthStateChange`) — navigation can interrupt a still-pending
677
778
  // async write, which is exactly what once left `session_ids` empty after a
678
- // successful sign-in. Shared by the FedCM/SSO path and the cold-boot
779
+ // successful sign-in. Shared by the SSO/silent-restore path and the cold-boot
679
780
  // refresh-cookie restore so both land the same durable record.
680
781
  const persistSessionDurably = useCallback(async sessionId => {
681
782
  const readyStorage = await getReadyStorage();
@@ -694,7 +795,7 @@ export const OxyProvider = ({
694
795
  // A session is now durably committed — set the returning-user hint so a
695
796
  // future cold boot whose local session has lapsed still gets ONE `/sso`
696
797
  // establish bounce (see `markPriorSessionHint`). Every web commit path
697
- // (FedCM / silent iframe / SSO return / password / cookie restore) funnels
798
+ // (silent iframe / SSO return / password / cookie restore) funnels
698
799
  // through here, so this is the single chokepoint for the hint.
699
800
  await readyStorage.setItem(storageKeys.priorSession, '1');
700
801
  }, [getReadyStorage, logger, storageKeys.activeSessionId, storageKeys.sessionIds, storageKeys.priorSession]);
@@ -713,10 +814,10 @@ export const OxyProvider = ({
713
814
  // Idempotent and monotonic via `authResolvedRef`: the first call wins and the
714
815
  // setters fire at most once, so the restore `finally` backstop becomes a no-op
715
816
  // once a commit site has already marked resolution. Called from EVERY place a
716
- // user is actually committed (the FedCM/iframe/SSO path
717
- // `handleWebSSOSession`, the cookie-restore path, and the stored-session path)
718
- // so the common reload case unblocks the loading gate without sitting behind
719
- // the remaining (now-skipped) cold-boot steps.
817
+ // user is actually committed (the iframe/SSO path
818
+ // `handleWebSSOSession` and the stored-session path) so the common reload
819
+ // case unblocks the loading gate without sitting behind the remaining
820
+ // (now-skipped) cold-boot steps.
720
821
  const markAuthResolved = useCallback(() => {
721
822
  if (authResolvedRef.current) {
722
823
  return;
@@ -729,110 +830,13 @@ export const OxyProvider = ({
729
830
  markAuthResolvedRef.current = markAuthResolved;
730
831
 
731
832
  // `handleWebSSOSession` is declared further down (it depends on values that
732
- // are only available there). The FedCM/iframe cold-boot steps need to commit
833
+ // are only available there). The iframe/SSO cold-boot steps need to commit
733
834
  // a recovered session through it, so we route the call through a ref that is
734
835
  // populated once the callback exists. The ref is assigned synchronously on
735
836
  // every render before the cold-boot effect can fire (the effect is gated on
736
837
  // `storage` + `initialized`, both of which settle after first render).
737
838
  const handleWebSSOSessionRef = useRef(null);
738
839
 
739
- // Cold-boot session restore via the secure refresh cookies (web only).
740
- //
741
- // Calls `oxyServices.refreshAllSessions()` → `POST /auth/refresh-all` with
742
- // `credentials: 'include'`. The server rotates every device-local
743
- // `oxy_rt_${authuser}` cookie in parallel and returns one entry per valid
744
- // account (Google-style multi-account).
745
- //
746
- // Active-account selection: the persisted `oxy_active_authuser` slot index
747
- // wins when it matches a returned account; otherwise the lowest `authuser`
748
- // is picked. JS never sees the refresh cookies (httpOnly).
749
- //
750
- // Returns `true` when at least one session was restored (caller short-
751
- // circuits the bearer path); `false` on no signed-in accounts / any failure
752
- // (caller proceeds unauthenticated through the existing flow — nothing is
753
- // cleared).
754
- const restoreViaRefreshCookie = useCallback(async () => {
755
- if (!isWebBrowser()) {
756
- return false;
757
- }
758
- let snapshot;
759
- try {
760
- // Bound the refresh so a cross-domain/stalled call cannot hang the cold
761
- // boot in front of the terminal `/sso` bounce (see COOKIE_RESTORE_TIMEOUT).
762
- snapshot = await oxyServices.refreshAllSessions({
763
- timeout: COOKIE_RESTORE_TIMEOUT
764
- });
765
- } catch (fetchError) {
766
- // Offline / network error — fall through to the cached/stored-session flow.
767
- if (__DEV__) {
768
- loggerUtil.debug('Refresh-all cookie restore network error (expected when offline)', {
769
- component: 'OxyContext',
770
- method: 'restoreViaRefreshCookie'
771
- }, fetchError);
772
- }
773
- return false;
774
- }
775
- if (snapshot.accounts.length === 0) {
776
- return false;
777
- }
778
-
779
- // Pick the active account: persisted authuser if it still matches a returned
780
- // account, otherwise the lowest authuser (deterministic). The server has
781
- // already sorted ascending so [0] is the lowest.
782
- const activeAccount = selectActiveRefreshAccount(snapshot.accounts, readActiveAuthuser());
783
-
784
- // Plant the active access token. Sibling accounts' access tokens stay in
785
- // the snapshot (the chooser can drive a per-account refresh via
786
- // `refreshTokenViaCookie({authuser})` on switch).
787
- oxyServices.httpService.setTokens(activeAccount.accessToken);
788
-
789
- // Fetch the full user with the freshly planted token. The refresh-all
790
- // payload includes a minimal user shape (id, username, name, avatar,
791
- // email, color) — sufficient for the chooser but the auth store wants the
792
- // canonical User document for downstream rendering.
793
- let fullUser;
794
- try {
795
- fullUser = await oxyServices.getCurrentUser();
796
- } catch (userError) {
797
- // Token planted but profile fetch failed (e.g. transient network). Do
798
- // not claim a restored session; fall through so the stored-session flow
799
- // can retry. Leave the planted token in place — it is valid and harmless.
800
- if (__DEV__) {
801
- loggerUtil.debug('Refresh-all cookie restore: getCurrentUser failed', {
802
- component: 'OxyContext',
803
- method: 'restoreViaRefreshCookie'
804
- }, userError);
805
- }
806
- return false;
807
- }
808
-
809
- // Build a ClientSession per returned account so the multi-session store
810
- // reflects every device-local slot, not just the active one. The active
811
- // account is flagged `isCurrent: true`.
812
- const now = new Date();
813
- const clientSessions = snapshot.accounts.map(account => ({
814
- sessionId: account.sessionId,
815
- deviceId: '',
816
- expiresAt: account.expiresAt || new Date(now.getTime() + DEFAULT_SESSION_VALIDITY_MS).toISOString(),
817
- lastActive: now.toISOString(),
818
- userId: account.user?.id,
819
- isCurrent: account.sessionId === activeAccount.sessionId,
820
- authuser: account.authuser
821
- }));
822
- updateSessionsRef.current(clientSessions, {
823
- merge: true
824
- });
825
- setActiveSessionIdRef.current(activeAccount.sessionId);
826
- writeActiveAuthuser(activeAccount.authuser);
827
- await persistSessionDurably(activeAccount.sessionId);
828
- loginSuccessRef.current(fullUser);
829
- // A session is now committed — unblock the auth-resolution gate immediately
830
- // rather than waiting for `runColdBoot` to return (idempotent).
831
- markAuthResolvedRef.current();
832
- onAuthStateChangeRef.current?.(fullUser);
833
- return true;
834
- }, [oxyServices, persistSessionDurably]);
835
-
836
840
  // Native (and offline) stored-session restore — the ONLY restore path that
837
841
  // runs on React Native, and the web fallback when no cross-domain step won.
838
842
  //
@@ -847,8 +851,23 @@ export const OxyProvider = ({
847
851
  const storedSessionIdsJson = await storage.getItem(storageKeys.sessionIds);
848
852
  const storedSessionIdsFromStorage = storedSessionIdsJson ? JSON.parse(storedSessionIdsJson) : [];
849
853
  let storedActiveSessionId = await storage.getItem(storageKeys.activeSessionId);
854
+ // OPTIONAL HINT ONLY (no longer a gate). Nothing writes the active-`authuser`
855
+ // marker since the SessionClient cutover (`writeActiveAuthuser` was deleted),
856
+ // so it is `null` on every fresh install; when a legacy value is present it
857
+ // is used purely to backfill `clientSession.authuser` for the active session
858
+ // below. It MUST NOT decide whether restore is attempted — see the guard.
850
859
  const storedActiveAuthuser = isWebBrowser() ? readActiveAuthuser() : null;
851
- if (isWebBrowser() && !oxyServices.getAccessToken() && (storedActiveSessionId === null || storedActiveAuthuser === null)) {
860
+
861
+ // On web with no in-memory bearer, bail ONLY when there is genuinely nothing
862
+ // to restore — neither a stored active id NOR any stored session ids. A
863
+ // surviving `sessionIds` list (e.g. the active-id key was removed after a
864
+ // server-side revocation while sibling sessions remain) MUST still be
865
+ // validated over the network and have a valid session elected as active
866
+ // below. The old guard also required the deleted `activeAuthuser` marker,
867
+ // which since the cutover is never written — that made EVERY web reload
868
+ // whose local bearer had lapsed bail before any validation. Native always
869
+ // proceeds (it plants the shared KeyManager bearer next).
870
+ if (isWebBrowser() && !oxyServices.getAccessToken() && storedActiveSessionId === null && storedSessionIdsFromStorage.length === 0) {
852
871
  return false;
853
872
  }
854
873
  const nativeSharedSession = !isWebBrowser() ? await KeyManager.getSharedSession().catch(() => null) : null;
@@ -945,23 +964,59 @@ export const OxyProvider = ({
945
964
  } catch (switchError) {
946
965
  // Silently handle expected errors (invalid sessions, timeouts, network issues)
947
966
  if (isInvalidSessionError(switchError)) {
967
+ // The stored active id was rejected server-side. Clear it and drop the
968
+ // dead session from the local view, then FALL THROUGH to the election
969
+ // below to promote a surviving valid session — a stale/removed active
970
+ // marker must not strand the other still-valid accounts.
948
971
  await storage.removeItem(storageKeys.activeSessionId);
949
- updateSessionsRef.current(validSessions.filter(session => session.sessionId !== storedActiveSessionId), {
972
+ validSessions = validSessions.filter(session => session.sessionId !== storedActiveSessionId);
973
+ updateSessionsRef.current(validSessions, {
950
974
  merge: false,
951
975
  preserveSessionIds: unvalidatedSessionIds
952
976
  });
953
977
  // Don't log expected session errors during restoration
954
978
  } else if (isTimeoutOrNetworkError(switchError)) {
955
- // Timeout/network error - non-critical, don't block
979
+ // Timeout/network the stored active session may still be valid; we
980
+ // just could not confirm it. Do NOT elect a different session (that
981
+ // could switch accounts under the user); leave restore to a retry / the
982
+ // remaining cold-boot steps.
956
983
  if (__DEV__) {
957
984
  loggerUtil.debug('Active session validation timeout (expected when offline)', {
958
985
  component: 'OxyContext',
959
986
  method: 'restoreStoredSession'
960
987
  }, switchError);
961
988
  }
989
+ return false;
962
990
  } else {
963
- // Only log unexpected errors
991
+ // Only log unexpected errors, and do not elect on an ambiguous failure.
964
992
  logger('Active session validation error', switchError);
993
+ return false;
994
+ }
995
+ }
996
+ }
997
+
998
+ // Election fallback. Reached when there is no usable stored active id —
999
+ // either it was never persisted (e.g. removed after a server-side revocation
1000
+ // while sibling sessions survived) or it was just rejected as invalid above.
1001
+ // Promote the FIRST validated session to active and commit it through the
1002
+ // same `switchSession` path. The server owns which account is truly active,
1003
+ // so any valid session is a safe provisional active — the post-restore
1004
+ // SessionClient handoff reconciles the real active account immediately after.
1005
+ const electedSession = validSessions[0];
1006
+ if (electedSession) {
1007
+ try {
1008
+ await switchSessionRef.current(electedSession.sessionId);
1009
+ await storage.setItem(storageKeys.activeSessionId, electedSession.sessionId);
1010
+ await storage.setItem(storageKeys.priorSession, '1');
1011
+ markAuthResolvedRef.current();
1012
+ return true;
1013
+ } catch (electionError) {
1014
+ // Activation requires an in-memory bearer on web; without one (a plain
1015
+ // web reload) the switch throws and recovery defers to the cross-domain
1016
+ // cold-boot steps. Invalid/timeout/network are expected here — surface
1017
+ // only genuinely unexpected failures.
1018
+ if (!isInvalidSessionError(electionError) && !isTimeoutOrNetworkError(electionError)) {
1019
+ logger('Elected session activation error', electionError);
965
1020
  }
966
1021
  }
967
1022
  }
@@ -1036,21 +1091,31 @@ export const OxyProvider = ({
1036
1091
  // web-only step is gated by `isWebBrowser()`, so on native ONLY
1037
1092
  // `stored-session` runs.
1038
1093
  //
1039
- // Order (web): SSO return → stored session → FedCM silent
1040
- // (central) → silent iframe (per-apex, the durable reload path) → cookie
1041
- // restore → SSO bounce (terminal).
1094
+ // Order (web): SSO return → stored session → shared-key sign-in (native
1095
+ // only, disabled here) → silent iframe (per-apex, the durable reload path)
1096
+ // → SSO bounce (terminal). This is a pure TOKEN-ACQUISITION ladder — it
1097
+ // mints the first per-domain access token by whichever means recovers one
1098
+ // fastest. Once a token is acquired (or the ladder exhausts), the
1099
+ // SERVER-authoritative `SessionClient` takes over: `addCurrentAccount` +
1100
+ // `start` bootstrap the device session set and multi-account/active-account
1101
+ // state from `GET /session/device/state`, so WHICH account is active is
1102
+ // decided server-side, not by a client-persisted slot. There is no oxy_rt
1103
+ // refresh-cookie restore step in this ladder, and no FedCM step — FedCM was
1104
+ // removed from the client sign-in/cold-boot path entirely (Chrome-only, and
1105
+ // its fast/silent failure mode combined with a caller's auth-guard effect
1106
+ // could re-trigger sign-in in a tight loop; see `CrossDomainAuth`'s doc
1107
+ // comment in `@oxyhq/core`).
1042
1108
  //
1043
- // LATENCY (FIX A): `stored-session` runs BEFORE the slow no-redirect probes
1044
- // (`fedcm-silent`, `silent-iframe`, `cookie-restore`). On a normal reload the
1045
- // local bearer validates in one round-trip and wins, so `runColdBoot`
1046
- // short-circuits and never sits through those probes' timeouts (the prior
1047
- // serial sum was a ~20-30s stall). `sso-return` MUST stay first it consumes
1048
- // the URL fragment before anything can strip it. On a
1049
- // first visit with no local session, `stored-session` skips and the
1050
- // cross-domain fallback chain (fedcm iframe cookie sso-bounce) runs
1051
- // exactly as before; the per-apex silent iframe still restores a durable
1052
- // cross-domain session on reload WITHOUT a top-level bounce, so when it wins
1053
- // `sso-bounce` never fires (no flash, no loop).
1109
+ // LATENCY (FIX A): `stored-session` runs BEFORE the slow no-redirect
1110
+ // `silent-iframe` probe. On a normal reload the local bearer validates in
1111
+ // one round-trip and wins, so `runColdBoot` short-circuits and never sits
1112
+ // through that probe's timeout. `sso-return` MUST stay first it consumes
1113
+ // the URL fragment before anything can strip it. On a first visit with no
1114
+ // local session, `stored-session` skips and the cross-domain fallback chain
1115
+ // (silent-iframe sso-bounce) runs exactly as before; the per-apex silent
1116
+ // iframe still restores a durable cross-domain session on reload WITHOUT a
1117
+ // top-level bounce, so when it wins `sso-bounce` never fires (no flash, no
1118
+ // loop).
1054
1119
  // Order (native): stored session only (every web-only step is disabled
1055
1120
  // off-browser).
1056
1121
  const restoreSessionsFromStorage = useCallback(async () => {
@@ -1058,18 +1123,18 @@ export const OxyProvider = ({
1058
1123
  return;
1059
1124
  }
1060
1125
  setTokenReady(false);
1126
+ // Fresh per-boot flag — see the declaration comment above `sessionClient`.
1127
+ registeredDuringBootRef.current = false;
1061
1128
  const commitWebSession = handleWebSSOSessionRef.current;
1062
- const silentKey = silentColdBootKey(oxyServices);
1063
- const fedcmSupported = isWebBrowser() && oxyServices.isFedCMSupported?.() === true;
1064
1129
 
1065
1130
  // FIX-B precondition flag: set true the instant the (now-earlier)
1066
- // `stored-session` step recovers a local bearer session. The slow web-only
1067
- // probes (`fedcm-silent`, `silent-iframe`) AND `enabled` on `!storedSessionRestored`
1068
- // so they are explicitly skipped once a local session won. `runColdBoot`
1131
+ // `stored-session` step recovers a local bearer session. The slow
1132
+ // web-only `silent-iframe` probe is `enabled` on `!storedSessionRestored`
1133
+ // so it is explicitly skipped once a local session won. `runColdBoot`
1069
1134
  // already short-circuits on the first `{kind:'session'}`, so on a winning
1070
- // reload those `enabled` bodies are never even reached — this flag makes the
1135
+ // reload that `enabled` body is never even reached — this flag makes the
1071
1136
  // intent explicit and is redundant-safe. On a first-visit-no-local-session,
1072
- // `stored-session` skips, this stays false, and the probes run as before.
1137
+ // `stored-session` skips, this stays false, and the probe runs as before.
1073
1138
  let storedSessionRestored = false;
1074
1139
 
1075
1140
  // FIX-B smart-gate input: has this device/app EVER had a signed-in Oxy
@@ -1092,52 +1157,16 @@ export const OxyProvider = ({
1092
1157
  }
1093
1158
  }
1094
1159
 
1095
- // LAST-ACTIVE-ACCOUNT priority (web multi-account). When the user has an
1096
- // explicit persisted active slot (`oxy_active_authuser`, written on every
1097
- // device sign-in / account SWITCH / cookie restore), the multi-account
1098
- // refresh-cookie restore is the ONLY cold-boot step that honors WHICH slot
1099
- // was last active `restoreViaRefreshCookie` selects it via
1100
- // `selectActiveRefreshAccount(accounts, readActiveAuthuser())`. The
1101
- // `fedcm-silent` and per-apex `silent-iframe` steps only ever recover the
1102
- // PRIMARY central IdP session, so if either ran first they would clobber a
1103
- // switched (managed/org) account back to the primary on reload — the exact
1104
- // "switch is lost on reload" regression. So when a slot selection exists we
1105
- // run cookie-restore BEFORE those probes (and disable the later duplicate).
1106
- //
1107
- // Read ONCE here (synchronously usable by the step `enabled` gates below).
1108
- // It is non-null ONLY on first-party web apps that persist the slot. Both the
1109
- // device sign-in/switch paths AND `handleWebSSOSession` (FedCM/SSO/credentials
1110
- // commit funnel) now persist it — but only when the session genuinely joined
1111
- // the device set: `establishDeviceRefreshSlot` returns an `authuser` ONLY on
1112
- // first-party web (same registrable apex as the API), so a cross-apex RP never
1113
- // writes a phantom slot and native has no localStorage. Both therefore keep
1114
- // their existing order untouched. The earlier `stored-session` step still runs
1115
- // first (FIX-A latency), and when a slot exists but its cookies have all
1116
- // lapsed, cookie-restore simply returns no accounts and the chain falls through
1117
- // to the cross-domain fallbacks exactly as before.
1118
- const prioritizeMultiAccount = isWebBrowser() && readActiveAuthuser() !== null;
1119
-
1120
- // DELIBERATELY-SIGNED-OUT gate (web): when the user pressed "Sign out", any
1121
- // credential that can silently re-mint a session may still be live on the
1122
- // next reload — the central IdP session (FedCM credential association /
1123
- // per-apex `fedcm_session` cookie) AND the device refresh cookies. Since
1124
- // PR #455 the PRIMARY web session also joins the `oxy_rt_<authuser>` device
1125
- // set, so the un-gated cookie-restore steps would `refresh-all` that still-
1126
- // present cookie and sign the user back in without intent — exactly the
1127
- // "sign-out is silently undone" regression. So this flag gates EVERY
1128
- // AUTOMATIC silent-restore cold-boot step: `stored-session`,
1129
- // `cookie-restore-active`, `fedcm-silent`, `silent-iframe`, and
1130
- // `cookie-restore`. The gate — not a cookie wipe — is the authority: the
1131
- // account may stay "known" for a fast deliberate re-sign-in, but no step
1132
- // restores it silently while the flag is set. Read the durable flag ONCE here
1133
- // (synchronously usable by every step `enabled` gate). Any deliberate sign-in
1134
- // (password, FedCM, account switch, device claim) clears it, so there is no
1135
- // "stuck signed out" state. The `sso-bounce` step needs no extra gate: it is
1136
- // already self-suppressed after sign-out (its `hasPriorSession` hint is
1137
- // cleared). `sso-return` is NOT gated — it commits the result of a deliberate
1138
- // top-level `/sso` bounce the user just initiated. `shared-key-signin` is
1139
- // native-only and the flag never sets on native (`markSignedOut` no-ops
1140
- // off-web), so its gate would be moot; it is left untouched.
1160
+ // DELIBERATELY-SIGNED-OUT gate (web): when the user pressed "Sign out", the
1161
+ // per-apex `fedcm_session` cookie can still be live, so the AUTOMATIC
1162
+ // `silent-iframe` step below would re-mint a session on the very next cold
1163
+ // boot and sign the user back in without intent. Read the durable flag
1164
+ // ONCE here (synchronously usable by the step `enabled` gate) and skip
1165
+ // that step while it is set. Any deliberate sign-in clears it. The
1166
+ // `sso-bounce` step is already self-suppressed after sign-out (its
1167
+ // `hasPriorSession` hint is cleared), and the local `stored-session`
1168
+ // restore step is unaffected a deliberate sign-out clears those
1169
+ // credentials anyway.
1141
1170
  const silentRestoreBlocked = isSilentRestoreSuppressed();
1142
1171
  try {
1143
1172
  const outcome = await runColdBoot({
@@ -1159,28 +1188,22 @@ export const OxyProvider = ({
1159
1188
  };
1160
1189
  }
1161
1190
  }, {
1162
- // 2) Stored-session bearer restore. Runs on ALL platforms EXCEPT when
1163
- // the deliberately-signed-out flag is set (web only it never sets on
1164
- // native, so native still always reaches exactly this step, its ONLY
1165
- // restore path). This is also the common WEB reload winner.
1191
+ // 2) Stored-session bearer restore. NO `enabled` gate runs on ALL
1192
+ // platforms. This is native's ONLY restore path (every web-only step
1193
+ // is disabled off-browser, so native reaches exactly this) AND the
1194
+ // common WEB reload winner.
1166
1195
  //
1167
- // SIGNED-OUT GATE: after a deliberate full sign-out the stored session
1168
- // state is cleared, so this would normally skip anyway — but the gate
1169
- // makes that authoritative rather than incidental, so no residual
1170
- // stored token can silently restore while the user is signed out.
1171
- //
1172
- // ORDERING (FIX A): this step runs BEFORE the slow web-only
1173
- // probes (`fedcm-silent`, `silent-iframe`, `cookie-restore`). On a
1174
- // normal reload the local bearer validates in one round-trip and
1175
- // wins; `runColdBoot` then short-circuits and never even evaluates
1176
- // the slow no-redirect probes that would otherwise time out (the
1177
- // ~20-30s serial stall). The `sso-return` step stays AHEAD of this
1178
- // one — it must consume the URL fragment before any
1179
- // later step (or anything else) strips it. On a first visit with no
1180
- // local session this step skips and the cross-domain fallback chain
1181
- // (fedcm → iframe → cookie → sso-bounce) runs exactly as before.
1196
+ // ORDERING (FIX A): this step now runs BEFORE the slow web-only
1197
+ // `silent-iframe` probe. On a normal reload the local bearer
1198
+ // validates in one round-trip and wins; `runColdBoot`
1199
+ // then short-circuits and never even evaluates the slow
1200
+ // no-redirect probe that would otherwise time out (the ~20-30s
1201
+ // serial stall). The `sso-return` step stays AHEAD of this one —
1202
+ // it must consume the URL fragment before any later step (or
1203
+ // anything else) strips it. On a first visit with no local
1204
+ // session this step skips and the cross-domain fallback chain
1205
+ // (silent-iframe sso-bounce) runs exactly as before.
1182
1206
  id: 'stored-session',
1183
- enabled: () => !silentRestoreBlocked,
1184
1207
  run: async () => {
1185
1208
  const restored = await restoreStoredSession();
1186
1209
  if (restored) {
@@ -1250,66 +1273,7 @@ export const OxyProvider = ({
1250
1273
  };
1251
1274
  }
1252
1275
  }, {
1253
- // 2.75) LAST-ACTIVE multi-account restore (WEB, prioritized). Runs
1254
- // ONLY when an explicit persisted slot selection exists
1255
- // (`prioritizeMultiAccount`) — i.e. the user previously signed in or
1256
- // SWITCHED accounts on this first-party app. It runs BEFORE the
1257
- // `fedcm-silent` / `silent-iframe` probes (which only ever recover the
1258
- // PRIMARY central session) so a switched managed/org account survives
1259
- // reload instead of being clobbered back to the primary. The restore
1260
- // itself is the SAME `restoreViaRefreshCookie` as the terminal-tier
1261
- // step below: it rotates every device-local `oxy_rt_<authuser>` cookie
1262
- // and picks the persisted slot via `selectActiveRefreshAccount`,
1263
- // committing it (token + state + durable persistence + `markAuthResolved`).
1264
- // When no slot is persisted this is disabled and the original order is
1265
- // unchanged; when the slot's cookies have lapsed it returns no accounts
1266
- // and the chain falls through to the cross-domain fallbacks below.
1267
- //
1268
- // SIGNED-OUT GATE: since PR #455 the primary's `oxy_rt` slot survives a
1269
- // deliberate sign-out, so without this gate `refresh-all` would re-mint
1270
- // and silently re-log-in the user on reload. The flag is cleared by any
1271
- // deliberate sign-in (including an account SWITCH, which also writes the
1272
- // new active slot), so the switch-survives-reload path is unaffected.
1273
- id: 'cookie-restore-active',
1274
- enabled: () => prioritizeMultiAccount && !silentRestoreBlocked,
1275
- run: async () => {
1276
- const restored = await restoreViaRefreshCookie();
1277
- return restored ? {
1278
- kind: 'session',
1279
- session: true
1280
- } : {
1281
- kind: 'skip'
1282
- };
1283
- }
1284
- }, {
1285
- // 3) FedCM silent reauthn (Chrome) against the CENTRAL IdP
1286
- // (auth.oxy.so). `silentSignInWithFedCM` plants the access token
1287
- // internally; we commit the returned session via
1288
- // `handleWebSSOSession`. Guarded so it fires at most once per page
1289
- // load across remounts. This is an enhancement layered above the
1290
- // opaque-code bounce: when it succeeds the bounce never fires.
1291
- //
1292
- // FIX-B: additionally skipped when the earlier `stored-session` step
1293
- // already recovered a local session — the probe cannot improve on a
1294
- // valid local bearer, and skipping it avoids the silent round-trip.
1295
- id: 'fedcm-silent',
1296
- enabled: () => !storedSessionRestored && !silentRestoreBlocked && fedcmSupported && !servicesSilentAttempted.has(silentKey),
1297
- run: async () => {
1298
- servicesSilentAttempted.add(silentKey);
1299
- const session = await oxyServices.silentSignInWithFedCM?.();
1300
- if (!session || !commitWebSession) {
1301
- return {
1302
- kind: 'skip'
1303
- };
1304
- }
1305
- await commitWebSession(session);
1306
- return {
1307
- kind: 'session',
1308
- session: true
1309
- };
1310
- }
1311
- }, {
1312
- // 4) First-party silent iframe at the PER-APEX IdP — the DURABLE
1276
+ // 3) First-party silent iframe at the PER-APEX IdP the DURABLE
1313
1277
  // cross-domain reload-restore path. The durable session lives as a
1314
1278
  // first-party `fedcm_session` cookie on `auth.<rp-apex>` (e.g.
1315
1279
  // `auth.mention.earth`), established during the `/sso` bounce's
@@ -1356,39 +1320,7 @@ export const OxyProvider = ({
1356
1320
  };
1357
1321
  }
1358
1322
  }, {
1359
- // 5) Refresh-cookie restore (first-party only). On `*.oxy.so` the
1360
- // httpOnly `oxy_rt_${n}` cookies ride along and resurrect every
1361
- // device-local slot. On a cross-domain RP (mention.earth, …) the
1362
- // cookie is `Domain=oxy.so` so it never reaches `api.<apex>` —
1363
- // `refreshAllSessions` returns `{accounts:[]}` and this skips. That
1364
- // is correct; cross-domain restore is handled by the SSO bounce.
1365
- // FIX-D: `restoreViaRefreshCookie` bounds the request with
1366
- // `COOKIE_RESTORE_TIMEOUT` so a cross-domain stall cannot hang here.
1367
- //
1368
- // Disabled when `prioritizeMultiAccount` already ran the
1369
- // `cookie-restore-active` step above (an explicit persisted slot) — that
1370
- // earlier step is the identical restore, so this terminal-tier copy
1371
- // would be a redundant second `refreshAllSessions`. It still runs in the
1372
- // common no-persisted-slot case (e.g. first visit to a first-party app
1373
- // whose central refresh cookies already exist).
1374
- //
1375
- // SIGNED-OUT GATE (same rationale as `cookie-restore-active`): the
1376
- // primary's `oxy_rt` slot survives a deliberate sign-out (PR #455), so
1377
- // this must be skipped while the signed-out flag is set or it would
1378
- // silently re-restore the primary on reload.
1379
- id: 'cookie-restore',
1380
- enabled: () => isWebBrowser() && !prioritizeMultiAccount && !silentRestoreBlocked,
1381
- run: async () => {
1382
- const restored = await restoreViaRefreshCookie();
1383
- return restored ? {
1384
- kind: 'session',
1385
- session: true
1386
- } : {
1387
- kind: 'skip'
1388
- };
1389
- }
1390
- }, {
1391
- // 6) SSO bounce (TERMINAL, web only, at most once). No local session
1323
+ // 4) SSO bounce (TERMINAL, web only, at most once). No local session
1392
1324
  // was found by any step above. Top-level navigate to the central
1393
1325
  // `auth.oxy.so/sso?prompt=none` so the IdP can either mint a session
1394
1326
  // (returning an opaque code we exchange on the callback) or report
@@ -1465,9 +1397,9 @@ export const OxyProvider = ({
1465
1397
  }, error);
1466
1398
  }
1467
1399
  },
1468
- // Defense-in-depth: a single step whose promise never settles (the
1469
- // production FedCM-silent hang) can no longer block the chain forever.
1470
- // On expiry the runner keeps iterating to the terminal `sso-bounce`
1400
+ // Defense-in-depth: a single step whose promise never settles can no
1401
+ // longer block the chain forever. On expiry the runner keeps iterating
1402
+ // to the terminal `sso-bounce`
1471
1403
  // step so a genuine no-local-session visit still reaches the
1472
1404
  // cross-domain `/sso` fallback; the `finally` backstop flips
1473
1405
  // `authResolved` regardless. See `COLD_BOOT_OVERALL_DEADLINE`.
@@ -1487,6 +1419,76 @@ export const OxyProvider = ({
1487
1419
  method: 'restoreSessionsFromStorage'
1488
1420
  });
1489
1421
  }
1422
+
1423
+ // TOKEN LADDER → SESSIONCLIENT AUTHORITY HANDOFF. The steps above are
1424
+ // ONLY a token-acquisition ladder — they mint the first per-domain
1425
+ // access token by whichever means recovers one fastest. Once a session
1426
+ // is known (either this cold boot committed one via the ladder, or an
1427
+ // access token is already held in memory — e.g. a prior render already
1428
+ // planted it), hand off to the server-authoritative `SessionClient`:
1429
+ // `addCurrentAccount` registers this recovered account+session into the
1430
+ // server `DeviceSession` (derives identity from the bearer), `start`
1431
+ // bootstraps the full device-session state (server `activeAccountId` +
1432
+ // `activeToken`) and connects the realtime socket, and `syncFromClient`
1433
+ // projects that state onto the exposed `sessions`/`activeSessionId`/
1434
+ // `user`. This is what lets a switched (managed/org) account survive
1435
+ // reload: the SERVER, not a client-persisted slot, owns which account is
1436
+ // active. Never call the client when no session was acquired — an
1437
+ // anonymous visitor must stay logged out. Failures are logged and
1438
+ // swallowed; they must never throw out of cold boot.
1439
+ //
1440
+ // `addCurrentAccount` is SKIPPED when `registeredDuringBootRef` is
1441
+ // already `true` — every ladder step except `stored-session` commits
1442
+ // through `handleWebSSOSession`, which registers the account itself
1443
+ // (see its own `sessionClient.addCurrentAccount()` call). Without this
1444
+ // guard a winning `sso-return` / `shared-key-signin` / `silent-iframe`
1445
+ // step would register the SAME account twice
1446
+ // (`POST /session/device/add` called back-to-back). `start()` and
1447
+ // `syncFromClient()` still always run — `start()` is idempotent
1448
+ // (no-ops once already started) and is what connects the realtime
1449
+ // socket for the first time.
1450
+ if (outcome.kind === 'session' || oxyServices.getAccessToken()) {
1451
+ // Self-contained: never throws (own try/catch), so it is safe to let it
1452
+ // outlive the race below and finish in the background if the deadline
1453
+ // trips — the socket subscription then projects the state when it lands.
1454
+ const handoff = (async () => {
1455
+ try {
1456
+ if (!registeredDuringBootRef.current) {
1457
+ // Nested guard: registering this account into the device set is
1458
+ // best-effort. If it throws, `start()` must STILL run so the
1459
+ // realtime socket connects and state projects — cold boot
1460
+ // re-registers the account into the device set on the next load.
1461
+ try {
1462
+ await sessionClient.addCurrentAccount();
1463
+ } catch (addAccountErr) {
1464
+ loggerUtil.warn('cold-boot: addCurrentAccount failed; continuing to start()', {
1465
+ component: 'OxyContext',
1466
+ method: 'restoreSessionsFromStorage'
1467
+ }, addAccountErr);
1468
+ }
1469
+ }
1470
+ await sessionClient.start();
1471
+ await syncFromClient();
1472
+ } catch (startErr) {
1473
+ loggerUtil.warn('cold-boot: SessionClient start failed', {
1474
+ component: 'OxyContext',
1475
+ method: 'restoreSessionsFromStorage'
1476
+ }, startErr);
1477
+ }
1478
+ })();
1479
+ // Bound how long auth resolution waits for the handoff (see
1480
+ // `SESSION_HANDOFF_DEADLINE`): the token is already planted, so on a
1481
+ // slow backend we proceed to `markAuthResolved` and let the handoff
1482
+ // complete asynchronously rather than stalling the spinner.
1483
+ let handoffDeadlineId;
1484
+ await Promise.race([handoff, new Promise(resolve => {
1485
+ handoffDeadlineId = setTimeout(resolve, SESSION_HANDOFF_DEADLINE);
1486
+ })]).finally(() => {
1487
+ if (handoffDeadlineId !== undefined) {
1488
+ clearTimeout(handoffDeadlineId);
1489
+ }
1490
+ });
1491
+ }
1490
1492
  } catch (error) {
1491
1493
  if (__DEV__) {
1492
1494
  loggerUtil.error('Auth init error', error instanceof Error ? error : new Error(String(error)), {
@@ -1505,7 +1507,7 @@ export const OxyProvider = ({
1505
1507
  // `authResolved` finally flip. Monotonic — never reverts on later restores.
1506
1508
  markAuthResolved();
1507
1509
  }
1508
- }, [oxyServices, storage, storageKeys.priorSession, restoreViaRefreshCookie, restoreStoredSession, runSsoReturn, markAuthResolved]);
1510
+ }, [oxyServices, storage, storageKeys.priorSession, restoreStoredSession, runSsoReturn, markAuthResolved, sessionClient, syncFromClient]);
1509
1511
  useEffect(() => {
1510
1512
  if (!storage || initialized) {
1511
1513
  return;
@@ -1620,34 +1622,37 @@ export const OxyProvider = ({
1620
1622
 
1621
1623
  // A committed web session re-enables automatic silent restore: clear the
1622
1624
  // durable "deliberately signed out" flag. This funnel is reached by deliberate
1623
- // sign-ins (password, interactive FedCM, `/sso` return) AND by the silent
1625
+ // sign-ins (password, `/sso` return) AND by the silent
1624
1626
  // cold-boot steps — but those are GATED on the flag, so when it is set they
1625
1627
  // never run and never reach here; clearing is therefore only hit on a genuine
1626
1628
  // (re-)sign-in or when restore was already permitted, both correct.
1627
1629
  clearSignedOut();
1628
1630
 
1629
- // Register this primary session in the device's first-party multi-account
1630
- // refresh-cookie set (web only). Every web primary-session restore funnels
1631
- // through here — FedCM silent (`/fedcm/exchange`), per-apex `/auth/silent`
1632
- // iframe, central `/sso` return, and keyless password sign-in. Of those, only
1633
- // a same-apex `/fedcm/exchange` plants an `oxy_rt_<authuser>` slot as a side
1634
- // effect; the cross-origin/credential-less restores (`/sso/exchange`,
1635
- // `/auth/silent` postMessage) cannot set an `api.oxy.so` cookie at all. So
1636
- // WITHOUT this call a web primary never joins the device set: `refresh-all`
1637
- // returns zero accounts, and account-switch persistence + the
1638
- // `cookie-restore-active` cold-boot step have no foundation. Calling the shared
1639
- // `POST /auth/session` primitive here makes EVERY primary participate in the
1640
- // set, re-plants the rotated token, and records the active `authuser` so the
1641
- // next cold boot's `cookie-restore-active` reconciles the persisted active
1642
- // (possibly switched) account instead of `fedcm-silent` clobbering it back to
1643
- // the primary. Best-effort: a `null` result (native / transient failure)
1644
- // leaves the in-session token untouched.
1645
- const primaryAuthuser = await oxyServices.establishDeviceRefreshSlot();
1646
- // A non-null slot is returned ONLY on first-party web (the helper gates on
1647
- // platform + same registrable apex), so recording the active slot here is safe
1648
- // and never writes a phantom authuser for a cross-apex RP or native.
1649
- if (typeof primaryAuthuser === 'number') {
1650
- writeActiveAuthuser(primaryAuthuser);
1631
+ // Register this recovered account+session into the server-authoritative
1632
+ // device-session set. Every web primary-session restore funnels through
1633
+ // here — per-apex `/auth/silent` iframe,
1634
+ // central `/sso` return, and keyless password sign-in and each of them
1635
+ // needs the resulting account added to the device's `DeviceSession` doc so
1636
+ // the sign-in persists across reload / other tabs / devices and the
1637
+ // realtime `device:<deviceId>` socket has an account to track.
1638
+ // `sessionClient.addCurrentAccount()` (`POST /session/device/add`) derives
1639
+ // identity from the bearer this function just planted above;
1640
+ // `syncFromClient()` reprojects the resulting server state onto the
1641
+ // exposed sessions/activeSessionId/user. Best-effort: a failure here must
1642
+ // NEVER fail the sign-in itself cold boot re-registers this account into
1643
+ // the device set on the next load regardless. `registeredDuringBootRef` is
1644
+ // flipped on success so the cold-boot post-ladder handoff (which reaches
1645
+ // every step, including this one) does not redundantly re-register the
1646
+ // SAME account with a second `POST /session/device/add`.
1647
+ try {
1648
+ await sessionClient.addCurrentAccount();
1649
+ registeredDuringBootRef.current = true;
1650
+ await syncFromClient();
1651
+ } catch (registrationError) {
1652
+ loggerUtil.warn('handleWebSSOSession: failed to register session into device set', {
1653
+ component: 'OxyContext',
1654
+ method: 'handleWebSSOSession'
1655
+ }, registrationError);
1651
1656
  }
1652
1657
  const clientSession = {
1653
1658
  sessionId: session.sessionId,
@@ -1655,10 +1660,7 @@ export const OxyProvider = ({
1655
1660
  expiresAt: session.expiresAt || new Date(Date.now() + DEFAULT_SESSION_VALIDITY_MS).toISOString(),
1656
1661
  lastActive: new Date().toISOString(),
1657
1662
  userId: session.user.id?.toString() ?? '',
1658
- isCurrent: true,
1659
- ...(typeof primaryAuthuser === 'number' ? {
1660
- authuser: primaryAuthuser
1661
- } : null)
1663
+ isCurrent: true
1662
1664
  };
1663
1665
  updateSessions([clientSession], {
1664
1666
  merge: true
@@ -1672,7 +1674,7 @@ export const OxyProvider = ({
1672
1674
  // because `onAuthStateChange` triggers a route navigation that can
1673
1675
  // interrupt/supersede a still-pending async write. Persisting first
1674
1676
  // guarantees the durable record lands; that is exactly what was missing
1675
- // when `oxy_session_session_ids` came back empty after a successful FedCM
1677
+ // when `oxy_session_session_ids` came back empty after a successful SSO
1676
1678
  // sign-in (the user appeared logged in until reload, then had no session to
1677
1679
  // restore). `persistSessionDurably` awaits the READY storage instance rather
1678
1680
  // than reading the possibly-null `storage` state, so a sign-in fired the
@@ -1699,14 +1701,14 @@ export const OxyProvider = ({
1699
1701
  fullUser = session.user;
1700
1702
  }
1701
1703
  loginSuccess(fullUser);
1702
- // A session is now committed (FedCM silent / per-apex iframe /
1704
+ // A session is now committed (per-apex iframe /
1703
1705
  // SSO-return all funnel through here) — unblock the auth-resolution
1704
1706
  // gate immediately, ahead of the cold-boot chain returning (idempotent).
1705
1707
  markAuthResolvedRef.current();
1706
1708
  onAuthStateChange?.(fullUser);
1707
- }, [oxyServices, updateSessions, setActiveSessionId, loginSuccess, onAuthStateChange, persistSessionDurably]);
1709
+ }, [oxyServices, updateSessions, setActiveSessionId, loginSuccess, onAuthStateChange, persistSessionDurably, sessionClient, syncFromClient]);
1708
1710
 
1709
- // Expose `handleWebSSOSession` to the cold-boot FedCM/iframe/SSO steps,
1711
+ // Expose `handleWebSSOSession` to the cold-boot iframe/SSO steps,
1710
1712
  // which reference it through a ref because they are declared above this
1711
1713
  // callback. Assigned synchronously on every render so the ref is populated
1712
1714
  // before the cold-boot effect (gated on `storage`/`initialized`) can fire.
@@ -1714,7 +1716,7 @@ export const OxyProvider = ({
1714
1716
 
1715
1717
  // Keyless native sign-in: username/email + password. The slimmed Accounts app
1716
1718
  // (which no longer holds a local identity key) uses this; it commits a
1717
- // successful session through the SAME `handleWebSSOSession` path FedCM / SSO
1719
+ // successful session through the SAME `handleWebSSOSession` path SSO
1718
1720
  // use, so state, durable persistence, profile fetch, and `markAuthResolved`
1719
1721
  // all run identically. The API returns either a full session OR a two-factor
1720
1722
  // handoff (`{ twoFactorRequired, loginToken }`) — we surface the latter as a
@@ -1747,32 +1749,29 @@ export const OxyProvider = ({
1747
1749
  };
1748
1750
  }, [oxyServices, handleWebSSOSession]);
1749
1751
 
1750
- // Cross-domain silent SSO is now owned by the `fedcm-silent` / `silent-iframe`
1751
- // cold-boot steps above (the ordered `runColdBoot` sequence). `useWebSSO`
1752
- // remains mounted for its module-level run-once guard and its interactive
1753
- // FedCM helpers, and as a bounded post-boot safety net: it can fire at most
1754
- // once per page load (its own module guard), and only AFTER cold boot has
1755
- // finished (`tokenReady`) with no user recovered. We deliberately keep
1756
- // `shouldTryWebSSO` as `tokenReady && !user && initialized` — it is NOT
1757
- // loosened; cold boot runs while `tokenReady` is false, so this never races
1758
- // the cold-boot silent step.
1759
- const shouldTryWebSSO = isWebBrowser() && tokenReady && !user && initialized;
1760
- useWebSSO({
1761
- oxyServices,
1762
- onSessionFound: handleWebSSOSession,
1763
- onError: error => {
1764
- if (__DEV__) {
1765
- loggerUtil.debug('Web SSO check failed (non-critical)', {
1766
- component: 'OxyContext'
1767
- }, error);
1768
- }
1769
- },
1770
- enabled: shouldTryWebSSO
1771
- });
1752
+ // Cross-domain silent SSO is owned ENTIRELY by the `silent-iframe` cold-boot
1753
+ // step above (the ordered `runColdBoot` sequence) plus the terminal
1754
+ // `sso-bounce` fallback. There is no post-boot safety net here: the FedCM
1755
+ // silent post-boot retry that used to run via `useWebSSO` was removed along
1756
+ // with the rest of the client FedCM surface (Chrome-only; see
1757
+ // `CrossDomainAuth`'s doc comment in `@oxyhq/core` for the production
1758
+ // incident that motivated the removal).
1772
1759
 
1773
1760
  // IdP session validation via lightweight iframe check
1774
1761
  // When user returns to tab, verify auth.oxy.so still has their session
1775
1762
  // If session is gone (cleared/logged out), clear local session too
1763
+ //
1764
+ // KEPT (session-sync cutover audit, Task 5): this is NOT redundant with the
1765
+ // `SessionClient` socket (`device:<deviceId>`, `session_state` push). That
1766
+ // socket only fires for revocations that flow through oxy-api's
1767
+ // `DeviceSession` authority (`POST /session/device/signout` and friends). A
1768
+ // sign-out at the CENTRAL IdP itself (`auth.oxy.so`'s own `AuthSession` /
1769
+ // `fedcm_session` — e.g. a direct IdP-side sign-out, or an admin/security
1770
+ // revocation there) does not currently touch `DeviceSession` at all, so no
1771
+ // socket push follows — this iframe poll is the ONLY mechanism that catches
1772
+ // it today. A future phase should move this server-side (IdP deactivation
1773
+ // -> `DeviceSession` update -> broadcast over the same socket), at which
1774
+ // point this client-side poll becomes redundant and can be deleted.
1776
1775
  const lastIdPCheckRef = useRef(0);
1777
1776
  const pendingIdPCleanupRef = useRef(null);
1778
1777
 
@@ -1838,35 +1837,40 @@ export const OxyProvider = ({
1838
1837
  pendingIdPCleanupRef.current = null;
1839
1838
  };
1840
1839
  }, [user, initialized, clearSessionState, resolvedAuthWebUrl]);
1841
- const activeSession = activeSessionId ? sessions.find(session => session.sessionId === activeSessionId) : undefined;
1842
- const currentDeviceId = activeSession?.deviceId ?? null;
1843
- const userId = user?.id;
1844
- const refreshSessionsWithUser = useCallback(() => refreshSessions(userId), [refreshSessions, userId]);
1845
- const handleSessionRemoved = useCallback(sessionId => {
1846
- trackRemovedSession(sessionId);
1847
- }, [trackRemovedSession]);
1848
- const handleRemoteSignOut = useCallback(() => {
1849
- toast.info('You have been signed out remotely.');
1850
- logout().catch(remoteError => logger('Failed to process remote sign out', remoteError));
1851
- }, [logger, logout]);
1852
- useSessionSocket({
1853
- userId,
1854
- activeSessionId,
1855
- currentDeviceId,
1856
- refreshSessions: refreshSessionsWithUser,
1857
- clearSessionState,
1858
- baseURL: oxyServices.getBaseURL(),
1859
- getAccessToken: () => oxyServices.getAccessToken(),
1860
- onRemoteSignOut: handleRemoteSignOut,
1861
- onSessionRemoved: handleSessionRemoved
1862
- });
1840
+
1841
+ // Exposed `refreshSessions`: re-bootstraps the server-authoritative device
1842
+ // state (`GET /session/device/state`, the SAME request `SessionClient.start()`
1843
+ // makes) and reprojects it via `syncFromClient` — a manual pull-to-refresh
1844
+ // counterpart to the realtime `device:<deviceId>` socket `client.start()`
1845
+ // already owns (there is no per-domain `useSessionSocket` anymore; a REMOTE
1846
+ // sign-out or account change arrives over that socket and is handled by
1847
+ // `syncFromClient`'s subscription below, including the full-sign-out gap fix
1848
+ // documented above it).
1849
+ const refreshSessionsForContext = useCallback(async () => {
1850
+ await sessionClient.bootstrap();
1851
+ await syncFromClient();
1852
+ }, [sessionClient, syncFromClient]);
1853
+
1854
+ // Exposed `switchSession`: routes through the server-authoritative
1855
+ // `SessionClient` (Fase 3-B) rather than `useSessionManagement`'s own
1856
+ // `switchSession` (which remains, unchanged, for `useAuthOperations`'
1857
+ // internal same-user duplicate-session dedup — a different, legacy
1858
+ // session-validate concept unrelated to switching the device's ACTIVE
1859
+ // account). Resolves the target account from the current device state,
1860
+ // asks the server to switch, reprojects, and returns the now-active user.
1863
1861
  const switchSessionForContext = useCallback(async sessionId => {
1864
- // Propagate the activated user so callers (the device-flow sign-in,
1865
- // `useSwitchSession`'s cache write, account chooser) receive it. The
1866
- // underlying session-management `switchSession` already resolves the
1867
- // `User`; the previous `Promise<void>` wrapper discarded it.
1868
- return switchSession(sessionId);
1869
- }, [switchSession]);
1862
+ const targetAccountId = sessionClient.getState()?.accounts.find(account => account.sessionId === sessionId)?.accountId;
1863
+ if (!targetAccountId) {
1864
+ throw new Error(`No device account found for session "${sessionId}"`);
1865
+ }
1866
+ await sessionClient.switchAccount(targetAccountId);
1867
+ await syncFromClient();
1868
+ const activeUser = useAuthStore.getState().user;
1869
+ if (!activeUser) {
1870
+ throw new Error('Active account profile could not be resolved after switch');
1871
+ }
1872
+ return activeUser;
1873
+ }, [sessionClient, syncFromClient]);
1870
1874
 
1871
1875
  // Identity management wrappers (delegate to KeyManager)
1872
1876
  const hasIdentity = useCallback(async () => {
@@ -1923,16 +1927,41 @@ export const OxyProvider = ({
1923
1927
  }
1924
1928
  }, [isAuthenticated, initialized, tokenReady, refreshAccounts]);
1925
1929
 
1930
+ // Shared post-switch side effects, run identically regardless of which
1931
+ // `switchToAccount` branch handled the switch below: reload the switchable
1932
+ // account graph (the new active account's relationships differ) and
1933
+ // invalidate every query so all data refetches as the new account.
1934
+ const runPostAccountSwitchSideEffects = useCallback(async () => {
1935
+ await refreshAccounts();
1936
+ queryClient.invalidateQueries();
1937
+ }, [refreshAccounts, queryClient]);
1938
+
1926
1939
  // Switch the active session INTO an account from the unified graph. In the
1927
- // REAL-SESSION model this is identical to switching device sign-ins: mint a
1928
- // real session for the target and make the whole app that account. The removed
1929
- // `X-Acting-As` delegation header is gone `oxyServices.switchToAccount`
1930
- // plants the minted access token (the refresh token is the server-set httpOnly
1931
- // `oxy_rt_<authuser>` cookie, so the session joins the device multi-account set
1932
- // and survives reload / `refresh-all`). We then commit the session into context
1933
- // state the SAME way sign-in / `switchSession` do, refresh the account graph,
1934
- // and invalidate every query so all data reloads as the new account.
1940
+ // REAL-SESSION model this is identical to switching device sign-ins: the
1941
+ // whole app becomes that account. There is exactly ONE uniform path for
1942
+ // that the device's server-authoritative `SessionClient.switchAccount()`
1943
+ // used for EVERY switch, org/managed accounts included:
1944
+ //
1945
+ // - If `accountId` is ALREADY registered on this device's multi-account
1946
+ // set (a prior `switchToAccount`/sign-in already added it), switch
1947
+ // straight through `sessionClient.switchAccount()` the SAME path
1948
+ // `switchSession` uses for ordinary device sessions. No new session is
1949
+ // minted, so no session churn / deactivation of the account's existing
1950
+ // session (H4).
1951
+ // - Only the FIRST time an account is switched into does it need minting:
1952
+ // `oxyServices.switchToAccount` mints+plants a real session, then
1953
+ // `sessionClient.addCurrentAccount()` registers it into the device set
1954
+ // (server-set httpOnly `oxy_rt_<authuser>` cookie, so the session joins
1955
+ // the device multi-account set and survives reload / `refresh-all`).
1956
+ // Every subsequent switch into that same account takes the branch above.
1935
1957
  const switchToAccount = useCallback(async accountId => {
1958
+ const deviceState = sessionClient.getState();
1959
+ if (deviceState?.accounts.some(account => account.accountId === accountId)) {
1960
+ await sessionClient.switchAccount(accountId);
1961
+ await syncFromClient();
1962
+ await runPostAccountSwitchSideEffects();
1963
+ return;
1964
+ }
1936
1965
  const result = await oxyServices.switchToAccount(accountId);
1937
1966
  if (!result?.user || !result?.sessionId) {
1938
1967
  throw new Error('Account switch did not return a valid session');
@@ -1944,8 +1973,10 @@ export const OxyProvider = ({
1944
1973
 
1945
1974
  // `oxyServices.switchToAccount` already planted `result.accessToken` as the
1946
1975
  // active token; mirror the minted session into the multi-account store and
1947
- // mark it current, recording the device `authuser` slot so web silent-switch
1948
- // and the device account chooser can address it.
1976
+ // mark it current. The device account SET (and its `authuser` slot
1977
+ // numbering) is server-authoritative via `SessionClient` the
1978
+ // `addCurrentAccount` + `syncFromClient` reprojection below supersedes this
1979
+ // local mirror with the server's own state.
1949
1980
  const now = new Date();
1950
1981
  const clientSession = {
1951
1982
  sessionId: result.sessionId,
@@ -1953,20 +1984,35 @@ export const OxyProvider = ({
1953
1984
  expiresAt: result.expiresAt || new Date(now.getTime() + DEFAULT_SESSION_VALIDITY_MS).toISOString(),
1954
1985
  lastActive: now.toISOString(),
1955
1986
  userId: result.user.id?.toString() ?? '',
1956
- isCurrent: true,
1957
- ...(typeof result.authuser === 'number' ? {
1958
- authuser: result.authuser
1959
- } : null)
1987
+ isCurrent: true
1960
1988
  };
1961
1989
  updateSessions([clientSession], {
1962
1990
  merge: true
1963
1991
  });
1964
1992
  setActiveSessionId(result.sessionId);
1965
- if (isWebBrowser() && typeof result.authuser === 'number') {
1966
- writeActiveAuthuser(result.authuser);
1967
- }
1968
1993
  await persistSessionDurably(result.sessionId);
1969
1994
 
1995
+ // Register the switched-to account into the device's server-authoritative
1996
+ // multi-account set (the SAME `SessionClient.addCurrentAccount()` the
1997
+ // cold-boot handoff uses — it derives identity from the bearer, which
1998
+ // `oxyServices.switchToAccount` already planted above) and reproject
1999
+ // state, so the switch persists across reload / other tabs / devices
2000
+ // instead of only living in this provider's local session-management
2001
+ // state. Best-effort: the token for the new account is already planted, so
2002
+ // a transient failure here must NEVER reject the switch — fall through to
2003
+ // `loginSuccess`/`onAuthStateChange` so the UI reflects the switched-to
2004
+ // account (cold boot re-registers into the device set on next load).
2005
+ // Mirrors the same guard in `handleWebSSOSession`.
2006
+ try {
2007
+ await sessionClient.addCurrentAccount();
2008
+ await syncFromClient();
2009
+ } catch (registrationError) {
2010
+ loggerUtil.warn('switchToAccount: failed to register account into device set', {
2011
+ component: 'OxyContext',
2012
+ method: 'switchToAccount'
2013
+ }, registrationError);
2014
+ }
2015
+
1970
2016
  // Fetch the canonical User for the new account (the switch result carries
1971
2017
  // only MinimalUserData); fall back to that minimal shape if the profile
1972
2018
  // fetch fails so the app still reflects the switched identity.
@@ -1984,14 +2030,8 @@ export const OxyProvider = ({
1984
2030
  }
1985
2031
  loginSuccess(fullUser);
1986
2032
  onAuthStateChange?.(fullUser);
1987
-
1988
- // Reload the switchable account graph (the new active account's relationships
1989
- // differ) and invalidate every query so all data refetches as the new
1990
- // account — the deviceAccounts probe re-enumerates the multi-account set so
1991
- // the switched account now appears as a device session.
1992
- await refreshAccounts();
1993
- queryClient.invalidateQueries();
1994
- }, [oxyServices, updateSessions, setActiveSessionId, persistSessionDurably, loginSuccess, onAuthStateChange, refreshAccounts, queryClient]);
2033
+ await runPostAccountSwitchSideEffects();
2034
+ }, [oxyServices, updateSessions, setActiveSessionId, persistSessionDurably, sessionClient, syncFromClient, loginSuccess, onAuthStateChange, runPostAccountSwitchSideEffects]);
1995
2035
  const createAccountFn = useCallback(async data => {
1996
2036
  const account = await oxyServices.createAccount(data);
1997
2037
  await refreshAccounts();
@@ -2025,7 +2065,7 @@ export const OxyProvider = ({
2025
2065
  logoutAll,
2026
2066
  switchSession: switchSessionForContext,
2027
2067
  removeSession: logout,
2028
- refreshSessions: refreshSessionsWithUser,
2068
+ refreshSessions: refreshSessionsForContext,
2029
2069
  setLanguage,
2030
2070
  getDeviceSessions,
2031
2071
  logoutAllDeviceSessions,
@@ -2042,7 +2082,7 @@ export const OxyProvider = ({
2042
2082
  switchToAccount,
2043
2083
  refreshAccounts,
2044
2084
  createAccount: createAccountFn
2045
- }), [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]);
2085
+ }), [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]);
2046
2086
  return /*#__PURE__*/_jsx(OxyContext.Provider, {
2047
2087
  value: contextValue,
2048
2088
  children: ssoCallbackIntercepting ? null : children