@oxyhq/services 13.3.0 → 14.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/lib/commonjs/index.js +11 -16
  2. package/lib/commonjs/index.js.map +1 -1
  3. package/lib/commonjs/ui/components/AccountMenu.js +103 -65
  4. package/lib/commonjs/ui/components/AccountMenu.js.map +1 -1
  5. package/lib/commonjs/ui/components/AccountSwitcher.js +84 -126
  6. package/lib/commonjs/ui/components/AccountSwitcher.js.map +1 -1
  7. package/lib/commonjs/ui/components/ProfileMenu.js +140 -153
  8. package/lib/commonjs/ui/components/ProfileMenu.js.map +1 -1
  9. package/lib/commonjs/ui/components/SignInModal.js +4 -4
  10. package/lib/commonjs/ui/components/SignInModal.js.map +1 -1
  11. package/lib/commonjs/ui/context/OxyContext.js +506 -466
  12. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  13. package/lib/commonjs/ui/context/hooks/useAuthOperations.js +59 -57
  14. package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
  15. package/lib/commonjs/ui/context/inSessionTokenRefresh.js +23 -42
  16. package/lib/commonjs/ui/context/inSessionTokenRefresh.js.map +1 -1
  17. package/lib/commonjs/ui/context/silentSessionRestore.js +12 -21
  18. package/lib/commonjs/ui/context/silentSessionRestore.js.map +1 -1
  19. package/lib/commonjs/ui/hooks/useAuth.js +4 -3
  20. package/lib/commonjs/ui/hooks/useAuth.js.map +1 -1
  21. package/lib/commonjs/ui/hooks/useOxyAuthSession.js +12 -7
  22. package/lib/commonjs/ui/hooks/useOxyAuthSession.js.map +1 -1
  23. package/lib/commonjs/ui/hooks/useSessionManagement.js +17 -65
  24. package/lib/commonjs/ui/hooks/useSessionManagement.js.map +1 -1
  25. package/lib/commonjs/ui/hooks/useSwitchableAccounts.js +288 -0
  26. package/lib/commonjs/ui/hooks/useSwitchableAccounts.js.map +1 -0
  27. package/lib/commonjs/ui/hooks/useWebSSO.js +12 -191
  28. package/lib/commonjs/ui/hooks/useWebSSO.js.map +1 -1
  29. package/lib/commonjs/ui/screens/OxyAuthScreen.js +2 -2
  30. package/lib/commonjs/ui/screens/OxyAuthScreen.js.map +1 -1
  31. package/lib/commonjs/ui/session/createSessionClient.js +28 -0
  32. package/lib/commonjs/ui/session/createSessionClient.js.map +1 -0
  33. package/lib/commonjs/ui/session/index.js +52 -0
  34. package/lib/commonjs/ui/session/index.js.map +1 -0
  35. package/lib/commonjs/ui/session/projectSessionState.js +86 -0
  36. package/lib/commonjs/ui/session/projectSessionState.js.map +1 -0
  37. package/lib/commonjs/ui/session/sessionClientHost.js +30 -0
  38. package/lib/commonjs/ui/session/sessionClientHost.js.map +1 -0
  39. package/lib/commonjs/ui/session/tokenTransport.js +77 -0
  40. package/lib/commonjs/ui/session/tokenTransport.js.map +1 -0
  41. package/lib/commonjs/ui/utils/activeAuthuser.js +35 -79
  42. package/lib/commonjs/ui/utils/activeAuthuser.js.map +1 -1
  43. package/lib/commonjs/ui/utils/sessionHelpers.js +1 -51
  44. package/lib/commonjs/ui/utils/sessionHelpers.js.map +1 -1
  45. package/lib/commonjs/utils/deviceFlowSignIn.js +70 -15
  46. package/lib/commonjs/utils/deviceFlowSignIn.js.map +1 -1
  47. package/lib/module/index.js +5 -4
  48. package/lib/module/index.js.map +1 -1
  49. package/lib/module/ui/components/AccountMenu.js +103 -65
  50. package/lib/module/ui/components/AccountMenu.js.map +1 -1
  51. package/lib/module/ui/components/AccountSwitcher.js +85 -127
  52. package/lib/module/ui/components/AccountSwitcher.js.map +1 -1
  53. package/lib/module/ui/components/ProfileMenu.js +143 -156
  54. package/lib/module/ui/components/ProfileMenu.js.map +1 -1
  55. package/lib/module/ui/components/SignInModal.js +4 -4
  56. package/lib/module/ui/components/SignInModal.js.map +1 -1
  57. package/lib/module/ui/context/OxyContext.js +510 -470
  58. package/lib/module/ui/context/OxyContext.js.map +1 -1
  59. package/lib/module/ui/context/hooks/useAuthOperations.js +61 -60
  60. package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
  61. package/lib/module/ui/context/inSessionTokenRefresh.js +24 -43
  62. package/lib/module/ui/context/inSessionTokenRefresh.js.map +1 -1
  63. package/lib/module/ui/context/silentSessionRestore.js +12 -20
  64. package/lib/module/ui/context/silentSessionRestore.js.map +1 -1
  65. package/lib/module/ui/hooks/useAuth.js +4 -3
  66. package/lib/module/ui/hooks/useAuth.js.map +1 -1
  67. package/lib/module/ui/hooks/useOxyAuthSession.js +12 -7
  68. package/lib/module/ui/hooks/useOxyAuthSession.js.map +1 -1
  69. package/lib/module/ui/hooks/useSessionManagement.js +18 -66
  70. package/lib/module/ui/hooks/useSessionManagement.js.map +1 -1
  71. package/lib/module/ui/hooks/useSwitchableAccounts.js +283 -0
  72. package/lib/module/ui/hooks/useSwitchableAccounts.js.map +1 -0
  73. package/lib/module/ui/hooks/useWebSSO.js +13 -192
  74. package/lib/module/ui/hooks/useWebSSO.js.map +1 -1
  75. package/lib/module/ui/screens/OxyAuthScreen.js +2 -2
  76. package/lib/module/ui/screens/OxyAuthScreen.js.map +1 -1
  77. package/lib/module/ui/session/createSessionClient.js +25 -0
  78. package/lib/module/ui/session/createSessionClient.js.map +1 -0
  79. package/lib/module/ui/session/index.js +17 -0
  80. package/lib/module/ui/session/index.js.map +1 -0
  81. package/lib/module/ui/session/projectSessionState.js +79 -0
  82. package/lib/module/ui/session/projectSessionState.js.map +1 -0
  83. package/lib/module/ui/session/sessionClientHost.js +26 -0
  84. package/lib/module/ui/session/sessionClientHost.js.map +1 -0
  85. package/lib/module/ui/session/tokenTransport.js +74 -0
  86. package/lib/module/ui/session/tokenTransport.js.map +1 -0
  87. package/lib/module/ui/utils/activeAuthuser.js +34 -76
  88. package/lib/module/ui/utils/activeAuthuser.js.map +1 -1
  89. package/lib/module/ui/utils/sessionHelpers.js +0 -49
  90. package/lib/module/ui/utils/sessionHelpers.js.map +1 -1
  91. package/lib/module/utils/deviceFlowSignIn.js +71 -16
  92. package/lib/module/utils/deviceFlowSignIn.js.map +1 -1
  93. package/lib/typescript/commonjs/index.d.ts +2 -3
  94. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  95. package/lib/typescript/commonjs/ui/components/AccountMenu.d.ts +4 -2
  96. package/lib/typescript/commonjs/ui/components/AccountMenu.d.ts.map +1 -1
  97. package/lib/typescript/commonjs/ui/components/AccountSwitcher.d.ts +7 -8
  98. package/lib/typescript/commonjs/ui/components/AccountSwitcher.d.ts.map +1 -1
  99. package/lib/typescript/commonjs/ui/components/ProfileMenu.d.ts +12 -9
  100. package/lib/typescript/commonjs/ui/components/ProfileMenu.d.ts.map +1 -1
  101. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts +16 -13
  102. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
  103. package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts +27 -3
  104. package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  105. package/lib/typescript/commonjs/ui/context/inSessionTokenRefresh.d.ts +22 -17
  106. package/lib/typescript/commonjs/ui/context/inSessionTokenRefresh.d.ts.map +1 -1
  107. package/lib/typescript/commonjs/ui/context/silentSessionRestore.d.ts +12 -18
  108. package/lib/typescript/commonjs/ui/context/silentSessionRestore.d.ts.map +1 -1
  109. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts +1 -0
  110. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  111. package/lib/typescript/commonjs/ui/hooks/useAuth.d.ts +1 -1
  112. package/lib/typescript/commonjs/ui/hooks/useAuth.d.ts.map +1 -1
  113. package/lib/typescript/commonjs/ui/hooks/useOxyAuthSession.d.ts +2 -2
  114. package/lib/typescript/commonjs/ui/hooks/useOxyAuthSession.d.ts.map +1 -1
  115. package/lib/typescript/commonjs/ui/hooks/useSessionManagement.d.ts.map +1 -1
  116. package/lib/typescript/commonjs/ui/hooks/useSwitchableAccounts.d.ts +162 -0
  117. package/lib/typescript/commonjs/ui/hooks/useSwitchableAccounts.d.ts.map +1 -0
  118. package/lib/typescript/commonjs/ui/hooks/useWebSSO.d.ts +13 -52
  119. package/lib/typescript/commonjs/ui/hooks/useWebSSO.d.ts.map +1 -1
  120. package/lib/typescript/commonjs/ui/session/createSessionClient.d.ts +15 -0
  121. package/lib/typescript/commonjs/ui/session/createSessionClient.d.ts.map +1 -0
  122. package/lib/typescript/commonjs/ui/session/index.d.ts +15 -0
  123. package/lib/typescript/commonjs/ui/session/index.d.ts.map +1 -0
  124. package/lib/typescript/commonjs/ui/session/projectSessionState.d.ts +45 -0
  125. package/lib/typescript/commonjs/ui/session/projectSessionState.d.ts.map +1 -0
  126. package/lib/typescript/commonjs/ui/session/sessionClientHost.d.ts +14 -0
  127. package/lib/typescript/commonjs/ui/session/sessionClientHost.d.ts.map +1 -0
  128. package/lib/typescript/commonjs/ui/session/tokenTransport.d.ts +24 -0
  129. package/lib/typescript/commonjs/ui/session/tokenTransport.d.ts.map +1 -0
  130. package/lib/typescript/commonjs/ui/utils/activeAuthuser.d.ts +21 -29
  131. package/lib/typescript/commonjs/ui/utils/activeAuthuser.d.ts.map +1 -1
  132. package/lib/typescript/commonjs/ui/utils/sessionHelpers.d.ts +0 -19
  133. package/lib/typescript/commonjs/ui/utils/sessionHelpers.d.ts.map +1 -1
  134. package/lib/typescript/commonjs/utils/deviceFlowSignIn.d.ts +51 -19
  135. package/lib/typescript/commonjs/utils/deviceFlowSignIn.d.ts.map +1 -1
  136. package/lib/typescript/module/index.d.ts +2 -3
  137. package/lib/typescript/module/index.d.ts.map +1 -1
  138. package/lib/typescript/module/ui/components/AccountMenu.d.ts +4 -2
  139. package/lib/typescript/module/ui/components/AccountMenu.d.ts.map +1 -1
  140. package/lib/typescript/module/ui/components/AccountSwitcher.d.ts +7 -8
  141. package/lib/typescript/module/ui/components/AccountSwitcher.d.ts.map +1 -1
  142. package/lib/typescript/module/ui/components/ProfileMenu.d.ts +12 -9
  143. package/lib/typescript/module/ui/components/ProfileMenu.d.ts.map +1 -1
  144. package/lib/typescript/module/ui/context/OxyContext.d.ts +16 -13
  145. package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
  146. package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts +27 -3
  147. package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  148. package/lib/typescript/module/ui/context/inSessionTokenRefresh.d.ts +22 -17
  149. package/lib/typescript/module/ui/context/inSessionTokenRefresh.d.ts.map +1 -1
  150. package/lib/typescript/module/ui/context/silentSessionRestore.d.ts +12 -18
  151. package/lib/typescript/module/ui/context/silentSessionRestore.d.ts.map +1 -1
  152. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts +1 -0
  153. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  154. package/lib/typescript/module/ui/hooks/useAuth.d.ts +1 -1
  155. package/lib/typescript/module/ui/hooks/useAuth.d.ts.map +1 -1
  156. package/lib/typescript/module/ui/hooks/useOxyAuthSession.d.ts +2 -2
  157. package/lib/typescript/module/ui/hooks/useOxyAuthSession.d.ts.map +1 -1
  158. package/lib/typescript/module/ui/hooks/useSessionManagement.d.ts.map +1 -1
  159. package/lib/typescript/module/ui/hooks/useSwitchableAccounts.d.ts +162 -0
  160. package/lib/typescript/module/ui/hooks/useSwitchableAccounts.d.ts.map +1 -0
  161. package/lib/typescript/module/ui/hooks/useWebSSO.d.ts +13 -52
  162. package/lib/typescript/module/ui/hooks/useWebSSO.d.ts.map +1 -1
  163. package/lib/typescript/module/ui/session/createSessionClient.d.ts +15 -0
  164. package/lib/typescript/module/ui/session/createSessionClient.d.ts.map +1 -0
  165. package/lib/typescript/module/ui/session/index.d.ts +15 -0
  166. package/lib/typescript/module/ui/session/index.d.ts.map +1 -0
  167. package/lib/typescript/module/ui/session/projectSessionState.d.ts +45 -0
  168. package/lib/typescript/module/ui/session/projectSessionState.d.ts.map +1 -0
  169. package/lib/typescript/module/ui/session/sessionClientHost.d.ts +14 -0
  170. package/lib/typescript/module/ui/session/sessionClientHost.d.ts.map +1 -0
  171. package/lib/typescript/module/ui/session/tokenTransport.d.ts +24 -0
  172. package/lib/typescript/module/ui/session/tokenTransport.d.ts.map +1 -0
  173. package/lib/typescript/module/ui/utils/activeAuthuser.d.ts +21 -29
  174. package/lib/typescript/module/ui/utils/activeAuthuser.d.ts.map +1 -1
  175. package/lib/typescript/module/ui/utils/sessionHelpers.d.ts +0 -19
  176. package/lib/typescript/module/ui/utils/sessionHelpers.d.ts.map +1 -1
  177. package/lib/typescript/module/utils/deviceFlowSignIn.d.ts +51 -19
  178. package/lib/typescript/module/utils/deviceFlowSignIn.d.ts.map +1 -1
  179. package/package.json +4 -4
  180. package/src/index.ts +9 -8
  181. package/src/ui/components/AccountMenu.tsx +117 -70
  182. package/src/ui/components/AccountSwitcher.tsx +88 -134
  183. package/src/ui/components/ProfileMenu.tsx +158 -162
  184. package/src/ui/components/SignInModal.tsx +5 -5
  185. package/src/ui/context/OxyContext.tsx +560 -475
  186. package/src/ui/context/hooks/useAuthOperations.ts +72 -63
  187. package/src/ui/context/inSessionTokenRefresh.ts +23 -42
  188. package/src/ui/context/silentSessionRestore.ts +12 -26
  189. package/src/ui/hooks/useAuth.ts +4 -3
  190. package/src/ui/hooks/useOxyAuthSession.ts +13 -8
  191. package/src/ui/hooks/useSessionManagement.ts +16 -87
  192. package/src/ui/hooks/useSwitchableAccounts.ts +397 -0
  193. package/src/ui/hooks/useWebSSO.ts +13 -227
  194. package/src/ui/screens/OxyAuthScreen.tsx +2 -2
  195. package/src/ui/session/__tests__/createSessionClient.test.ts +41 -0
  196. package/src/ui/session/__tests__/projectSessionState.test.ts +114 -0
  197. package/src/ui/session/__tests__/sessionClientHost.test.ts +43 -0
  198. package/src/ui/session/__tests__/tokenTransport.test.ts +142 -0
  199. package/src/ui/session/createSessionClient.ts +21 -0
  200. package/src/ui/session/index.ts +19 -0
  201. package/src/ui/session/projectSessionState.ts +85 -0
  202. package/src/ui/session/sessionClientHost.ts +27 -0
  203. package/src/ui/session/tokenTransport.ts +76 -0
  204. package/src/ui/utils/activeAuthuser.ts +34 -76
  205. package/src/ui/utils/sessionHelpers.ts +0 -66
  206. package/src/utils/__tests__/deviceFlowSignIn.test.ts +243 -34
  207. package/src/utils/deviceFlowSignIn.ts +98 -21
  208. package/lib/commonjs/ui/components/accountMenuRows.js +0 -33
  209. package/lib/commonjs/ui/components/accountMenuRows.js.map +0 -1
  210. package/lib/commonjs/ui/hooks/useDeviceAccounts.js +0 -280
  211. package/lib/commonjs/ui/hooks/useDeviceAccounts.js.map +0 -1
  212. package/lib/commonjs/ui/hooks/useSessionSocket.js +0 -203
  213. package/lib/commonjs/ui/hooks/useSessionSocket.js.map +0 -1
  214. package/lib/commonjs/utils/silentGuardKey.js +0 -54
  215. package/lib/commonjs/utils/silentGuardKey.js.map +0 -1
  216. package/lib/module/ui/components/accountMenuRows.js +0 -29
  217. package/lib/module/ui/components/accountMenuRows.js.map +0 -1
  218. package/lib/module/ui/hooks/useDeviceAccounts.js +0 -276
  219. package/lib/module/ui/hooks/useDeviceAccounts.js.map +0 -1
  220. package/lib/module/ui/hooks/useSessionSocket.js +0 -199
  221. package/lib/module/ui/hooks/useSessionSocket.js.map +0 -1
  222. package/lib/module/utils/silentGuardKey.js +0 -49
  223. package/lib/module/utils/silentGuardKey.js.map +0 -1
  224. package/lib/typescript/commonjs/ui/components/accountMenuRows.d.ts +0 -34
  225. package/lib/typescript/commonjs/ui/components/accountMenuRows.d.ts.map +0 -1
  226. package/lib/typescript/commonjs/ui/hooks/useDeviceAccounts.d.ts +0 -133
  227. package/lib/typescript/commonjs/ui/hooks/useDeviceAccounts.d.ts.map +0 -1
  228. package/lib/typescript/commonjs/ui/hooks/useSessionSocket.d.ts +0 -14
  229. package/lib/typescript/commonjs/ui/hooks/useSessionSocket.d.ts.map +0 -1
  230. package/lib/typescript/commonjs/utils/silentGuardKey.d.ts +0 -31
  231. package/lib/typescript/commonjs/utils/silentGuardKey.d.ts.map +0 -1
  232. package/lib/typescript/module/ui/components/accountMenuRows.d.ts +0 -34
  233. package/lib/typescript/module/ui/components/accountMenuRows.d.ts.map +0 -1
  234. package/lib/typescript/module/ui/hooks/useDeviceAccounts.d.ts +0 -133
  235. package/lib/typescript/module/ui/hooks/useDeviceAccounts.d.ts.map +0 -1
  236. package/lib/typescript/module/ui/hooks/useSessionSocket.d.ts +0 -14
  237. package/lib/typescript/module/ui/hooks/useSessionSocket.d.ts.map +0 -1
  238. package/lib/typescript/module/utils/silentGuardKey.d.ts +0 -31
  239. package/lib/typescript/module/utils/silentGuardKey.d.ts.map +0 -1
  240. package/src/ui/components/accountMenuRows.ts +0 -48
  241. package/src/ui/hooks/useDeviceAccounts.ts +0 -344
  242. package/src/ui/hooks/useSessionSocket.ts +0 -233
  243. package/src/ui/utils/__tests__/activeAuthuser.test.ts +0 -75
  244. package/src/utils/__tests__/silentGuardKey.test.ts +0 -82
  245. package/src/utils/silentGuardKey.ts +0 -46
@@ -1,5 +1,5 @@
1
- import { useCallback, useRef } from 'react';
2
- import type { ApiError, User } from '@oxyhq/core';
1
+ import { useCallback } from 'react';
2
+ import type { ApiError, SessionClient, User } from '@oxyhq/core';
3
3
  import type { AuthState } from '../../stores/authStore';
4
4
  import type { ClientSession, SessionLoginResponse } from '@oxyhq/core';
5
5
  import { DeviceManager } from '@oxyhq/core';
@@ -10,9 +10,7 @@ import type { OxyServices } from '@oxyhq/core';
10
10
  import { SignatureService } from '@oxyhq/core';
11
11
  import { isWebBrowser } from '../../hooks/useWebSSO';
12
12
  import {
13
- clearActiveAuthuser,
14
13
  clearSsoBounceState,
15
- writeActiveAuthuser,
16
14
  markSignedOut,
17
15
  clearSignedOut,
18
16
  } from '../../utils/activeAuthuser';
@@ -21,7 +19,6 @@ import { isCrossApexWeb, CrossApexDirectSignInError } from '../../../utils/cross
21
19
  export interface UseAuthOperationsOptions {
22
20
  oxyServices: OxyServices;
23
21
  storage: StorageInterface | null;
24
- sessions: ClientSession[];
25
22
  activeSessionId: string | null;
26
23
  setActiveSessionId: (sessionId: string | null) => void;
27
24
  updateSessions: (sessions: ClientSession[], options?: { merge?: boolean }) => void;
@@ -35,7 +32,17 @@ export interface UseAuthOperationsOptions {
35
32
  * expired session still recovers via a returning-user bounce. Best-effort.
36
33
  */
37
34
  clearPriorSessionHint: () => Promise<void>;
35
+ /** Used only by `performSignIn`'s same-user duplicate-session dedup (legacy session-validate path; unrelated to the SessionClient device-account set). */
38
36
  switchSession: (sessionId: string) => Promise<User>;
37
+ /**
38
+ * The Fase 3-A/3-B `SessionClient` (server-authoritative device account
39
+ * set). `logout` / `logoutAll` route SERVER-side revocation through
40
+ * `sessionClient.signOut(...)` instead of the bearer/cookie logout
41
+ * endpoints.
42
+ */
43
+ sessionClient: SessionClient;
44
+ /** Reprojects `sessionClient.getState()` onto sessions/activeSessionId/user (Task 1's callback). Awaited after a partial `signOut` so the exposed state reflects the server truth before the call resolves. */
45
+ syncFromClient: () => Promise<void>;
39
46
  applyLanguagePreference: (user: User) => Promise<void>;
40
47
  onAuthStateChange?: (user: User | null) => void;
41
48
  onError?: (error: ApiError) => void;
@@ -67,8 +74,13 @@ const LOGOUT_ALL_ERROR_CODE = 'LOGOUT_ALL_ERROR';
67
74
  * best-effort storage write. The clear is invoked synchronously (so unit tests
68
75
  * can assert it ran) but its async settle is detached; any rejection is logged,
69
76
  * never thrown.
77
+ *
78
+ * Exported so `OxyContext`'s `syncFromClient` zero-account branch (a REMOTE
79
+ * full sign-out) can invoke the EXACT same cleanup as the LOCAL `logout` /
80
+ * `logoutAll` paths below — a remote sign-out must be indistinguishable from
81
+ * a local one to the next cold boot.
70
82
  */
71
- function clearPriorSessionHintSafe(
83
+ export function clearPriorSessionHintSafe(
72
84
  clearPriorSessionHint: () => Promise<void>,
73
85
  logger?: (message: string, error?: unknown) => void,
74
86
  ): void {
@@ -83,7 +95,6 @@ function clearPriorSessionHintSafe(
83
95
  */
84
96
  export const useAuthOperations = ({
85
97
  oxyServices,
86
- sessions,
87
98
  activeSessionId,
88
99
  setActiveSessionId,
89
100
  updateSessions,
@@ -91,6 +102,8 @@ export const useAuthOperations = ({
91
102
  clearSessionState,
92
103
  clearPriorSessionHint,
93
104
  switchSession,
105
+ sessionClient,
106
+ syncFromClient,
94
107
  applyLanguagePreference,
95
108
  onAuthStateChange,
96
109
  onError,
@@ -100,10 +113,6 @@ export const useAuthOperations = ({
100
113
  setAuthState,
101
114
  logger,
102
115
  }: UseAuthOperationsOptions): UseAuthOperationsResult => {
103
- // Ref to avoid recreating callbacks when sessions change
104
- const sessionsRef = useRef(sessions);
105
- sessionsRef.current = sessions;
106
-
107
116
  /**
108
117
  * Internal function to perform challenge-response sign in.
109
118
  */
@@ -141,19 +150,22 @@ export const useAuthOperations = ({
141
150
 
142
151
  // Deliberate sign-in re-enables automatic silent restore: clear the durable
143
152
  // "deliberately signed out" flag so a prior sign-out no longer suppresses
144
- // the `fedcm-silent` / per-apex iframe cold-boot steps.
153
+ // the per-apex iframe cold-boot step.
145
154
  clearSignedOut();
146
155
 
147
- // Register this primary session in the device's first-party multi-account
148
- // refresh-cookie set so a web key-based sign-in survives reload via
149
- // `refresh-all` and coexists with switched accounts. `/auth/verify` mints the
150
- // session but (unlike `/auth/login` / `/fedcm/exchange`) does NOT plant an
151
- // `oxy_rt_<authuser>` slot, so we establish it here through the shared
152
- // `POST /auth/session` primitive. No-op on native and cross-apex (returns
153
- // `null`); records the active slot when one is genuinely allocated.
154
- const verifyAuthuser = await oxyServices.establishDeviceRefreshSlot();
155
- if (typeof verifyAuthuser === 'number') {
156
- writeActiveAuthuser(verifyAuthuser);
156
+ // Register this recovered account+session into the server-authoritative
157
+ // device-session set: `sessionClient.addCurrentAccount()` ->
158
+ // `POST /session/device/add` derives identity from the bearer
159
+ // `verifyChallenge` already planted internally, then `syncFromClient()`
160
+ // reprojects the resulting server state onto the exposed
161
+ // sessions/activeSessionId/user. Best-effort: a failure here must NEVER
162
+ // fail the sign-in itself cold boot re-registers this account into the
163
+ // device set on the next load regardless.
164
+ try {
165
+ await sessionClient.addCurrentAccount();
166
+ await syncFromClient();
167
+ } catch (registrationError) {
168
+ logger?.('Failed to register sign-in into device session set', registrationError);
157
169
  }
158
170
 
159
171
  // Get full user data
@@ -216,8 +228,10 @@ export const useAuthOperations = ({
216
228
  onAuthStateChange,
217
229
  oxyServices,
218
230
  saveActiveSessionId,
231
+ sessionClient,
219
232
  setActiveSessionId,
220
233
  switchSession,
234
+ syncFromClient,
221
235
  updateSessions,
222
236
  ],
223
237
  );
@@ -264,38 +278,37 @@ export const useAuthOperations = ({
264
278
 
265
279
  try {
266
280
  const sessionToLogout = targetSessionId || activeSessionId;
267
- // Web multi-account sessions carry an `authuser` slot index backed by
268
- // an httpOnly `oxy_rt_${n}` cookie. Native sessions fall through to the
269
- // bearer-protected endpoint.
270
- const targetSession = sessionsRef.current.find((s) => s.sessionId === sessionToLogout);
271
- const targetAuthuser = targetSession?.authuser;
272
- if (isWebBrowser() && typeof targetAuthuser === 'number') {
273
- await oxyServices.logoutSessionByAuthuser(targetAuthuser);
274
- } else {
275
- await oxyServices.logoutSession(activeSessionId, sessionToLogout);
281
+
282
+ // Resolve the device account backing this session from the
283
+ // server-authoritative `SessionClient` state — SERVER revocation now
284
+ // goes through `sessionClient.signOut(...)` instead of the
285
+ // bearer/cookie logout endpoints.
286
+ const targetAccountId = sessionClient
287
+ .getState()
288
+ ?.accounts.find((account) => account.sessionId === sessionToLogout)?.accountId;
289
+ if (!targetAccountId) {
290
+ throw new Error(`No device account found for session "${sessionToLogout}"`);
276
291
  }
277
292
 
278
- const filteredSessions = sessionsRef.current.filter((session) => session.sessionId !== sessionToLogout);
279
- updateSessions(filteredSessions, { merge: false });
293
+ await sessionClient.signOut({ accountId: targetAccountId });
280
294
 
281
- if (sessionToLogout === activeSessionId) {
282
- if (filteredSessions.length > 0) {
283
- await switchSession(filteredSessions[0].sessionId);
284
- } else {
285
- // Genuine FULL sign-out (no sessions remain): clear the per-origin
286
- // SSO bounce state so a fresh deliberate sign-in can re-probe, drop
287
- // the persisted active device slot so the next cold boot does not
288
- // prioritize a now-signed-out `oxy_active_authuser`, and SET the
289
- // deliberately-signed-out flag so the silent cold-boot steps
290
- // (`fedcm-silent` / per-apex iframe) do not re-mint a session from a
291
- // still-live IdP session on the next reload (mirrors `logoutAll`).
292
- markSignedOut();
293
- clearActiveAuthuser();
294
- clearSsoBounceState();
295
- clearPriorSessionHintSafe(clearPriorSessionHint, logger);
296
- await clearSessionState();
297
- return;
298
- }
295
+ // The server has already decided what (if anything) remains active on
296
+ // this device; reproject that truth onto the exposed sessions /
297
+ // activeSessionId / user before deciding whether additional LOCAL
298
+ // teardown is needed.
299
+ const remainingAccounts = sessionClient.getState()?.accounts ?? [];
300
+ await syncFromClient();
301
+
302
+ if (sessionToLogout === activeSessionId && remainingAccounts.length === 0) {
303
+ // Genuine FULL sign-out (no sessions remain): clear the per-origin
304
+ // SSO bounce state so a fresh deliberate sign-in can re-probe, and
305
+ // SET the deliberately-signed-out flag so the silent cold-boot step
306
+ // (per-apex iframe) does not re-mint a session from a
307
+ // still-live IdP session on the next reload (mirrors `logoutAll`).
308
+ markSignedOut();
309
+ clearSsoBounceState();
310
+ clearPriorSessionHintSafe(clearPriorSessionHint, logger);
311
+ await clearSessionState();
299
312
  }
300
313
  } catch (error) {
301
314
  const isInvalid = isInvalidSessionError(error);
@@ -324,10 +337,9 @@ export const useAuthOperations = ({
324
337
  clearPriorSessionHint,
325
338
  logger,
326
339
  onError,
327
- oxyServices,
340
+ sessionClient,
328
341
  setAuthState,
329
- switchSession,
330
- updateSessions,
342
+ syncFromClient,
331
343
  ],
332
344
  );
333
345
 
@@ -343,15 +355,12 @@ export const useAuthOperations = ({
343
355
  }
344
356
 
345
357
  try {
346
- // Always invoke the bearer-protected global endpoint first: the public
347
- // `logoutAll`/`signOutAll` contract means revoking this user's sessions
348
- // across devices, not just clearing refresh cookies presented by the
349
- // current browser. On web, follow up with the cookie endpoint so every
350
- // device-local account slot is expired in the browser as well.
351
- await oxyServices.logoutAllSessions(activeSessionId);
358
+ // Server-side revocation of every account on this device now flows
359
+ // through the SessionClient (`POST /session/device/signout` with
360
+ // `{ all: true }`) replaces the bearer `logoutAllSessions` +
361
+ // web-cookie `logoutAllSessionsViaCookie` pair.
362
+ await sessionClient.signOut({ all: true });
352
363
  if (isWebBrowser()) {
353
- await oxyServices.logoutAllSessionsViaCookie();
354
- clearActiveAuthuser();
355
364
  // Deliberate full sign-out: suppress automatic silent restore on the next
356
365
  // cold boot so a still-live IdP session does not re-mint a session.
357
366
  markSignedOut();
@@ -373,7 +382,7 @@ export const useAuthOperations = ({
373
382
  });
374
383
  throw error instanceof Error ? error : new Error('Logout all failed');
375
384
  }
376
- }, [activeSessionId, clearSessionState, clearPriorSessionHint, logger, onError, oxyServices, setAuthState]);
385
+ }, [activeSessionId, clearSessionState, clearPriorSessionHint, logger, onError, sessionClient, setAuthState]);
377
386
 
378
387
  return {
379
388
  signIn,
@@ -15,10 +15,10 @@
15
15
  *
16
16
  * 1. {@link createInSessionRefreshHandler} — an `AuthRefreshHandler` installed
17
17
  * on the owner client. It re-mints a fresh access token WITHOUT a page
18
- * reload by composing the SAME silent-restore primitives cold boot uses
19
- * ({@link mintSessionViaPerApexIframe}, {@link selectActiveRefreshAccount})
20
- * not a copy. The linked client (`createLinkedClient`) inherits the fix for
21
- * free: its refresh delegates back to the owner's `refreshAccessToken`.
18
+ * reload by composing the SAME silent-restore primitive cold boot uses
19
+ * ({@link mintSessionViaPerApexIframe}) not a copy. The linked client
20
+ * (`createLinkedClient`) inherits the fix for free: its refresh delegates
21
+ * back to the owner's `refreshAccessToken`.
22
22
  *
23
23
  * 2. {@link startTokenRefreshScheduler} — a proactive scheduler that refreshes
24
24
  * ~{@link TOKEN_REFRESH_LEAD_MS} before expiry (and on web tab-focus / native
@@ -29,6 +29,14 @@
29
29
  * (single in-flight `tokenRefreshPromise` + cooldown) — this module does not
30
30
  * reimplement them, so the timer / foreground / per-request triggers collapse to
31
31
  * one network attempt (no refresh storm).
32
+ *
33
+ * NOTE (session-sync cutover, Task 5): the web arm chain used to fall through
34
+ * to a same-apex `oxy_rt` refresh-cookie arm (`refreshAllSessions` +
35
+ * `selectActiveRefreshAccount`). That arm is DELETED — the device account set
36
+ * is now server-authoritative via `SessionClient` (`@oxyhq/core`), which
37
+ * `OxyContext` bootstraps/reprojects independently of in-session token
38
+ * refresh. `selectActiveRefreshAccount` had no other caller and was deleted
39
+ * with it.
32
40
  */
33
41
  import type {
34
42
  OxyServices,
@@ -38,8 +46,7 @@ import type {
38
46
  import { logger as loggerUtil } from '@oxyhq/core';
39
47
  import { AppState, type AppStateStatus } from 'react-native';
40
48
  import { isWebBrowser } from '../hooks/useWebSSO';
41
- import { readActiveAuthuser } from '../utils/activeAuthuser';
42
- import { mintSessionViaPerApexIframe, selectActiveRefreshAccount } from './silentSessionRestore';
49
+ import { mintSessionViaPerApexIframe } from './silentSessionRestore';
43
50
 
44
51
  /**
45
52
  * Per-arm fail-fast budget (ms) for the web first-party `/auth/silent` iframe
@@ -51,14 +58,6 @@ import { mintSessionViaPerApexIframe, selectActiveRefreshAccount } from './silen
51
58
  */
52
59
  const SILENT_IFRAME_REFRESH_TIMEOUT = 4000;
53
60
 
54
- /**
55
- * Per-arm fail-fast budget (ms) for the same-apex refresh-cookie arm
56
- * (`refreshAllSessions`). On a cross-apex RP the `Domain=oxy.so` cookie never
57
- * reaches `api.<apex>`, so this returns `{accounts:[]}` quickly; the bound only
58
- * matters if that endpoint stalls.
59
- */
60
- const COOKIE_REFRESH_TIMEOUT = 4000;
61
-
62
61
  /**
63
62
  * Lead time (ms) before access-token expiry at which the proactive scheduler
64
63
  * refreshes. Mirrors `HttpService`'s per-request `TOKEN_REFRESH_LEAD_SECONDS`
@@ -90,22 +89,19 @@ type RefreshArm = () => Promise<string | null>;
90
89
  * sign-in) so a genuinely dead session reconciles to logged-out rather than
91
90
  * staying a zombie.
92
91
  *
93
- * WEB, in order:
94
- * 1. Per-apex `/auth/silent` iframe — {@link mintSessionViaPerApexIframe}
95
- * (shared verbatim with cold boot). The durable cross-apex path; also
96
- * covers `*.oxy.so` (the per-apex host IS the central host there).
97
- * 2. FedCM silent re-auth (Chrome) `silentSignInWithFedCM`.
98
- * 3. Same-apex refresh cookie `refreshAllSessions` + the shared
99
- * {@link selectActiveRefreshAccount}; plant the active account's rotated
100
- * token. On a cross-apex RP it returns `{accounts:[]}` (clean no-op).
101
- * Unlike the cold-boot cookie restore this does NOT rebuild multi-session
102
- * state — an in-session refresh only needs a fresh bearer.
92
+ * WEB: Per-apex `/auth/silent` iframe — {@link mintSessionViaPerApexIframe}
93
+ * (shared verbatim with cold boot). The durable cross-apex path; also
94
+ * covers `*.oxy.so` (the per-apex host IS the central host there). There
95
+ * is no FedCM arm FedCM was removed from the client sign-in/refresh path
96
+ * entirely (Chrome-only; see `CrossDomainAuth`'s doc comment in
97
+ * `@oxyhq/core` for the production sign-in loop that motivated the
98
+ * removal).
103
99
  *
104
100
  * NO RECURSION: no arm issues a request through the authed client's
105
- * `refreshAccessToken` path. The iframe/FedCM transports are postMessage /
106
- * credential APIs; the follow-up `/session/user` fetch inside `silentSignIn`
101
+ * `refreshAccessToken` path. The iframe transport is postMessage-based; the
102
+ * follow-up `/session/user` fetch inside `silentSignIn`
107
103
  * runs against the just-planted FULL-TTL token (≫ the preflight lead), so it
108
- * never re-enters the refresh path; `refreshAllSessions` uses a raw `fetch`.
104
+ * never re-enters the refresh path.
109
105
  */
110
106
  export function createInSessionRefreshHandler(oxyServices: OxyServices): AuthRefreshHandler {
111
107
  const runArm = async (label: string, reason: AuthRefreshReason, arm: RefreshArm): Promise<string | null> => {
@@ -129,21 +125,6 @@ export function createInSessionRefreshHandler(oxyServices: OxyServices): AuthRef
129
125
  ? oxyServices.getAccessToken()
130
126
  : null,
131
127
  ],
132
- ['fedcm-silent', async () => {
133
- if (oxyServices.isFedCMSupported?.() !== true) {
134
- return null;
135
- }
136
- return (await oxyServices.silentSignInWithFedCM?.()) ? oxyServices.getAccessToken() : null;
137
- }],
138
- ['refresh-cookie', async () => {
139
- const snapshot = await oxyServices.refreshAllSessions({ timeout: COOKIE_REFRESH_TIMEOUT });
140
- if (snapshot.accounts.length === 0) {
141
- return null;
142
- }
143
- const active = selectActiveRefreshAccount(snapshot.accounts, readActiveAuthuser());
144
- oxyServices.httpService.setTokens(active.accessToken);
145
- return oxyServices.getAccessToken();
146
- }],
147
128
  ];
148
129
 
149
130
  return async (reason: AuthRefreshReason): Promise<string | null> => {
@@ -1,14 +1,20 @@
1
1
  /**
2
- * Silent, no-reload session-restore PRIMITIVES — the single shared
2
+ * Silent, no-reload session-restore PRIMITIVE — the single shared
3
3
  * implementation used by BOTH cold boot (`OxyContext.restoreSessionsFromStorage`)
4
4
  * and in-session token refresh (`createInSessionRefreshHandler`). Neither caller
5
- * re-implements these; they compose them. Keeping one home avoids the two paths
5
+ * re-implements it; they compose it. Keeping one home avoids the two paths
6
6
  * drifting on "how do we mint a first-party token without a page reload".
7
7
  *
8
- * Each function is platform-agnostic at the type level and returns plain data;
9
- * the callers own the side effects that differ between them (cold boot COMMITS
10
- * the recovered session into provider state; refresh only reads the freshly
11
- * planted bearer).
8
+ * Platform-agnostic at the type level and returns plain data; the callers own
9
+ * the side effects that differ between them (cold boot COMMITS the recovered
10
+ * session into provider state; refresh only reads the freshly planted bearer).
11
+ *
12
+ * NOTE (session-sync cutover, Task 5): this file used to also export
13
+ * `selectActiveRefreshAccount`, which picked the active account out of a
14
+ * `refreshAllSessions` (`oxy_rt` refresh-cookie) snapshot. Deleted — its only
15
+ * caller was the in-session refresh handler's now-deleted `refresh-cookie`
16
+ * arm. The device account SET is server-authoritative via `SessionClient`
17
+ * (`@oxyhq/core`) now.
12
18
  */
13
19
  import type { OxyServices, SessionLoginResponse } from '@oxyhq/core';
14
20
  import { autoDetectAuthWebUrl } from '@oxyhq/core';
@@ -45,23 +51,3 @@ export async function mintSessionViaPerApexIframe(
45
51
  }
46
52
  return session;
47
53
  }
48
-
49
- /**
50
- * Pick the active account from a `refreshAllSessions` snapshot: the persisted
51
- * `authuser` slot when it still matches a returned account, otherwise the lowest
52
- * `authuser` (the server sorts ascending, so `[0]`). Callers guarantee a
53
- * non-empty list, so the result is always defined.
54
- *
55
- * Shared by cold-boot cookie restore and the in-session refresh cookie arm so
56
- * the active-slot selection can never diverge between them.
57
- */
58
- export function selectActiveRefreshAccount<T extends { authuser: number }>(
59
- accounts: T[],
60
- persistedAuthuser: number | null,
61
- ): T {
62
- const matched =
63
- persistedAuthuser !== null
64
- ? accounts.find((account) => account.authuser === persistedAuthuser)
65
- : undefined;
66
- return matched ?? accounts[0];
67
- }
@@ -18,7 +18,7 @@
18
18
  * ```
19
19
  *
20
20
  * Cross-domain SSO:
21
- * - Web: Automatic via FedCM (Chrome 108+, Safari 16.4+)
21
+ * - Web: Automatic via the per-apex `/auth/silent` iframe + terminal `/sso` bounce (SDK cold boot)
22
22
  * - Native: Automatic via shared Keychain/Account Manager
23
23
  * - Manual sign-in: signIn() redirects to the IdP (web) or opens the auth sheet (native)
24
24
  */
@@ -150,8 +150,9 @@ export function useAuth(): UseAuthReturn {
150
150
  const isIdentityProvider = isWebBrowser() &&
151
151
  window.location.hostname === idpHostname;
152
152
 
153
- // Web (not on IdP): use the tokenless redirect SSO flow. FedCM / silent SSO
154
- // already run on page load; an explicit click needs interactive auth.
153
+ // Web (not on IdP): use the tokenless redirect SSO flow. The silent
154
+ // cross-domain restore (per-apex `/auth/silent` iframe + `/sso` bounce)
155
+ // already ran on page load; an explicit click needs interactive auth.
155
156
  if (isWebBrowser() && !publicKey && !isIdentityProvider) {
156
157
  oxyServices.signInWithRedirect?.({
157
158
  redirectUri: window.location.href,
@@ -23,7 +23,7 @@
23
23
  import { useState, useEffect, useCallback, useRef } from 'react';
24
24
  import { Linking, Platform } from 'react-native';
25
25
  import io, { type Socket } from 'socket.io-client';
26
- import type { OxyServices, User } from '@oxyhq/core';
26
+ import type { OxyServices, SessionLoginResponse, User } from '@oxyhq/core';
27
27
  import { createDebugLogger } from '@oxyhq/core';
28
28
  import { completeDeviceFlowSignIn } from '../../utils/deviceFlowSignIn';
29
29
 
@@ -232,7 +232,7 @@ function generateSessionToken(): string {
232
232
  export function useOxyAuthSession(
233
233
  oxyServices: OxyServices,
234
234
  clientId: string | null,
235
- switchSession: ((sessionId: string) => Promise<User>) | undefined,
235
+ commitSession: ((session: SessionLoginResponse) => Promise<void>) | undefined,
236
236
  options: UseOxyAuthSessionOptions = {},
237
237
  ): UseOxyAuthSessionResult {
238
238
  const { onSignedIn } = options;
@@ -284,8 +284,13 @@ export function useOxyAuthSession(
284
284
  // Without that exchange the SDK has no bearer token — the session is
285
285
  // authorized server-side but the app never becomes authenticated and the UI
286
286
  // sits "Waiting for authorization..." forever. Once `claimSessionByToken`
287
- // plants the tokens in the HttpService, the rest of the session wiring flows
288
- // through the normal `switchSession` path. Shared with both containers via
287
+ // plants the tokens in the HttpService, the claimed session is committed via
288
+ // `commitSession` (`useOxy().handleWebSession`) the SAME path a fresh
289
+ // password sign-in or a silent cross-domain restore uses to register the
290
+ // account into the device's server-authoritative session set. It is NOT yet a
291
+ // member of that set, so
292
+ // `switchSession` (an account-SWITCH between accounts already on the
293
+ // device) is the wrong primitive here. Shared with both containers via
289
294
  // `completeDeviceFlowSignIn` so the two paths cannot drift.
290
295
  const handleAuthSuccess = useCallback(
291
296
  async (sessionId: string, sessionToken: string) => {
@@ -293,14 +298,14 @@ export function useOxyAuthSession(
293
298
  isProcessingRef.current = true;
294
299
 
295
300
  try {
296
- if (!switchSession) {
297
- throw new Error('Session management unavailable');
301
+ if (!commitSession) {
302
+ throw new Error('Session commit unavailable');
298
303
  }
299
304
  const user = await completeDeviceFlowSignIn({
300
305
  oxyServices,
301
306
  sessionId,
302
307
  sessionToken,
303
- switchSession,
308
+ commitSession,
304
309
  });
305
310
  onSignedInRef.current?.(user);
306
311
  } catch (err) {
@@ -309,7 +314,7 @@ export function useOxyAuthSession(
309
314
  isProcessingRef.current = false;
310
315
  }
311
316
  },
312
- [oxyServices, switchSession],
317
+ [oxyServices, commitSession],
313
318
  );
314
319
 
315
320
  // Start polling for authorization.
@@ -2,14 +2,13 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
2
  import { isDev, type ApiError, type User } from '@oxyhq/core';
3
3
  import type { ClientSession } from '@oxyhq/core';
4
4
  import { mergeSessions, normalizeAndSortSessions, sessionsArraysEqual } from '@oxyhq/core';
5
- import { fetchSessionsWithFallback, validateSessionBatch } from '../utils/sessionHelpers';
5
+ import { fetchSessionsWithFallback } from '../utils/sessionHelpers';
6
6
  import { getStorageKeys, type StorageInterface } from '../utils/storageHelpers';
7
7
  import { handleAuthError, isInvalidSessionError } from '../utils/errorHandlers';
8
8
  import type { OxyServices } from '@oxyhq/core';
9
9
  import type { QueryClient } from '@tanstack/react-query';
10
10
  import { clearQueryCache } from './queryClient';
11
11
  import { isWebBrowser } from './useWebSSO';
12
- import { writeActiveAuthuser } from '../utils/activeAuthuser';
13
12
 
14
13
  export interface UseSessionManagementOptions {
15
14
  oxyServices: OxyServices;
@@ -229,68 +228,18 @@ export const useSessionManagement = ({
229
228
  };
230
229
  }, []);
231
230
 
232
- const findReplacementSession = useCallback(
233
- async (sessionIds: string[]): Promise<User | null> => {
234
- if (!sessionIds.length) {
235
- return null;
236
- }
237
-
238
- const validationResults = await validateSessionBatch(oxyServices, sessionIds, {
239
- maxConcurrency: 3,
240
- });
241
-
242
- const validSession = validationResults.find((result) => result.valid);
243
- if (!validSession) {
244
- return null;
245
- }
246
-
247
- const validation = await oxyServices.validateSession(validSession.sessionId, {
248
- useHeaderValidation: true,
249
- });
250
-
251
- if (!validation?.valid || !validation.user) {
252
- return null;
253
- }
254
-
255
- const user = validation.user as User;
256
- await activateSession(validSession.sessionId, user);
257
- return user;
258
- },
259
- [activateSession, oxyServices],
260
- );
261
-
262
231
  const switchSession = useCallback(
263
232
  async (sessionId: string): Promise<User> => {
264
233
  try {
265
- // Web multi-account: when the target session was sourced from
266
- // `refreshAllSessions` it carries its `authuser` slot index. We
267
- // proactively plant that slot's access token via the httpOnly
268
- // refresh cookie BEFORE validating, so the bearer-protected
269
- // validate/getCurrentUser calls have the correct in-memory token
270
- // after a cold reload or account switch in another tab. The native
271
- // path arrives here only after a bearer has been planted by
234
+ // On web, the bearer must already be in memory (planted by
235
+ // `claimSessionByToken`, a cold-boot silent-restore step, or a prior
236
+ // `SessionClient` sync) before validating there is no client-side
237
+ // `oxy_rt` refresh-cookie slot to fall back on; the device account SET
238
+ // is server-authoritative via `SessionClient` now. The native path
239
+ // arrives here only after a bearer has been planted by
272
240
  // `claimSessionByToken` or secure shared-session restore.
273
- if (isWebBrowser()) {
274
- const targetSession = sessionsRef.current.find((s) => s.sessionId === sessionId);
275
- const targetAuthuser = targetSession?.authuser;
276
- if (typeof targetAuthuser === 'number') {
277
- const refreshed = await oxyServices.refreshTokenViaCookie({ authuser: targetAuthuser });
278
- if (refreshed === null) {
279
- // Slot's refresh cookie is missing / expired / reused. Fall
280
- // through to the invalid-session branch below by throwing the
281
- // canonical invalid-session error.
282
- throw new Error('Session is invalid or expired');
283
- }
284
- // Plant the slot's fresh access token; subsequent bearer calls
285
- // (`validateSession`, `getCurrentUser`) will use it. The server
286
- // also rotated the cookie at this point.
287
- oxyServices.httpService.setTokens(refreshed.accessToken);
288
- writeActiveAuthuser(targetAuthuser);
289
- }
290
-
291
- if (!oxyServices.getAccessToken()) {
292
- throw new Error('Session is invalid or expired');
293
- }
241
+ if (isWebBrowser() && !oxyServices.getAccessToken()) {
242
+ throw new Error('Session is invalid or expired');
294
243
  }
295
244
 
296
245
  const validation = await oxyServices.validateSession(sessionId, { useHeaderValidation: true });
@@ -322,22 +271,12 @@ export const useSessionManagement = ({
322
271
  const invalidSession = isInvalidSessionError(error);
323
272
 
324
273
  if (invalidSession) {
274
+ // Server authority (`SessionClient` bootstrap/sync) reconciles which
275
+ // account is actually active — just drop the invalid session from
276
+ // the local view rather than guessing at a replacement.
325
277
  updateSessions(sessionsRef.current.filter((session) => session.sessionId !== sessionId), {
326
278
  merge: false,
327
279
  });
328
- if (sessionId === activeSessionIdRef.current) {
329
- const otherSessionIds = sessionsRef.current
330
- .filter(
331
- (session) =>
332
- session.sessionId !== sessionId && !removedSessionsRef.current.has(session.sessionId),
333
- )
334
- .map((session) => session.sessionId);
335
-
336
- const replacementUser = await findReplacementSession(otherSessionIds);
337
- if (replacementUser) {
338
- return replacementUser;
339
- }
340
- }
341
280
  }
342
281
 
343
282
  handleAuthError(error, {
@@ -352,7 +291,6 @@ export const useSessionManagement = ({
352
291
  },
353
292
  [
354
293
  activateSession,
355
- findReplacementSession,
356
294
  logger,
357
295
  onError,
358
296
  oxyServices,
@@ -389,18 +327,10 @@ export const useSessionManagement = ({
389
327
  updateSessions(deviceSessions, { merge: true });
390
328
  } catch (error) {
391
329
  if (isInvalidSessionError(error)) {
392
- const otherSessions = sessionsRef.current
393
- .filter(
394
- (session) =>
395
- session.sessionId !== activeSessionId &&
396
- !removedSessionsRef.current.has(session.sessionId),
397
- )
398
- .map((session) => session.sessionId);
399
-
400
- const replacementUser = await findReplacementSession(otherSessions);
401
- if (!replacementUser) {
402
- await clearSessionState();
403
- }
330
+ // Server authority (`SessionClient` bootstrap/sync) reconciles
331
+ // which account is actually active — just clear local state
332
+ // rather than guessing at a replacement session.
333
+ await clearSessionState();
404
334
  return;
405
335
  }
406
336
 
@@ -422,7 +352,6 @@ export const useSessionManagement = ({
422
352
  },
423
353
  [
424
354
  clearSessionState,
425
- findReplacementSession,
426
355
  logger,
427
356
  onError,
428
357
  oxyServices,