@oxyhq/services 13.2.0 → 14.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (260) hide show
  1. package/lib/commonjs/index.js +11 -17
  2. package/lib/commonjs/index.js.map +1 -1
  3. package/lib/commonjs/ui/components/AccountMenu.js +103 -65
  4. package/lib/commonjs/ui/components/AccountMenu.js.map +1 -1
  5. package/lib/commonjs/ui/components/AccountSwitcher.js +84 -126
  6. package/lib/commonjs/ui/components/AccountSwitcher.js.map +1 -1
  7. package/lib/commonjs/ui/components/ProfileButton.js +81 -22
  8. package/lib/commonjs/ui/components/ProfileButton.js.map +1 -1
  9. package/lib/commonjs/ui/components/ProfileMenu.js +244 -142
  10. package/lib/commonjs/ui/components/ProfileMenu.js.map +1 -1
  11. package/lib/commonjs/ui/components/SignInModal.js +4 -4
  12. package/lib/commonjs/ui/components/SignInModal.js.map +1 -1
  13. package/lib/commonjs/ui/context/OxyContext.js +506 -466
  14. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  15. package/lib/commonjs/ui/context/hooks/useAuthOperations.js +59 -57
  16. package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
  17. package/lib/commonjs/ui/context/inSessionTokenRefresh.js +23 -42
  18. package/lib/commonjs/ui/context/inSessionTokenRefresh.js.map +1 -1
  19. package/lib/commonjs/ui/context/silentSessionRestore.js +12 -21
  20. package/lib/commonjs/ui/context/silentSessionRestore.js.map +1 -1
  21. package/lib/commonjs/ui/hooks/useAuth.js +4 -3
  22. package/lib/commonjs/ui/hooks/useAuth.js.map +1 -1
  23. package/lib/commonjs/ui/hooks/useOxyAuthSession.js +12 -7
  24. package/lib/commonjs/ui/hooks/useOxyAuthSession.js.map +1 -1
  25. package/lib/commonjs/ui/hooks/useSessionManagement.js +17 -65
  26. package/lib/commonjs/ui/hooks/useSessionManagement.js.map +1 -1
  27. package/lib/commonjs/ui/hooks/useSwitchableAccounts.js +288 -0
  28. package/lib/commonjs/ui/hooks/useSwitchableAccounts.js.map +1 -0
  29. package/lib/commonjs/ui/hooks/useWebSSO.js +12 -191
  30. package/lib/commonjs/ui/hooks/useWebSSO.js.map +1 -1
  31. package/lib/commonjs/ui/index.js.map +1 -1
  32. package/lib/commonjs/ui/screens/OxyAuthScreen.js +2 -2
  33. package/lib/commonjs/ui/screens/OxyAuthScreen.js.map +1 -1
  34. package/lib/commonjs/ui/session/createSessionClient.js +28 -0
  35. package/lib/commonjs/ui/session/createSessionClient.js.map +1 -0
  36. package/lib/commonjs/ui/session/index.js +52 -0
  37. package/lib/commonjs/ui/session/index.js.map +1 -0
  38. package/lib/commonjs/ui/session/projectSessionState.js +86 -0
  39. package/lib/commonjs/ui/session/projectSessionState.js.map +1 -0
  40. package/lib/commonjs/ui/session/sessionClientHost.js +30 -0
  41. package/lib/commonjs/ui/session/sessionClientHost.js.map +1 -0
  42. package/lib/commonjs/ui/session/tokenTransport.js +77 -0
  43. package/lib/commonjs/ui/session/tokenTransport.js.map +1 -0
  44. package/lib/commonjs/ui/utils/activeAuthuser.js +35 -79
  45. package/lib/commonjs/ui/utils/activeAuthuser.js.map +1 -1
  46. package/lib/commonjs/ui/utils/sessionHelpers.js +1 -51
  47. package/lib/commonjs/ui/utils/sessionHelpers.js.map +1 -1
  48. package/lib/commonjs/utils/deviceFlowSignIn.js +70 -15
  49. package/lib/commonjs/utils/deviceFlowSignIn.js.map +1 -1
  50. package/lib/module/index.js +5 -5
  51. package/lib/module/index.js.map +1 -1
  52. package/lib/module/ui/components/AccountMenu.js +103 -65
  53. package/lib/module/ui/components/AccountMenu.js.map +1 -1
  54. package/lib/module/ui/components/AccountSwitcher.js +85 -127
  55. package/lib/module/ui/components/AccountSwitcher.js.map +1 -1
  56. package/lib/module/ui/components/ProfileButton.js +82 -23
  57. package/lib/module/ui/components/ProfileButton.js.map +1 -1
  58. package/lib/module/ui/components/ProfileMenu.js +247 -145
  59. package/lib/module/ui/components/ProfileMenu.js.map +1 -1
  60. package/lib/module/ui/components/SignInModal.js +4 -4
  61. package/lib/module/ui/components/SignInModal.js.map +1 -1
  62. package/lib/module/ui/context/OxyContext.js +510 -470
  63. package/lib/module/ui/context/OxyContext.js.map +1 -1
  64. package/lib/module/ui/context/hooks/useAuthOperations.js +61 -60
  65. package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
  66. package/lib/module/ui/context/inSessionTokenRefresh.js +24 -43
  67. package/lib/module/ui/context/inSessionTokenRefresh.js.map +1 -1
  68. package/lib/module/ui/context/silentSessionRestore.js +12 -20
  69. package/lib/module/ui/context/silentSessionRestore.js.map +1 -1
  70. package/lib/module/ui/hooks/useAuth.js +4 -3
  71. package/lib/module/ui/hooks/useAuth.js.map +1 -1
  72. package/lib/module/ui/hooks/useOxyAuthSession.js +12 -7
  73. package/lib/module/ui/hooks/useOxyAuthSession.js.map +1 -1
  74. package/lib/module/ui/hooks/useSessionManagement.js +18 -66
  75. package/lib/module/ui/hooks/useSessionManagement.js.map +1 -1
  76. package/lib/module/ui/hooks/useSwitchableAccounts.js +283 -0
  77. package/lib/module/ui/hooks/useSwitchableAccounts.js.map +1 -0
  78. package/lib/module/ui/hooks/useWebSSO.js +13 -192
  79. package/lib/module/ui/hooks/useWebSSO.js.map +1 -1
  80. package/lib/module/ui/index.js +0 -1
  81. package/lib/module/ui/index.js.map +1 -1
  82. package/lib/module/ui/screens/OxyAuthScreen.js +2 -2
  83. package/lib/module/ui/screens/OxyAuthScreen.js.map +1 -1
  84. package/lib/module/ui/session/createSessionClient.js +25 -0
  85. package/lib/module/ui/session/createSessionClient.js.map +1 -0
  86. package/lib/module/ui/session/index.js +17 -0
  87. package/lib/module/ui/session/index.js.map +1 -0
  88. package/lib/module/ui/session/projectSessionState.js +79 -0
  89. package/lib/module/ui/session/projectSessionState.js.map +1 -0
  90. package/lib/module/ui/session/sessionClientHost.js +26 -0
  91. package/lib/module/ui/session/sessionClientHost.js.map +1 -0
  92. package/lib/module/ui/session/tokenTransport.js +74 -0
  93. package/lib/module/ui/session/tokenTransport.js.map +1 -0
  94. package/lib/module/ui/utils/activeAuthuser.js +34 -76
  95. package/lib/module/ui/utils/activeAuthuser.js.map +1 -1
  96. package/lib/module/ui/utils/sessionHelpers.js +0 -49
  97. package/lib/module/ui/utils/sessionHelpers.js.map +1 -1
  98. package/lib/module/utils/deviceFlowSignIn.js +71 -16
  99. package/lib/module/utils/deviceFlowSignIn.js.map +1 -1
  100. package/lib/typescript/commonjs/index.d.ts +2 -3
  101. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  102. package/lib/typescript/commonjs/ui/components/AccountMenu.d.ts +4 -2
  103. package/lib/typescript/commonjs/ui/components/AccountMenu.d.ts.map +1 -1
  104. package/lib/typescript/commonjs/ui/components/AccountSwitcher.d.ts +7 -8
  105. package/lib/typescript/commonjs/ui/components/AccountSwitcher.d.ts.map +1 -1
  106. package/lib/typescript/commonjs/ui/components/ProfileButton.d.ts +11 -1
  107. package/lib/typescript/commonjs/ui/components/ProfileButton.d.ts.map +1 -1
  108. package/lib/typescript/commonjs/ui/components/ProfileMenu.d.ts +12 -9
  109. package/lib/typescript/commonjs/ui/components/ProfileMenu.d.ts.map +1 -1
  110. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts +16 -13
  111. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
  112. package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts +27 -3
  113. package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  114. package/lib/typescript/commonjs/ui/context/inSessionTokenRefresh.d.ts +22 -17
  115. package/lib/typescript/commonjs/ui/context/inSessionTokenRefresh.d.ts.map +1 -1
  116. package/lib/typescript/commonjs/ui/context/silentSessionRestore.d.ts +12 -18
  117. package/lib/typescript/commonjs/ui/context/silentSessionRestore.d.ts.map +1 -1
  118. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts +1 -0
  119. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  120. package/lib/typescript/commonjs/ui/hooks/useAuth.d.ts +1 -1
  121. package/lib/typescript/commonjs/ui/hooks/useAuth.d.ts.map +1 -1
  122. package/lib/typescript/commonjs/ui/hooks/useOxyAuthSession.d.ts +2 -2
  123. package/lib/typescript/commonjs/ui/hooks/useOxyAuthSession.d.ts.map +1 -1
  124. package/lib/typescript/commonjs/ui/hooks/useSessionManagement.d.ts.map +1 -1
  125. package/lib/typescript/commonjs/ui/hooks/useSwitchableAccounts.d.ts +162 -0
  126. package/lib/typescript/commonjs/ui/hooks/useSwitchableAccounts.d.ts.map +1 -0
  127. package/lib/typescript/commonjs/ui/hooks/useWebSSO.d.ts +13 -52
  128. package/lib/typescript/commonjs/ui/hooks/useWebSSO.d.ts.map +1 -1
  129. package/lib/typescript/commonjs/ui/index.d.ts.map +1 -1
  130. package/lib/typescript/commonjs/ui/session/createSessionClient.d.ts +15 -0
  131. package/lib/typescript/commonjs/ui/session/createSessionClient.d.ts.map +1 -0
  132. package/lib/typescript/commonjs/ui/session/index.d.ts +15 -0
  133. package/lib/typescript/commonjs/ui/session/index.d.ts.map +1 -0
  134. package/lib/typescript/commonjs/ui/session/projectSessionState.d.ts +45 -0
  135. package/lib/typescript/commonjs/ui/session/projectSessionState.d.ts.map +1 -0
  136. package/lib/typescript/commonjs/ui/session/sessionClientHost.d.ts +14 -0
  137. package/lib/typescript/commonjs/ui/session/sessionClientHost.d.ts.map +1 -0
  138. package/lib/typescript/commonjs/ui/session/tokenTransport.d.ts +24 -0
  139. package/lib/typescript/commonjs/ui/session/tokenTransport.d.ts.map +1 -0
  140. package/lib/typescript/commonjs/ui/utils/activeAuthuser.d.ts +21 -29
  141. package/lib/typescript/commonjs/ui/utils/activeAuthuser.d.ts.map +1 -1
  142. package/lib/typescript/commonjs/ui/utils/sessionHelpers.d.ts +0 -19
  143. package/lib/typescript/commonjs/ui/utils/sessionHelpers.d.ts.map +1 -1
  144. package/lib/typescript/commonjs/utils/deviceFlowSignIn.d.ts +51 -19
  145. package/lib/typescript/commonjs/utils/deviceFlowSignIn.d.ts.map +1 -1
  146. package/lib/typescript/module/index.d.ts +2 -3
  147. package/lib/typescript/module/index.d.ts.map +1 -1
  148. package/lib/typescript/module/ui/components/AccountMenu.d.ts +4 -2
  149. package/lib/typescript/module/ui/components/AccountMenu.d.ts.map +1 -1
  150. package/lib/typescript/module/ui/components/AccountSwitcher.d.ts +7 -8
  151. package/lib/typescript/module/ui/components/AccountSwitcher.d.ts.map +1 -1
  152. package/lib/typescript/module/ui/components/ProfileButton.d.ts +11 -1
  153. package/lib/typescript/module/ui/components/ProfileButton.d.ts.map +1 -1
  154. package/lib/typescript/module/ui/components/ProfileMenu.d.ts +12 -9
  155. package/lib/typescript/module/ui/components/ProfileMenu.d.ts.map +1 -1
  156. package/lib/typescript/module/ui/context/OxyContext.d.ts +16 -13
  157. package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
  158. package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts +27 -3
  159. package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  160. package/lib/typescript/module/ui/context/inSessionTokenRefresh.d.ts +22 -17
  161. package/lib/typescript/module/ui/context/inSessionTokenRefresh.d.ts.map +1 -1
  162. package/lib/typescript/module/ui/context/silentSessionRestore.d.ts +12 -18
  163. package/lib/typescript/module/ui/context/silentSessionRestore.d.ts.map +1 -1
  164. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts +1 -0
  165. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  166. package/lib/typescript/module/ui/hooks/useAuth.d.ts +1 -1
  167. package/lib/typescript/module/ui/hooks/useAuth.d.ts.map +1 -1
  168. package/lib/typescript/module/ui/hooks/useOxyAuthSession.d.ts +2 -2
  169. package/lib/typescript/module/ui/hooks/useOxyAuthSession.d.ts.map +1 -1
  170. package/lib/typescript/module/ui/hooks/useSessionManagement.d.ts.map +1 -1
  171. package/lib/typescript/module/ui/hooks/useSwitchableAccounts.d.ts +162 -0
  172. package/lib/typescript/module/ui/hooks/useSwitchableAccounts.d.ts.map +1 -0
  173. package/lib/typescript/module/ui/hooks/useWebSSO.d.ts +13 -52
  174. package/lib/typescript/module/ui/hooks/useWebSSO.d.ts.map +1 -1
  175. package/lib/typescript/module/ui/index.d.ts.map +1 -1
  176. package/lib/typescript/module/ui/session/createSessionClient.d.ts +15 -0
  177. package/lib/typescript/module/ui/session/createSessionClient.d.ts.map +1 -0
  178. package/lib/typescript/module/ui/session/index.d.ts +15 -0
  179. package/lib/typescript/module/ui/session/index.d.ts.map +1 -0
  180. package/lib/typescript/module/ui/session/projectSessionState.d.ts +45 -0
  181. package/lib/typescript/module/ui/session/projectSessionState.d.ts.map +1 -0
  182. package/lib/typescript/module/ui/session/sessionClientHost.d.ts +14 -0
  183. package/lib/typescript/module/ui/session/sessionClientHost.d.ts.map +1 -0
  184. package/lib/typescript/module/ui/session/tokenTransport.d.ts +24 -0
  185. package/lib/typescript/module/ui/session/tokenTransport.d.ts.map +1 -0
  186. package/lib/typescript/module/ui/utils/activeAuthuser.d.ts +21 -29
  187. package/lib/typescript/module/ui/utils/activeAuthuser.d.ts.map +1 -1
  188. package/lib/typescript/module/ui/utils/sessionHelpers.d.ts +0 -19
  189. package/lib/typescript/module/ui/utils/sessionHelpers.d.ts.map +1 -1
  190. package/lib/typescript/module/utils/deviceFlowSignIn.d.ts +51 -19
  191. package/lib/typescript/module/utils/deviceFlowSignIn.d.ts.map +1 -1
  192. package/package.json +4 -4
  193. package/src/index.ts +9 -9
  194. package/src/ui/components/AccountMenu.tsx +117 -70
  195. package/src/ui/components/AccountSwitcher.tsx +88 -134
  196. package/src/ui/components/ProfileButton.tsx +95 -17
  197. package/src/ui/components/ProfileMenu.tsx +263 -155
  198. package/src/ui/components/SignInModal.tsx +5 -5
  199. package/src/ui/context/OxyContext.tsx +560 -475
  200. package/src/ui/context/hooks/useAuthOperations.ts +72 -63
  201. package/src/ui/context/inSessionTokenRefresh.ts +23 -42
  202. package/src/ui/context/silentSessionRestore.ts +12 -26
  203. package/src/ui/hooks/useAuth.ts +4 -3
  204. package/src/ui/hooks/useOxyAuthSession.ts +13 -8
  205. package/src/ui/hooks/useSessionManagement.ts +16 -87
  206. package/src/ui/hooks/useSwitchableAccounts.ts +397 -0
  207. package/src/ui/hooks/useWebSSO.ts +13 -227
  208. package/src/ui/index.ts +0 -1
  209. package/src/ui/screens/OxyAuthScreen.tsx +2 -2
  210. package/src/ui/session/__tests__/createSessionClient.test.ts +41 -0
  211. package/src/ui/session/__tests__/projectSessionState.test.ts +114 -0
  212. package/src/ui/session/__tests__/sessionClientHost.test.ts +43 -0
  213. package/src/ui/session/__tests__/tokenTransport.test.ts +142 -0
  214. package/src/ui/session/createSessionClient.ts +21 -0
  215. package/src/ui/session/index.ts +19 -0
  216. package/src/ui/session/projectSessionState.ts +85 -0
  217. package/src/ui/session/sessionClientHost.ts +27 -0
  218. package/src/ui/session/tokenTransport.ts +76 -0
  219. package/src/ui/utils/activeAuthuser.ts +34 -76
  220. package/src/ui/utils/sessionHelpers.ts +0 -66
  221. package/src/utils/__tests__/deviceFlowSignIn.test.ts +243 -34
  222. package/src/utils/deviceFlowSignIn.ts +98 -21
  223. package/lib/commonjs/ui/components/accountMenuRows.js +0 -33
  224. package/lib/commonjs/ui/components/accountMenuRows.js.map +0 -1
  225. package/lib/commonjs/ui/hooks/useDeviceAccounts.js +0 -280
  226. package/lib/commonjs/ui/hooks/useDeviceAccounts.js.map +0 -1
  227. package/lib/commonjs/ui/hooks/useSessionSocket.js +0 -203
  228. package/lib/commonjs/ui/hooks/useSessionSocket.js.map +0 -1
  229. package/lib/commonjs/utils/silentGuardKey.js +0 -54
  230. package/lib/commonjs/utils/silentGuardKey.js.map +0 -1
  231. package/lib/module/ui/components/accountMenuRows.js +0 -29
  232. package/lib/module/ui/components/accountMenuRows.js.map +0 -1
  233. package/lib/module/ui/hooks/useDeviceAccounts.js +0 -276
  234. package/lib/module/ui/hooks/useDeviceAccounts.js.map +0 -1
  235. package/lib/module/ui/hooks/useSessionSocket.js +0 -199
  236. package/lib/module/ui/hooks/useSessionSocket.js.map +0 -1
  237. package/lib/module/utils/silentGuardKey.js +0 -49
  238. package/lib/module/utils/silentGuardKey.js.map +0 -1
  239. package/lib/typescript/commonjs/ui/components/accountMenuRows.d.ts +0 -34
  240. package/lib/typescript/commonjs/ui/components/accountMenuRows.d.ts.map +0 -1
  241. package/lib/typescript/commonjs/ui/hooks/useDeviceAccounts.d.ts +0 -133
  242. package/lib/typescript/commonjs/ui/hooks/useDeviceAccounts.d.ts.map +0 -1
  243. package/lib/typescript/commonjs/ui/hooks/useSessionSocket.d.ts +0 -14
  244. package/lib/typescript/commonjs/ui/hooks/useSessionSocket.d.ts.map +0 -1
  245. package/lib/typescript/commonjs/utils/silentGuardKey.d.ts +0 -31
  246. package/lib/typescript/commonjs/utils/silentGuardKey.d.ts.map +0 -1
  247. package/lib/typescript/module/ui/components/accountMenuRows.d.ts +0 -34
  248. package/lib/typescript/module/ui/components/accountMenuRows.d.ts.map +0 -1
  249. package/lib/typescript/module/ui/hooks/useDeviceAccounts.d.ts +0 -133
  250. package/lib/typescript/module/ui/hooks/useDeviceAccounts.d.ts.map +0 -1
  251. package/lib/typescript/module/ui/hooks/useSessionSocket.d.ts +0 -14
  252. package/lib/typescript/module/ui/hooks/useSessionSocket.d.ts.map +0 -1
  253. package/lib/typescript/module/utils/silentGuardKey.d.ts +0 -31
  254. package/lib/typescript/module/utils/silentGuardKey.d.ts.map +0 -1
  255. package/src/ui/components/accountMenuRows.ts +0 -48
  256. package/src/ui/hooks/useDeviceAccounts.ts +0 -344
  257. package/src/ui/hooks/useSessionSocket.ts +0 -233
  258. package/src/ui/utils/__tests__/activeAuthuser.test.ts +0 -75
  259. package/src/utils/__tests__/silentGuardKey.test.ts +0 -82
  260. package/src/utils/silentGuardKey.ts +0 -46
@@ -0,0 +1,41 @@
1
+ import { SessionClient } from '@oxyhq/core';
2
+ import { createSessionClient } from '../createSessionClient';
3
+
4
+ function fakeOxy() {
5
+ const listeners = new Set<(t: string | null) => void>();
6
+ return {
7
+ makeRequest: jest.fn().mockResolvedValue(undefined),
8
+ getBaseURL: jest.fn().mockReturnValue('https://api.oxy.so'),
9
+ getAccessToken: jest.fn().mockReturnValue(null),
10
+ setTokens: jest.fn(),
11
+ onTokensChanged: jest.fn((l: (t: string | null) => void) => {
12
+ listeners.add(l);
13
+ return () => listeners.delete(l);
14
+ }),
15
+ silentSignIn: jest.fn().mockResolvedValue(null),
16
+ signInWithSharedIdentity: jest.fn().mockResolvedValue(null),
17
+ };
18
+ }
19
+
20
+ describe('createSessionClient', () => {
21
+ test('wires a SessionClient instance backed by the host + token transport', () => {
22
+ const oxy = fakeOxy();
23
+
24
+ const { client, host } = createSessionClient(oxy as never);
25
+
26
+ expect(client).toBeInstanceOf(SessionClient);
27
+ expect(typeof client.bootstrap).toBe('function');
28
+ expect(client.getState()).toBeNull();
29
+ expect(typeof host.setCurrentAccountId).toBe('function');
30
+ });
31
+
32
+ test('the returned host reflects setCurrentAccountId', () => {
33
+ const oxy = fakeOxy();
34
+
35
+ const { host } = createSessionClient(oxy as never);
36
+
37
+ expect(host.getCurrentAccountId()).toBeNull();
38
+ host.setCurrentAccountId('u1');
39
+ expect(host.getCurrentAccountId()).toBe('u1');
40
+ });
41
+ });
@@ -0,0 +1,114 @@
1
+ import type { DeviceSessionState } from '@oxyhq/contracts';
2
+ import type { User } from '@oxyhq/core';
3
+ import {
4
+ accountIdsOf,
5
+ activeSessionIdOf,
6
+ activeUserOf,
7
+ deviceStateToClientSessions,
8
+ } from '../projectSessionState';
9
+
10
+ function makeUser(id: string): User {
11
+ return {
12
+ id,
13
+ publicKey: `pk-${id}`,
14
+ username: `user-${id}`,
15
+ name: {},
16
+ };
17
+ }
18
+
19
+ // DeviceSessionState.updatedAt is an epoch-ms number on the wire (see
20
+ // packages/contracts/src/deviceSession.ts: `updatedAt: z.number()`), not an
21
+ // ISO string.
22
+ const UPDATED_AT_MS = Date.UTC(2026, 6, 1, 0, 0, 0, 0);
23
+ const UPDATED_AT_ISO = new Date(UPDATED_AT_MS).toISOString();
24
+
25
+ const state: DeviceSessionState = {
26
+ deviceId: 'device-1',
27
+ accounts: [
28
+ { accountId: 'a1', sessionId: 'sess-a1', authuser: 0 },
29
+ { accountId: 'a2', sessionId: 'sess-a2', authuser: 1 },
30
+ ],
31
+ activeAccountId: 'a2',
32
+ revision: 1,
33
+ updatedAt: UPDATED_AT_MS,
34
+ };
35
+
36
+ const usersById = new Map<string, User>([
37
+ ['a1', makeUser('a1')],
38
+ ['a2', makeUser('a2')],
39
+ ]);
40
+
41
+ describe('projectSessionState', () => {
42
+ describe('activeSessionIdOf', () => {
43
+ test('returns the active account sessionId', () => {
44
+ expect(activeSessionIdOf(state)).toBe('sess-a2');
45
+ });
46
+
47
+ test('returns null for null state', () => {
48
+ expect(activeSessionIdOf(null)).toBeNull();
49
+ });
50
+
51
+ test('returns null when activeAccountId is null', () => {
52
+ expect(activeSessionIdOf({ ...state, activeAccountId: null })).toBeNull();
53
+ });
54
+ });
55
+
56
+ describe('deviceStateToClientSessions', () => {
57
+ test('maps every account in order with isCurrent + authuser', () => {
58
+ const sessions = deviceStateToClientSessions(state, usersById);
59
+ expect(sessions).toHaveLength(2);
60
+ expect(sessions[0]).toEqual({
61
+ sessionId: 'sess-a1',
62
+ deviceId: 'device-1',
63
+ expiresAt: UPDATED_AT_ISO,
64
+ lastActive: UPDATED_AT_ISO,
65
+ userId: 'a1',
66
+ isCurrent: false,
67
+ authuser: 0,
68
+ });
69
+ expect(sessions[1]).toEqual({
70
+ sessionId: 'sess-a2',
71
+ deviceId: 'device-1',
72
+ expiresAt: UPDATED_AT_ISO,
73
+ lastActive: UPDATED_AT_ISO,
74
+ userId: 'a2',
75
+ isCurrent: true,
76
+ authuser: 1,
77
+ });
78
+ });
79
+
80
+ test('still projects a session for an account absent from usersById', () => {
81
+ const sessions = deviceStateToClientSessions(state, new Map());
82
+ expect(sessions).toHaveLength(2);
83
+ expect(sessions.map((session) => session.userId)).toEqual(['a1', 'a2']);
84
+ });
85
+ });
86
+
87
+ describe('activeUserOf', () => {
88
+ test('returns the active account user', () => {
89
+ expect(activeUserOf(state, usersById)).toEqual(makeUser('a2'));
90
+ });
91
+
92
+ test('returns null for null state', () => {
93
+ expect(activeUserOf(null, usersById)).toBeNull();
94
+ });
95
+
96
+ test('returns null when activeAccountId is null', () => {
97
+ expect(activeUserOf({ ...state, activeAccountId: null }, usersById)).toBeNull();
98
+ });
99
+
100
+ test('returns null when the active account id is absent from usersById', () => {
101
+ expect(activeUserOf(state, new Map())).toBeNull();
102
+ });
103
+ });
104
+
105
+ describe('accountIdsOf', () => {
106
+ test('returns every account id in order', () => {
107
+ expect(accountIdsOf(state)).toEqual(['a1', 'a2']);
108
+ });
109
+
110
+ test('returns [] for null state', () => {
111
+ expect(accountIdsOf(null)).toEqual([]);
112
+ });
113
+ });
114
+ });
@@ -0,0 +1,43 @@
1
+ import { createSessionClientHost } from '../sessionClientHost';
2
+
3
+ function fakeOxy() {
4
+ const listeners = new Set<(t: string | null) => void>();
5
+ return {
6
+ makeRequest: jest.fn().mockResolvedValue({ ok: true }),
7
+ getBaseURL: jest.fn().mockReturnValue('https://api.oxy.so'),
8
+ getAccessToken: jest.fn().mockReturnValue('tok'),
9
+ setTokens: jest.fn(),
10
+ onTokensChanged: jest.fn((l: (t: string | null) => void) => { listeners.add(l); return () => listeners.delete(l); }),
11
+ _emit: (t: string | null) => listeners.forEach((l) => l(t)),
12
+ };
13
+ }
14
+
15
+ test('delegates REST + token methods to oxyServices', async () => {
16
+ const oxy = fakeOxy();
17
+ const host = createSessionClientHost(oxy as never);
18
+ await host.makeRequest('GET', '/session/device/state', undefined, { cache: false });
19
+ expect(oxy.makeRequest).toHaveBeenCalledWith('GET', '/session/device/state', undefined, { cache: false });
20
+ expect(host.getBaseURL()).toBe('https://api.oxy.so');
21
+ expect(host.getAccessToken()).toBe('tok');
22
+ host.setTokens('new');
23
+ expect(oxy.setTokens).toHaveBeenCalledWith('new');
24
+ });
25
+
26
+ test('getCurrentAccountId reflects setCurrentAccountId', () => {
27
+ const host = createSessionClientHost(fakeOxy() as never);
28
+ expect(host.getCurrentAccountId()).toBeNull();
29
+ host.setCurrentAccountId('u1');
30
+ expect(host.getCurrentAccountId()).toBe('u1');
31
+ });
32
+
33
+ test('onTokensChanged forwards to oxyServices and unsubscribes', () => {
34
+ const oxy = fakeOxy();
35
+ const host = createSessionClientHost(oxy as never);
36
+ const cb = jest.fn();
37
+ const unsub = host.onTokensChanged(cb);
38
+ oxy._emit(null);
39
+ expect(cb).toHaveBeenCalledWith(null);
40
+ unsub();
41
+ oxy._emit('x');
42
+ expect(cb).toHaveBeenCalledTimes(1);
43
+ });
@@ -0,0 +1,142 @@
1
+ import type { DeviceSessionState } from '@oxyhq/contracts';
2
+ import { logger } from '@oxyhq/core';
3
+ import { createTokenTransport } from '../tokenTransport';
4
+ import { isWebBrowser } from '../../hooks/useWebSSO';
5
+
6
+ jest.mock('../../hooks/useWebSSO', () => ({
7
+ isWebBrowser: jest.fn(),
8
+ }));
9
+
10
+ const mockedIsWebBrowser = isWebBrowser as jest.MockedFunction<typeof isWebBrowser>;
11
+
12
+ function fakeOxy(accessToken: string | null) {
13
+ return {
14
+ getAccessToken: jest.fn().mockReturnValue(accessToken),
15
+ silentSignIn: jest.fn().mockResolvedValue(null),
16
+ signInWithSharedIdentity: jest.fn().mockResolvedValue(null),
17
+ };
18
+ }
19
+
20
+ const state: DeviceSessionState = {
21
+ deviceId: 'device-1',
22
+ accounts: [{ accountId: 'a1', sessionId: 'sess-a1', authuser: 0 }],
23
+ activeAccountId: 'a1',
24
+ revision: 1,
25
+ updatedAt: Date.UTC(2026, 6, 1, 0, 0, 0, 0),
26
+ };
27
+
28
+ describe('createTokenTransport', () => {
29
+ beforeEach(() => {
30
+ jest.clearAllMocks();
31
+ });
32
+
33
+ test('no-ops when a token is already present', async () => {
34
+ const oxy = fakeOxy('tok');
35
+ const transport = createTokenTransport(oxy as never);
36
+
37
+ await transport.ensureActiveToken(state);
38
+
39
+ expect(oxy.silentSignIn).not.toHaveBeenCalled();
40
+ expect(oxy.signInWithSharedIdentity).not.toHaveBeenCalled();
41
+ });
42
+
43
+ test('mints via silentSignIn on web when no token is present', async () => {
44
+ mockedIsWebBrowser.mockReturnValue(true);
45
+ const oxy = fakeOxy(null);
46
+ const transport = createTokenTransport(oxy as never);
47
+
48
+ await transport.ensureActiveToken(state);
49
+
50
+ expect(oxy.silentSignIn).toHaveBeenCalledTimes(1);
51
+ expect(oxy.signInWithSharedIdentity).not.toHaveBeenCalled();
52
+ });
53
+
54
+ test('mints via signInWithSharedIdentity off web when no token is present', async () => {
55
+ mockedIsWebBrowser.mockReturnValue(false);
56
+ const oxy = fakeOxy(null);
57
+ const transport = createTokenTransport(oxy as never);
58
+
59
+ await transport.ensureActiveToken(state);
60
+
61
+ expect(oxy.signInWithSharedIdentity).toHaveBeenCalledTimes(1);
62
+ expect(oxy.silentSignIn).not.toHaveBeenCalled();
63
+ });
64
+
65
+ test('resolves (never rejects) and logs a warning when the mint throws', async () => {
66
+ mockedIsWebBrowser.mockReturnValue(true);
67
+ const oxy = fakeOxy(null);
68
+ oxy.silentSignIn.mockRejectedValue(new Error('mint boom'));
69
+ const transport = createTokenTransport(oxy as never);
70
+ const warnSpy = jest.spyOn(logger, 'warn').mockImplementation(() => undefined);
71
+
72
+ await expect(transport.ensureActiveToken(state)).resolves.toBeUndefined();
73
+
74
+ expect(warnSpy).toHaveBeenCalledWith(
75
+ 'ensureActiveToken: mint failed',
76
+ { component: 'TokenTransport' },
77
+ expect.any(Error),
78
+ );
79
+
80
+ warnSpy.mockRestore();
81
+ });
82
+
83
+ test('resolves cleanly when the mint returns null', async () => {
84
+ mockedIsWebBrowser.mockReturnValue(true);
85
+ const oxy = fakeOxy(null);
86
+ const transport = createTokenTransport(oxy as never);
87
+
88
+ await expect(transport.ensureActiveToken(state)).resolves.toBeUndefined();
89
+ expect(oxy.silentSignIn).toHaveBeenCalledTimes(1);
90
+ });
91
+
92
+ test('coalesces concurrent calls into a single mint', async () => {
93
+ mockedIsWebBrowser.mockReturnValue(true);
94
+ const oxy = fakeOxy(null);
95
+ let resolveMint: ((value: null) => void) | null = null;
96
+ oxy.silentSignIn.mockImplementation(
97
+ () =>
98
+ new Promise((resolve) => {
99
+ resolveMint = resolve;
100
+ }),
101
+ );
102
+ const transport = createTokenTransport(oxy as never);
103
+
104
+ const first = transport.ensureActiveToken(state);
105
+ const second = transport.ensureActiveToken(state);
106
+
107
+ expect(oxy.silentSignIn).toHaveBeenCalledTimes(1);
108
+
109
+ resolveMint?.(null);
110
+ await Promise.all([first, second]);
111
+
112
+ expect(oxy.silentSignIn).toHaveBeenCalledTimes(1);
113
+
114
+ // A later call after the in-flight mint settled starts a fresh mint
115
+ // (the mock reassigns `resolveMint` on the second silentSignIn call).
116
+ const third = transport.ensureActiveToken(state);
117
+ expect(oxy.silentSignIn).toHaveBeenCalledTimes(2);
118
+ resolveMint?.(null);
119
+ await third;
120
+ });
121
+
122
+ test('treats a throwing getAccessToken as no-token and still mints', async () => {
123
+ mockedIsWebBrowser.mockReturnValue(true);
124
+ const oxy = fakeOxy(null);
125
+ oxy.getAccessToken.mockImplementation(() => {
126
+ throw new Error('storage unavailable');
127
+ });
128
+ const transport = createTokenTransport(oxy as never);
129
+ const warnSpy = jest.spyOn(logger, 'warn').mockImplementation(() => undefined);
130
+
131
+ await expect(transport.ensureActiveToken(state)).resolves.toBeUndefined();
132
+
133
+ expect(warnSpy).toHaveBeenCalledWith(
134
+ 'ensureActiveToken: getAccessToken threw',
135
+ { component: 'TokenTransport' },
136
+ expect.any(Error),
137
+ );
138
+ expect(oxy.silentSignIn).toHaveBeenCalledTimes(1);
139
+
140
+ warnSpy.mockRestore();
141
+ });
142
+ });
@@ -0,0 +1,21 @@
1
+ import type { OxyServices } from '@oxyhq/core';
2
+ import { SessionClient } from '@oxyhq/core';
3
+ import { createSessionClientHost } from './sessionClientHost';
4
+ import { createTokenTransport } from './tokenTransport';
5
+
6
+ /**
7
+ * Wires a `SessionClient` for `@oxyhq/services`: builds the `SessionClientHost`
8
+ * adapter (Task 1) and the platform `TokenTransport` (Task 3) over the given
9
+ * `OxyServices` instance, and returns both the client and the host — the host
10
+ * is returned (not just the client) so `OxyContext` (Fase 3-B) can call
11
+ * `host.setCurrentAccountId(...)` as the active account changes.
12
+ */
13
+ export function createSessionClient(oxyServices: OxyServices): {
14
+ client: SessionClient;
15
+ host: ReturnType<typeof createSessionClientHost>;
16
+ } {
17
+ const host = createSessionClientHost(oxyServices);
18
+ const transport = createTokenTransport(oxyServices);
19
+ const client = new SessionClient(host, { transport });
20
+ return { client, host };
21
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Session-sync integration layer (Fase 3-A) — intra-package use only.
3
+ *
4
+ * These modules let `@oxyhq/services` drive the platform-agnostic
5
+ * `SessionClient` (in `@oxyhq/core`): a thin host adapter over `OxyServices`,
6
+ * pure `DeviceSessionState → services` projection helpers, a platform token
7
+ * transport, and the factory that wires them together. Nothing here is
8
+ * re-exported from the package root — `OxyContext` consumes it directly in
9
+ * Fase 3-B.
10
+ */
11
+ export { createSessionClient } from './createSessionClient';
12
+ export { createSessionClientHost } from './sessionClientHost';
13
+ export { createTokenTransport } from './tokenTransport';
14
+ export {
15
+ accountIdsOf,
16
+ activeSessionIdOf,
17
+ activeUserOf,
18
+ deviceStateToClientSessions,
19
+ } from './projectSessionState';
@@ -0,0 +1,85 @@
1
+ import type { DeviceSessionState } from '@oxyhq/contracts';
2
+ import type { ClientSession, User } from '@oxyhq/core';
3
+
4
+ /**
5
+ * Pure projection helpers: `DeviceSessionState` (the device-scoped
6
+ * multi-account session-sync state produced by `@oxyhq/core`'s
7
+ * `SessionClient`) -> the shapes `OxyContext` renders today
8
+ * (`ClientSession[]`, an active session id, an active `User`).
9
+ *
10
+ * No I/O. The caller fetches profiles via
11
+ * `oxyServices.getUsersByIds(accountIdsOf(state))` and builds `usersById`
12
+ * from the result before calling `deviceStateToClientSessions` /
13
+ * `activeUserOf`.
14
+ */
15
+
16
+ /**
17
+ * Maps every `SessionAccount` in `state.accounts` to a `ClientSession`.
18
+ *
19
+ * `DeviceSessionState` carries no per-account `expiresAt` / `lastActive` —
20
+ * both are set to `state.updatedAt` (converted to an ISO-8601 string; the
21
+ * wire value is an epoch-ms number) as a provisional value. Fase 3-B
22
+ * rewrites `ClientSession` to make `expiresAt`/`lastActive` optional (they
23
+ * are not derivable from `DeviceSessionState`).
24
+ *
25
+ * `usersById` is accepted for signature symmetry with `activeUserOf` even
26
+ * though `ClientSession` only stores `userId` — a session is still
27
+ * projected for an account whose id is absent from `usersById` (no
28
+ * placeholder user is fabricated).
29
+ */
30
+ export function deviceStateToClientSessions(
31
+ state: DeviceSessionState,
32
+ usersById: Map<string, User>,
33
+ ): ClientSession[] {
34
+ const provisionalTimestamp = new Date(state.updatedAt).toISOString();
35
+ return state.accounts.map((account) => ({
36
+ sessionId: account.sessionId,
37
+ deviceId: state.deviceId,
38
+ // provisional: Fase 3-B rewrites ClientSession to make expiresAt/lastActive
39
+ // optional (they are not in DeviceSessionState)
40
+ expiresAt: provisionalTimestamp,
41
+ lastActive: provisionalTimestamp,
42
+ userId: account.accountId,
43
+ isCurrent: account.accountId === state.activeAccountId,
44
+ authuser: account.authuser,
45
+ }));
46
+ }
47
+
48
+ /**
49
+ * The active account's `sessionId`, or `null` when there is no state or no
50
+ * active account is set.
51
+ */
52
+ export function activeSessionIdOf(state: DeviceSessionState | null): string | null {
53
+ if (state === null || state.activeAccountId === null) {
54
+ return null;
55
+ }
56
+ const activeAccountId = state.activeAccountId;
57
+ const activeAccount = state.accounts.find((account) => account.accountId === activeAccountId);
58
+ return activeAccount?.sessionId ?? null;
59
+ }
60
+
61
+ /**
62
+ * The active account's `User`, resolved from `usersById`. `null` when there
63
+ * is no state, no active account is set, or the active account id is absent
64
+ * from `usersById`.
65
+ */
66
+ export function activeUserOf(
67
+ state: DeviceSessionState | null,
68
+ usersById: Map<string, User>,
69
+ ): User | null {
70
+ if (state === null || state.activeAccountId === null) {
71
+ return null;
72
+ }
73
+ return usersById.get(state.activeAccountId) ?? null;
74
+ }
75
+
76
+ /**
77
+ * All account ids in `state`, suitable for an `oxyServices.getUsersByIds(...)`
78
+ * fetch. `[]` for `null` state.
79
+ */
80
+ export function accountIdsOf(state: DeviceSessionState | null): string[] {
81
+ if (state === null) {
82
+ return [];
83
+ }
84
+ return state.accounts.map((account) => account.accountId);
85
+ }
@@ -0,0 +1,27 @@
1
+ import type { OxyServices, SessionClientHost } from '@oxyhq/core';
2
+
3
+ /**
4
+ * Thin `SessionClientHost` adapter over an `OxyServices` instance.
5
+ *
6
+ * `SessionClient` (in `@oxyhq/core`) is host-agnostic: it only needs a REST +
7
+ * token surface. `OxyServices` already exposes all of that except
8
+ * `getCurrentAccountId`, which has no direct equivalent — the adapter holds a
9
+ * mutable ref set by the caller (`OxyContext`, in Fase 3-B) via
10
+ * `setCurrentAccountId`.
11
+ */
12
+ export function createSessionClientHost(
13
+ oxyServices: OxyServices,
14
+ ): SessionClientHost & { setCurrentAccountId(id: string | null): void } {
15
+ let currentAccountId: string | null = null;
16
+ return {
17
+ makeRequest: (method, url, data, options) => oxyServices.makeRequest(method, url, data, options),
18
+ getBaseURL: () => oxyServices.getBaseURL(),
19
+ getAccessToken: () => oxyServices.getAccessToken(),
20
+ onTokensChanged: (listener) => oxyServices.onTokensChanged(listener),
21
+ setTokens: (accessToken) => oxyServices.setTokens(accessToken),
22
+ getCurrentAccountId: () => currentAccountId,
23
+ setCurrentAccountId: (id) => {
24
+ currentAccountId = id;
25
+ },
26
+ };
27
+ }
@@ -0,0 +1,76 @@
1
+ import type { DeviceSessionState } from '@oxyhq/contracts';
2
+ import type { OxyServices, TokenTransport } from '@oxyhq/core';
3
+ import { logger } from '@oxyhq/core';
4
+ import { isWebBrowser } from '../hooks/useWebSSO';
5
+
6
+ /**
7
+ * Platform `TokenTransport` for `SessionClient`: mints an access token when
8
+ * none is currently held, reusing the SAME primitives `OxyContext`'s cold
9
+ * boot already relies on (never re-implemented here):
10
+ *
11
+ * - web: `oxyServices.silentSignIn()` (per-apex `/auth/silent` iframe).
12
+ * - native: `oxyServices.signInWithSharedIdentity()` (app-group keychain).
13
+ *
14
+ * Both primitives plant the token internally on success (the "Sign-In Token
15
+ * Planting" rule) — `ensureActiveToken` never calls `setTokens` itself.
16
+ *
17
+ * `ensureActiveToken` treats a PRESENT token as sufficient in this phase; it
18
+ * does not decode/match the token's subject against `state.activeAccountId`
19
+ * (that refinement is Fase 3-B). Account switching itself is server-driven
20
+ * via the `activeToken` carried in the sync envelope — this transport is
21
+ * only the fallback used when the envelope carried no token.
22
+ *
23
+ * A failed mint is logged and swallowed: it must never throw out of
24
+ * `ensureActiveToken`, since that would crash the caller (the SessionClient
25
+ * socket handler in Fase 3-B).
26
+ */
27
+ export function createTokenTransport(oxyServices: OxyServices): TokenTransport {
28
+ // Coalesces concurrent mints: `SessionClient.applyState` can fire
29
+ // `ensureActiveToken` on rapid successive state pushes; a second call while a
30
+ // mint is already in flight must reuse it, not spawn a second silent iframe /
31
+ // shared-key challenge round-trip.
32
+ let inFlightMint: Promise<void> | null = null;
33
+
34
+ return {
35
+ async ensureActiveToken(state: DeviceSessionState): Promise<void> {
36
+ // Read the current token defensively: it is an in-memory getter that
37
+ // should never throw, but the documented contract is that this method
38
+ // never throws out — so a surprising throw is logged and treated as "no
39
+ // token" (fall through to mint) rather than rejecting the promise.
40
+ try {
41
+ if (oxyServices.getAccessToken()) {
42
+ return;
43
+ }
44
+ } catch (error) {
45
+ logger.warn('ensureActiveToken: getAccessToken threw', { component: 'TokenTransport' }, error);
46
+ }
47
+
48
+ if (inFlightMint) {
49
+ return inFlightMint;
50
+ }
51
+
52
+ inFlightMint = (async () => {
53
+ try {
54
+ const session = isWebBrowser()
55
+ ? await oxyServices.silentSignIn()
56
+ : await oxyServices.signInWithSharedIdentity();
57
+
58
+ if (!session) {
59
+ logger.debug('ensureActiveToken: platform mint returned no session', {
60
+ component: 'TokenTransport',
61
+ deviceId: state.deviceId,
62
+ });
63
+ }
64
+ } catch (error) {
65
+ logger.warn('ensureActiveToken: mint failed', { component: 'TokenTransport' }, error);
66
+ }
67
+ })();
68
+
69
+ try {
70
+ await inFlightMint;
71
+ } finally {
72
+ inFlightMint = null;
73
+ }
74
+ },
75
+ };
76
+ }