@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,133 +0,0 @@
1
- import type { RefreshAllAccountUser, User } from '@oxyhq/core';
2
- /**
3
- * The per-account user shape carried by a {@link DeviceAccount}. Either:
4
- * - the minimal projection returned by `POST /auth/refresh-all`
5
- * ({@link RefreshAllAccountUser}, the shared apex path), or
6
- * - the SDK's canonical {@link User} document (the active row on the local
7
- * fallback path, which already has the full user loaded in `useOxy()`).
8
- *
9
- * Both satisfy `getAccountDisplayName`'s `DisplayNameUserShape`, so display
10
- * resolution is uniform across paths.
11
- */
12
- export type DeviceAccountUser = RefreshAllAccountUser | User;
13
- /**
14
- * One signed-in account on this device, fully hydrated for the account
15
- * chooser. Unlike the old `AccountMenu` behaviour (which only carried the
16
- * ACTIVE session's user), EVERY entry here carries real per-account
17
- * `displayName` / `email` / `avatarUrl` / `color`.
18
- */
19
- export interface DeviceAccount {
20
- /** Session id used to switch to this account on native. */
21
- sessionId: string;
22
- /**
23
- * Device-local refresh-cookie slot index (0..N-1), present only on the
24
- * shared apex path. Web silent-switch (`refreshTokenViaCookie({ authuser }`)
25
- * keys off this; absent on the local fallback and on native.
26
- */
27
- authuser?: number;
28
- /** Whether this account is the currently-active session. */
29
- isCurrent: boolean;
30
- /** Friendly display name (never blank — falls back to a handle/sentinel). */
31
- displayName: string;
32
- /**
33
- * Real account email, or `null` when the account genuinely has none.
34
- * NEVER a synthesized `username@oxy.so` — a missing email stays `null` and
35
- * the UI falls back to the `@handle` secondary line.
36
- */
37
- email: string | null;
38
- /** Resolved avatar thumbnail URL, or `undefined` when the account has no avatar. */
39
- avatarUrl?: string;
40
- /** Account's preferred Bloom color preset, or `null` when unset. */
41
- color: string | null;
42
- /** The underlying per-account user payload (shared projection or full User). */
43
- user: DeviceAccountUser;
44
- }
45
- export interface UseDeviceAccountsResult {
46
- /** Every account signed in on this device, current one(s) flagged `isCurrent`. */
47
- accounts: DeviceAccount[];
48
- /** True until the first detection settles. */
49
- isLoading: boolean;
50
- /** The currently-active session id (mirrors `useOxy().activeSessionId`). */
51
- currentSessionId: string | null;
52
- /**
53
- * `true` when the list came from the shared apex path
54
- * (`refreshAllSessions()` returned >0 accounts — the cross-domain SSO cookie
55
- * set on `*.oxy.so`). `false` when it came from the local `useOxy()`
56
- * fallback (native, or cross-domain web where the apex cookie is absent).
57
- */
58
- fromSharedApex: boolean;
59
- }
60
- /**
61
- * Resolve which entries are the current account, robustly.
62
- *
63
- * Primary signal: `entry.sessionId === activeSessionId`. On the web shared-apex
64
- * path, `refreshAllSessions()` returns SERVER-side session ids that may not
65
- * equal the locally-stored `activeSessionId` (a different storage namespace /
66
- * server perspective), so a pure `sessionId` match can flag NO row as current —
67
- * the bug that surfaced as "Not signed in" even while authenticated.
68
- *
69
- * Fallbacks, applied only when the `sessionId` match found nothing AND the user
70
- * is authenticated:
71
- * 1. Match the live `user.id` against each entry's per-account user id.
72
- * 2. If still nothing and there is exactly one account, mark that one current.
73
- *
74
- * At most ONE entry is ever marked current.
75
- *
76
- * Pure & side-effect free so it is unit-testable without rendering React.
77
- */
78
- export declare function markCurrentAccount(accounts: DeviceAccount[], activeSessionId: string | null | undefined, liveUserId: string | null | undefined, isAuthenticated: boolean): DeviceAccount[];
79
- /**
80
- * Resolve every account signed in on this device for the unified account
81
- * switcher, with real per-account name / email / avatar / color.
82
- *
83
- * ## Data sources (web vs native)
84
- *
85
- * - **Web on a `*.oxy.so` host** → `oxyServices.refreshAllSessions()` returns
86
- * the full apex device-account list (identical to what `auth.oxy.so` sees,
87
- * because the `Domain=oxy.so` refresh cookies reach `api.oxy.so`). Used
88
- * directly — this is the `fromSharedApex: true` path.
89
- * - **Cross-domain web (non-`oxy.so` apex)** OR **native (no browser cookies)**
90
- * → `refreshAllSessions()` yields `{ accounts: [] }` (the apex cookies never
91
- * reach the request: cross-domain by `Domain`, native by having no cookie
92
- * jar at all; a 401 is also normalised to `{ accounts: [] }` inside the
93
- * core mixin). In that case we FALL BACK to the SDK's local multi-account
94
- * list from `useOxy()` (`sessions` + `activeSessionId` + the active `user`).
95
- * This is the `fromSharedApex: false` path.
96
- *
97
- * The fallback decision is purely data-driven: **if `refreshAllSessions()`
98
- * returns >0 accounts, use the shared path; otherwise fall back to local
99
- * `useOxy()` sessions.** No host sniffing is needed — the cookie scoping does
100
- * the discrimination for us, and the same code path works on native (where the
101
- * fetch returns `{ accounts: [] }`).
102
- *
103
- * ## Why React Query (not a `useRef`/`useState` start-once like the auth app)
104
- *
105
- * The auth app's `use-device-accounts.ts` hand-rolls a `startedRef` because it
106
- * has no React Query. The SDK does. `refreshAllSessions()` ROTATES single-use
107
- * refresh cookies on every call, so it must run AT MOST ONCE per page load:
108
- * we model that with `staleTime: Infinity` + `gcTime: Infinity` +
109
- * `refetchOnWindowFocus/Reconnect/Mount: false` + `retry: false`. React Query
110
- * dedupes concurrent mounts and caches the single result for the page's
111
- * lifetime — the exact "run once" guarantee the auth app documents, but
112
- * without the manual ref/state machinery.
113
- *
114
- * ## Validation note (intentionally NO zod re-parse)
115
- *
116
- * The auth app's hook calls `fetch` directly, so IT must `safeParse` the wire
117
- * response with `@oxyhq/contracts`. This hook calls
118
- * `oxyServices.refreshAllSessions()`, whose core mixin ALREADY validates and
119
- * normalises the wire response (skips entries missing required fields,
120
- * normalises `authuser` null→0, builds the `RefreshAllAccountUser` shape) — the
121
- * mixin is the single source of truth. Re-validating the SDK's own already-typed
122
- * output here would be redundant double-validation, so we do not re-parse.
123
- *
124
- * ## Error handling
125
- *
126
- * `refreshAllSessions()` already maps 401/404/abort to `{ accounts: [] }`
127
- * internally. Any OTHER failure (network, 5xx) propagates as a thrown error and
128
- * is surfaced by React Query (`query.isError`) — never swallowed. Callers that
129
- * don't care can ignore it; the hook still returns the local fallback list so
130
- * the chooser stays usable.
131
- */
132
- export declare function useDeviceAccounts(): UseDeviceAccountsResult;
133
- //# sourceMappingURL=useDeviceAccounts.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"useDeviceAccounts.d.ts","sourceRoot":"","sources":["../../../../../src/ui/hooks/useDeviceAccounts.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACR,qBAAqB,EACrB,IAAI,EAEP,MAAM,aAAa,CAAC;AAKrB;;;;;;;;;GASG;AACH,MAAM,MAAM,iBAAiB,GAAG,qBAAqB,GAAG,IAAI,CAAC;AAE7D;;;;;GAKG;AACH,MAAM,WAAW,aAAa;IAC1B,2DAA2D;IAC3D,SAAS,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4DAA4D;IAC5D,SAAS,EAAE,OAAO,CAAC;IACnB,6EAA6E;IAC7E,WAAW,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,oFAAoF;IACpF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oEAAoE;IACpE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,gFAAgF;IAChF,IAAI,EAAE,iBAAiB,CAAC;CAC3B;AAED,MAAM,WAAW,uBAAuB;IACpC,kFAAkF;IAClF,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,8CAA8C;IAC9C,SAAS,EAAE,OAAO,CAAC;IACnB,4EAA4E;IAC5E,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC;;;;;OAKG;IACH,cAAc,EAAE,OAAO,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,CAC9B,QAAQ,EAAE,aAAa,EAAE,EACzB,eAAe,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAC1C,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACrC,eAAe,EAAE,OAAO,GACzB,aAAa,EAAE,CAiCjB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AACH,wBAAgB,iBAAiB,IAAI,uBAAuB,CA4J3D"}
@@ -1,14 +0,0 @@
1
- interface UseSessionSocketProps {
2
- userId: string | null | undefined;
3
- activeSessionId: string | null | undefined;
4
- currentDeviceId: string | null | undefined;
5
- refreshSessions: () => Promise<void>;
6
- clearSessionState: () => Promise<void>;
7
- baseURL: string;
8
- getAccessToken: () => string | null;
9
- onRemoteSignOut?: () => void;
10
- onSessionRemoved?: (sessionId: string) => void;
11
- }
12
- export declare function useSessionSocket({ userId, activeSessionId, currentDeviceId, refreshSessions, clearSessionState, baseURL, getAccessToken, onRemoteSignOut, onSessionRemoved, }: UseSessionSocketProps): void;
13
- export {};
14
- //# sourceMappingURL=useSessionSocket.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"useSessionSocket.d.ts","sourceRoot":"","sources":["../../../../../src/ui/hooks/useSessionSocket.ts"],"names":[],"mappings":"AA8BA,UAAU,qBAAqB;IAC7B,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAClC,eAAe,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAC3C,eAAe,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAC3C,eAAe,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,iBAAiB,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;IACpC,eAAe,CAAC,EAAE,MAAM,IAAI,CAAC;IAC7B,gBAAgB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;CAChD;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,MAAM,EACN,eAAe,EACf,eAAe,EACf,eAAe,EACf,iBAAiB,EACjB,OAAO,EACP,cAAc,EACd,eAAe,EACf,gBAAgB,GACjB,EAAE,qBAAqB,QAoLvB"}
@@ -1,31 +0,0 @@
1
- /**
2
- * Shared, pure helpers for building the `origin|baseURL` signature used as the
3
- * module-level run-once guard key for cold-boot silent-SSO probes
4
- * (`silentColdBootKey` in `OxyContext`, `ssoSignature` in `useWebSSO`).
5
- *
6
- * NATIVE SAFETY (the bug this fixes): React Native aliases a global `window`
7
- * (it points at the JS global object), so `typeof window !== 'undefined'` is
8
- * `true` on native — but `window.location` is `undefined`. Reading
9
- * `window.location.origin` after only a `typeof window` check therefore throws
10
- * `TypeError: Cannot read property 'origin' of undefined` on native. Because
11
- * the key is built UNCONDITIONALLY at the top of the cold-boot path (before its
12
- * try/catch), that throw escaped session restore entirely and broke
13
- * cross-session restore on native. Both prior copies of the guard had the same
14
- * insufficient `typeof window` check and were prone to drift, so the read is
15
- * consolidated here behind a guard that also verifies `window.location`.
16
- */
17
- /**
18
- * Read `window.location.origin` safely on every platform.
19
- *
20
- * Returns the browser origin on web, and the sentinel `'no-origin'` anywhere
21
- * `window.location` is absent (React Native, SSR/Node). Never throws.
22
- */
23
- export declare function safeWindowOrigin(): string;
24
- /**
25
- * Build the stable `origin|baseURL` signature for the silent-SSO run-once
26
- * guard. Two providers pointed at the same API from the same origin share one
27
- * attempt. `getBaseURL` is invoked defensively (it may be absent or throw on a
28
- * partially-initialised client); any failure degrades to an empty baseURL.
29
- */
30
- export declare function buildSilentGuardKey(getBaseURL?: () => string | undefined): string;
31
- //# sourceMappingURL=silentGuardKey.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"silentGuardKey.d.ts","sourceRoot":"","sources":["../../../../src/utils/silentGuardKey.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH;;;;;GAKG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAKzC;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,UAAU,CAAC,EAAE,MAAM,MAAM,GAAG,SAAS,GAAG,MAAM,CASjF"}
@@ -1,48 +0,0 @@
1
- import type { DeviceAccount, DeviceAccountUser } from '../hooks/useDeviceAccounts';
2
-
3
- export interface AccountRow {
4
- sessionId: string;
5
- /** Device-local refresh-cookie slot index (web silent-switch). */
6
- authuser?: number;
7
- isActive: boolean;
8
- displayName: string;
9
- secondary: string | null;
10
- avatarUri?: string;
11
- user: DeviceAccountUser | null;
12
- }
13
-
14
- export interface BuildAccountRowsInput {
15
- /**
16
- * Per-device account entries from {@link useDeviceAccounts}. Each entry
17
- * already carries real per-account `displayName` / `email` / `avatarUrl` /
18
- * `color`, so EVERY row (not just the active one) renders full identity.
19
- */
20
- accounts: DeviceAccount[];
21
- }
22
-
23
- /**
24
- * Pure builder for `AccountMenu` rows. Extracted so the multi-account display
25
- * logic can be unit-tested without rendering React Native.
26
- *
27
- * Maps each {@link DeviceAccount} (sourced from `useDeviceAccounts()`, which
28
- * hydrates EVERY account with real name/email/avatar/color from the shared
29
- * apex `refresh-all` path or the local fallback) into an `AccountRow`.
30
- *
31
- * `secondary` is the account's real email when present; otherwise it falls
32
- * back to the `@handle` line. A missing email is NEVER synthesized into a fake
33
- * `username@oxy.so` — the device-account layer already resolved `email` to the
34
- * real value or the `@handle` fallback.
35
- */
36
- export function buildAccountRows({
37
- accounts,
38
- }: BuildAccountRowsInput): AccountRow[] {
39
- return accounts.map((account: DeviceAccount): AccountRow => ({
40
- sessionId: account.sessionId,
41
- authuser: account.authuser,
42
- isActive: account.isCurrent,
43
- displayName: account.displayName,
44
- secondary: account.email,
45
- avatarUri: account.avatarUrl,
46
- user: account.user,
47
- }));
48
- }
@@ -1,344 +0,0 @@
1
- import { useMemo } from 'react';
2
- import { useQuery } from '@tanstack/react-query';
3
- import {
4
- getAccountDisplayName,
5
- getAccountFallbackHandle,
6
- } from '@oxyhq/core';
7
- import type {
8
- RefreshAllAccountUser,
9
- User,
10
- ClientSession,
11
- } from '@oxyhq/core';
12
- import { useOxy } from '../context/OxyContext';
13
- import { useI18n } from './useI18n';
14
- import { queryKeys } from './queries/queryKeys';
15
-
16
- /**
17
- * The per-account user shape carried by a {@link DeviceAccount}. Either:
18
- * - the minimal projection returned by `POST /auth/refresh-all`
19
- * ({@link RefreshAllAccountUser}, the shared apex path), or
20
- * - the SDK's canonical {@link User} document (the active row on the local
21
- * fallback path, which already has the full user loaded in `useOxy()`).
22
- *
23
- * Both satisfy `getAccountDisplayName`'s `DisplayNameUserShape`, so display
24
- * resolution is uniform across paths.
25
- */
26
- export type DeviceAccountUser = RefreshAllAccountUser | User;
27
-
28
- /**
29
- * One signed-in account on this device, fully hydrated for the account
30
- * chooser. Unlike the old `AccountMenu` behaviour (which only carried the
31
- * ACTIVE session's user), EVERY entry here carries real per-account
32
- * `displayName` / `email` / `avatarUrl` / `color`.
33
- */
34
- export interface DeviceAccount {
35
- /** Session id used to switch to this account on native. */
36
- sessionId: string;
37
- /**
38
- * Device-local refresh-cookie slot index (0..N-1), present only on the
39
- * shared apex path. Web silent-switch (`refreshTokenViaCookie({ authuser }`)
40
- * keys off this; absent on the local fallback and on native.
41
- */
42
- authuser?: number;
43
- /** Whether this account is the currently-active session. */
44
- isCurrent: boolean;
45
- /** Friendly display name (never blank — falls back to a handle/sentinel). */
46
- displayName: string;
47
- /**
48
- * Real account email, or `null` when the account genuinely has none.
49
- * NEVER a synthesized `username@oxy.so` — a missing email stays `null` and
50
- * the UI falls back to the `@handle` secondary line.
51
- */
52
- email: string | null;
53
- /** Resolved avatar thumbnail URL, or `undefined` when the account has no avatar. */
54
- avatarUrl?: string;
55
- /** Account's preferred Bloom color preset, or `null` when unset. */
56
- color: string | null;
57
- /** The underlying per-account user payload (shared projection or full User). */
58
- user: DeviceAccountUser;
59
- }
60
-
61
- export interface UseDeviceAccountsResult {
62
- /** Every account signed in on this device, current one(s) flagged `isCurrent`. */
63
- accounts: DeviceAccount[];
64
- /** True until the first detection settles. */
65
- isLoading: boolean;
66
- /** The currently-active session id (mirrors `useOxy().activeSessionId`). */
67
- currentSessionId: string | null;
68
- /**
69
- * `true` when the list came from the shared apex path
70
- * (`refreshAllSessions()` returned >0 accounts — the cross-domain SSO cookie
71
- * set on `*.oxy.so`). `false` when it came from the local `useOxy()`
72
- * fallback (native, or cross-domain web where the apex cookie is absent).
73
- */
74
- fromSharedApex: boolean;
75
- }
76
-
77
- /**
78
- * Resolve which entries are the current account, robustly.
79
- *
80
- * Primary signal: `entry.sessionId === activeSessionId`. On the web shared-apex
81
- * path, `refreshAllSessions()` returns SERVER-side session ids that may not
82
- * equal the locally-stored `activeSessionId` (a different storage namespace /
83
- * server perspective), so a pure `sessionId` match can flag NO row as current —
84
- * the bug that surfaced as "Not signed in" even while authenticated.
85
- *
86
- * Fallbacks, applied only when the `sessionId` match found nothing AND the user
87
- * is authenticated:
88
- * 1. Match the live `user.id` against each entry's per-account user id.
89
- * 2. If still nothing and there is exactly one account, mark that one current.
90
- *
91
- * At most ONE entry is ever marked current.
92
- *
93
- * Pure & side-effect free so it is unit-testable without rendering React.
94
- */
95
- export function markCurrentAccount(
96
- accounts: DeviceAccount[],
97
- activeSessionId: string | null | undefined,
98
- liveUserId: string | null | undefined,
99
- isAuthenticated: boolean,
100
- ): DeviceAccount[] {
101
- const bySession = accounts.map((account): DeviceAccount => ({
102
- ...account,
103
- isCurrent: Boolean(activeSessionId) && account.sessionId === activeSessionId,
104
- }));
105
-
106
- if (bySession.some((account) => account.isCurrent) || !isAuthenticated) {
107
- return bySession;
108
- }
109
-
110
- // No row matched by session id: fall back to the live user's id, marking at
111
- // most the FIRST matching entry current (never more than one).
112
- if (liveUserId) {
113
- let matched = false;
114
- const byUser = bySession.map((account): DeviceAccount => {
115
- if (!matched && account.user.id === liveUserId) {
116
- matched = true;
117
- return { ...account, isCurrent: true };
118
- }
119
- return account;
120
- });
121
- if (matched) {
122
- return byUser;
123
- }
124
- }
125
-
126
- // Authenticated, nothing matched, but there is exactly one account → it must
127
- // be the current one (single-account shared-apex / cross-domain case).
128
- if (bySession.length === 1) {
129
- return [{ ...bySession[0], isCurrent: true }];
130
- }
131
-
132
- return bySession;
133
- }
134
-
135
- /**
136
- * Resolve every account signed in on this device for the unified account
137
- * switcher, with real per-account name / email / avatar / color.
138
- *
139
- * ## Data sources (web vs native)
140
- *
141
- * - **Web on a `*.oxy.so` host** → `oxyServices.refreshAllSessions()` returns
142
- * the full apex device-account list (identical to what `auth.oxy.so` sees,
143
- * because the `Domain=oxy.so` refresh cookies reach `api.oxy.so`). Used
144
- * directly — this is the `fromSharedApex: true` path.
145
- * - **Cross-domain web (non-`oxy.so` apex)** OR **native (no browser cookies)**
146
- * → `refreshAllSessions()` yields `{ accounts: [] }` (the apex cookies never
147
- * reach the request: cross-domain by `Domain`, native by having no cookie
148
- * jar at all; a 401 is also normalised to `{ accounts: [] }` inside the
149
- * core mixin). In that case we FALL BACK to the SDK's local multi-account
150
- * list from `useOxy()` (`sessions` + `activeSessionId` + the active `user`).
151
- * This is the `fromSharedApex: false` path.
152
- *
153
- * The fallback decision is purely data-driven: **if `refreshAllSessions()`
154
- * returns >0 accounts, use the shared path; otherwise fall back to local
155
- * `useOxy()` sessions.** No host sniffing is needed — the cookie scoping does
156
- * the discrimination for us, and the same code path works on native (where the
157
- * fetch returns `{ accounts: [] }`).
158
- *
159
- * ## Why React Query (not a `useRef`/`useState` start-once like the auth app)
160
- *
161
- * The auth app's `use-device-accounts.ts` hand-rolls a `startedRef` because it
162
- * has no React Query. The SDK does. `refreshAllSessions()` ROTATES single-use
163
- * refresh cookies on every call, so it must run AT MOST ONCE per page load:
164
- * we model that with `staleTime: Infinity` + `gcTime: Infinity` +
165
- * `refetchOnWindowFocus/Reconnect/Mount: false` + `retry: false`. React Query
166
- * dedupes concurrent mounts and caches the single result for the page's
167
- * lifetime — the exact "run once" guarantee the auth app documents, but
168
- * without the manual ref/state machinery.
169
- *
170
- * ## Validation note (intentionally NO zod re-parse)
171
- *
172
- * The auth app's hook calls `fetch` directly, so IT must `safeParse` the wire
173
- * response with `@oxyhq/contracts`. This hook calls
174
- * `oxyServices.refreshAllSessions()`, whose core mixin ALREADY validates and
175
- * normalises the wire response (skips entries missing required fields,
176
- * normalises `authuser` null→0, builds the `RefreshAllAccountUser` shape) — the
177
- * mixin is the single source of truth. Re-validating the SDK's own already-typed
178
- * output here would be redundant double-validation, so we do not re-parse.
179
- *
180
- * ## Error handling
181
- *
182
- * `refreshAllSessions()` already maps 401/404/abort to `{ accounts: [] }`
183
- * internally. Any OTHER failure (network, 5xx) propagates as a thrown error and
184
- * is surfaced by React Query (`query.isError`) — never swallowed. Callers that
185
- * don't care can ignore it; the hook still returns the local fallback list so
186
- * the chooser stays usable.
187
- */
188
- export function useDeviceAccounts(): UseDeviceAccountsResult {
189
- const {
190
- oxyServices,
191
- sessions,
192
- activeSessionId,
193
- user,
194
- isAuthenticated,
195
- } = useOxy();
196
- const { locale } = useI18n();
197
-
198
- // Stable, per-API-origin query key. `refreshAllSessions` resolves against
199
- // the session base url derived from `getBaseURL()`, so keying on it scopes
200
- // the cached result to the API the provider is pointed at.
201
- const baseURL = oxyServices.getBaseURL();
202
-
203
- const query = useQuery({
204
- queryKey: [...queryKeys.accounts.all, 'deviceAccounts', baseURL] as const,
205
- queryFn: () => oxyServices.refreshAllSessions(),
206
- // Only attempt the shared apex path while signed in. When logged out
207
- // there is nothing to enumerate and the fallback (also empty) is used.
208
- enabled: isAuthenticated,
209
- // Single-use cookie rotation → run at most once per page load.
210
- staleTime: Number.POSITIVE_INFINITY,
211
- gcTime: Number.POSITIVE_INFINITY,
212
- refetchOnWindowFocus: false,
213
- refetchOnReconnect: false,
214
- refetchOnMount: false,
215
- retry: false,
216
- });
217
-
218
- const sharedAccounts = query.data?.accounts ?? [];
219
- const fromSharedApex = sharedAccounts.length > 0;
220
-
221
- const accounts = useMemo<DeviceAccount[]>(() => {
222
- const resolveAvatarUrl = (avatar: string | null | undefined): string | undefined =>
223
- avatar ? oxyServices.getFileDownloadUrl(avatar, 'thumb') : undefined;
224
-
225
- // Build a single current-account row from the live `useOxy().user`. Used
226
- // as a last-resort fallback when neither the shared-apex probe nor the
227
- // local session store yielded a row but the user IS authenticated (e.g.
228
- // a cross-domain host where the apex probe returned empty before the
229
- // local session store hydrated). The signed-in user must ALWAYS be
230
- // represented. Email is the REAL email or the `@handle` line — never a
231
- // synthesized `username@oxy.so`.
232
- const liveUserRow = (): DeviceAccount[] => {
233
- if (!isAuthenticated || !user || !activeSessionId) {
234
- return [];
235
- }
236
- const displayName = getAccountDisplayName(user, locale);
237
- const handle = getAccountFallbackHandle(user);
238
- const secondaryHandle = handle ? `@${handle}` : null;
239
- return [{
240
- sessionId: activeSessionId,
241
- authuser: undefined,
242
- isCurrent: true,
243
- displayName,
244
- email: user.email ?? secondaryHandle,
245
- avatarUrl: resolveAvatarUrl(user.avatar),
246
- color: user.color ?? null,
247
- user,
248
- }];
249
- };
250
-
251
- let built: DeviceAccount[];
252
-
253
- if (fromSharedApex) {
254
- // Shared apex path: every entry carries a real per-account user.
255
- built = sharedAccounts.flatMap((entry): DeviceAccount[] => {
256
- if (!entry.user) {
257
- return [];
258
- }
259
- const accountUser: DeviceAccountUser = entry.user;
260
- const displayName = getAccountDisplayName(accountUser, locale);
261
- const handle = getAccountFallbackHandle(accountUser);
262
- const email = entry.user.email ?? null;
263
- const secondaryHandle = handle ? `@${handle}` : null;
264
- return [{
265
- sessionId: entry.sessionId,
266
- authuser: entry.authuser,
267
- // Provisional; finalised by `markCurrentAccount` below so the
268
- // shared-apex path is robust to server/local session-id skew.
269
- isCurrent: false,
270
- displayName,
271
- // Real email, or null (NEVER synthesized). The UI uses the
272
- // `@handle` line only when email is genuinely absent.
273
- email: email ?? secondaryHandle,
274
- avatarUrl: resolveAvatarUrl(entry.user.avatar),
275
- color: entry.user.color ?? null,
276
- user: accountUser,
277
- }];
278
- });
279
- } else {
280
- // Local fallback path: build from the SDK's multi-session store. The
281
- // active session row gets the full loaded `user`; inactive fallback
282
- // rows carry only what the `ClientSession` exposes (no synthesized
283
- // identity — they show the active user's data only when active).
284
- built = (sessions ?? []).flatMap((session: ClientSession): DeviceAccount[] => {
285
- const isCurrent = session.sessionId === activeSessionId;
286
- if (!isCurrent || !user) {
287
- return [];
288
- }
289
- const accountUser: DeviceAccountUser = user;
290
- const displayName = getAccountDisplayName(accountUser, locale);
291
- const handle = getAccountFallbackHandle(accountUser);
292
- const email = user.email ?? null;
293
- const secondaryHandle = handle ? `@${handle}` : null;
294
- return [{
295
- sessionId: session.sessionId,
296
- authuser: session.authuser,
297
- isCurrent,
298
- displayName,
299
- email: email ?? secondaryHandle,
300
- avatarUrl: resolveAvatarUrl(user.avatar),
301
- color: user.color ?? null,
302
- user: accountUser,
303
- }];
304
- });
305
- }
306
-
307
- // Robust current-account detection: tolerate server/local session-id
308
- // skew on the shared-apex path by falling back to the live user's id and
309
- // the single-account heuristic.
310
- const flagged = markCurrentAccount(
311
- built,
312
- activeSessionId,
313
- user?.id ?? null,
314
- isAuthenticated,
315
- );
316
-
317
- // The signed-in user must ALWAYS be represented. If detection produced
318
- // an empty list yet the user is authenticated, synthesize a single
319
- // current row from the live `useOxy().user`.
320
- if (flagged.length === 0) {
321
- return liveUserRow();
322
- }
323
- return flagged;
324
- }, [
325
- fromSharedApex,
326
- sharedAccounts,
327
- sessions,
328
- activeSessionId,
329
- user,
330
- isAuthenticated,
331
- locale,
332
- oxyServices,
333
- ]);
334
-
335
- return {
336
- accounts,
337
- // `isLoading` only reflects the shared probe while it's the relevant
338
- // source. Once we know we're on the fallback (probe settled with 0
339
- // accounts) the local list is synchronously available.
340
- isLoading: isAuthenticated && query.isLoading,
341
- currentSessionId: activeSessionId ?? null,
342
- fromSharedApex,
343
- };
344
- }