@oxyhq/services 14.1.0 → 15.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 (262) hide show
  1. package/lib/commonjs/index.js +0 -17
  2. package/lib/commonjs/index.js.map +1 -1
  3. package/lib/commonjs/ui/components/SignInAccountChooser.js +189 -0
  4. package/lib/commonjs/ui/components/SignInAccountChooser.js.map +1 -0
  5. package/lib/commonjs/ui/components/SignInModal.js +350 -131
  6. package/lib/commonjs/ui/components/SignInModal.js.map +1 -1
  7. package/lib/commonjs/ui/context/OxyContext.js +340 -1547
  8. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  9. package/lib/commonjs/ui/context/hooks/useAuthOperations.js +61 -64
  10. package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
  11. package/lib/commonjs/ui/hooks/useAuth.js +14 -33
  12. package/lib/commonjs/ui/hooks/useAuth.js.map +1 -1
  13. package/lib/commonjs/ui/hooks/usePasswordSignIn.js +170 -0
  14. package/lib/commonjs/ui/hooks/usePasswordSignIn.js.map +1 -0
  15. package/lib/commonjs/ui/hooks/useProfileEditing.js +2 -5
  16. package/lib/commonjs/ui/hooks/useProfileEditing.js.map +1 -1
  17. package/lib/commonjs/ui/hooks/useSessionManagement.js +2 -2
  18. package/lib/commonjs/ui/hooks/useSessionManagement.js.map +1 -1
  19. package/lib/commonjs/ui/navigation/routes.js +1 -0
  20. package/lib/commonjs/ui/navigation/routes.js.map +1 -1
  21. package/lib/commonjs/ui/screens/EditProfileFieldScreen.js +93 -68
  22. package/lib/commonjs/ui/screens/EditProfileFieldScreen.js.map +1 -1
  23. package/lib/commonjs/ui/screens/EditProfileScreen.js +187 -0
  24. package/lib/commonjs/ui/screens/EditProfileScreen.js.map +1 -0
  25. package/lib/commonjs/ui/screens/ManageAccountScreen.js +3 -7
  26. package/lib/commonjs/ui/screens/ManageAccountScreen.js.map +1 -1
  27. package/lib/commonjs/ui/screens/OxyAuthScreen.js +231 -95
  28. package/lib/commonjs/ui/screens/OxyAuthScreen.js.map +1 -1
  29. package/lib/commonjs/ui/screens/ProfileScreen.js +15 -2
  30. package/lib/commonjs/ui/screens/ProfileScreen.js.map +1 -1
  31. package/lib/commonjs/ui/screens/linkFormat.js +38 -0
  32. package/lib/commonjs/ui/screens/linkFormat.js.map +1 -0
  33. package/lib/commonjs/ui/session/authStore.js +146 -0
  34. package/lib/commonjs/ui/session/authStore.js.map +1 -0
  35. package/lib/commonjs/ui/session/createSessionClient.js +23 -17
  36. package/lib/commonjs/ui/session/createSessionClient.js.map +1 -1
  37. package/lib/commonjs/ui/session/index.js +13 -7
  38. package/lib/commonjs/ui/session/index.js.map +1 -1
  39. package/lib/commonjs/ui/session/tokenTransport.js +19 -31
  40. package/lib/commonjs/ui/session/tokenTransport.js.map +1 -1
  41. package/lib/commonjs/ui/utils/isWebBrowser.js +13 -0
  42. package/lib/commonjs/ui/utils/isWebBrowser.js.map +1 -0
  43. package/lib/commonjs/utils/deviceFlowSignIn.js +13 -16
  44. package/lib/commonjs/utils/deviceFlowSignIn.js.map +1 -1
  45. package/lib/module/index.js +0 -5
  46. package/lib/module/index.js.map +1 -1
  47. package/lib/module/ui/components/SignInAccountChooser.js +183 -0
  48. package/lib/module/ui/components/SignInAccountChooser.js.map +1 -0
  49. package/lib/module/ui/components/SignInModal.js +352 -134
  50. package/lib/module/ui/components/SignInModal.js.map +1 -1
  51. package/lib/module/ui/context/OxyContext.js +342 -1551
  52. package/lib/module/ui/context/OxyContext.js.map +1 -1
  53. package/lib/module/ui/context/hooks/useAuthOperations.js +60 -63
  54. package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
  55. package/lib/module/ui/hooks/useAuth.js +14 -33
  56. package/lib/module/ui/hooks/useAuth.js.map +1 -1
  57. package/lib/module/ui/hooks/usePasswordSignIn.js +165 -0
  58. package/lib/module/ui/hooks/usePasswordSignIn.js.map +1 -0
  59. package/lib/module/ui/hooks/useProfileEditing.js +2 -5
  60. package/lib/module/ui/hooks/useProfileEditing.js.map +1 -1
  61. package/lib/module/ui/hooks/useSessionManagement.js +1 -1
  62. package/lib/module/ui/hooks/useSessionManagement.js.map +1 -1
  63. package/lib/module/ui/navigation/routes.js +1 -0
  64. package/lib/module/ui/navigation/routes.js.map +1 -1
  65. package/lib/module/ui/screens/EditProfileFieldScreen.js +91 -66
  66. package/lib/module/ui/screens/EditProfileFieldScreen.js.map +1 -1
  67. package/lib/module/ui/screens/EditProfileScreen.js +182 -0
  68. package/lib/module/ui/screens/EditProfileScreen.js.map +1 -0
  69. package/lib/module/ui/screens/ManageAccountScreen.js +3 -7
  70. package/lib/module/ui/screens/ManageAccountScreen.js.map +1 -1
  71. package/lib/module/ui/screens/OxyAuthScreen.js +233 -97
  72. package/lib/module/ui/screens/OxyAuthScreen.js.map +1 -1
  73. package/lib/module/ui/screens/ProfileScreen.js +15 -2
  74. package/lib/module/ui/screens/ProfileScreen.js.map +1 -1
  75. package/lib/module/ui/screens/linkFormat.js +31 -0
  76. package/lib/module/ui/screens/linkFormat.js.map +1 -0
  77. package/lib/module/ui/session/authStore.js +143 -0
  78. package/lib/module/ui/session/authStore.js.map +1 -0
  79. package/lib/module/ui/session/createSessionClient.js +22 -16
  80. package/lib/module/ui/session/createSessionClient.js.map +1 -1
  81. package/lib/module/ui/session/index.js +9 -9
  82. package/lib/module/ui/session/index.js.map +1 -1
  83. package/lib/module/ui/session/tokenTransport.js +20 -33
  84. package/lib/module/ui/session/tokenTransport.js.map +1 -1
  85. package/lib/module/ui/utils/isWebBrowser.js +11 -0
  86. package/lib/module/ui/utils/isWebBrowser.js.map +1 -0
  87. package/lib/module/utils/deviceFlowSignIn.js +14 -17
  88. package/lib/module/utils/deviceFlowSignIn.js.map +1 -1
  89. package/lib/typescript/commonjs/index.d.ts +0 -1
  90. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  91. package/lib/typescript/commonjs/ui/components/SignInAccountChooser.d.ts +29 -0
  92. package/lib/typescript/commonjs/ui/components/SignInAccountChooser.d.ts.map +1 -0
  93. package/lib/typescript/commonjs/ui/components/SignInModal.d.ts +15 -10
  94. package/lib/typescript/commonjs/ui/components/SignInModal.d.ts.map +1 -1
  95. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts +36 -42
  96. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
  97. package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts +18 -23
  98. package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  99. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts +0 -2
  100. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  101. package/lib/typescript/commonjs/ui/hooks/useAuth.d.ts.map +1 -1
  102. package/lib/typescript/commonjs/ui/hooks/usePasswordSignIn.d.ts +31 -0
  103. package/lib/typescript/commonjs/ui/hooks/usePasswordSignIn.d.ts.map +1 -0
  104. package/lib/typescript/commonjs/ui/hooks/useProfileEditing.d.ts +0 -1
  105. package/lib/typescript/commonjs/ui/hooks/useProfileEditing.d.ts.map +1 -1
  106. package/lib/typescript/commonjs/ui/navigation/routes.d.ts +1 -1
  107. package/lib/typescript/commonjs/ui/navigation/routes.d.ts.map +1 -1
  108. package/lib/typescript/commonjs/ui/screens/EditProfileFieldScreen.d.ts.map +1 -1
  109. package/lib/typescript/commonjs/ui/screens/EditProfileScreen.d.ts +14 -0
  110. package/lib/typescript/commonjs/ui/screens/EditProfileScreen.d.ts.map +1 -0
  111. package/lib/typescript/commonjs/ui/screens/ManageAccountScreen.d.ts +1 -1
  112. package/lib/typescript/commonjs/ui/screens/ManageAccountScreen.d.ts.map +1 -1
  113. package/lib/typescript/commonjs/ui/screens/OxyAuthScreen.d.ts +13 -12
  114. package/lib/typescript/commonjs/ui/screens/OxyAuthScreen.d.ts.map +1 -1
  115. package/lib/typescript/commonjs/ui/screens/ProfileScreen.d.ts.map +1 -1
  116. package/lib/typescript/commonjs/ui/screens/linkFormat.d.ts +23 -0
  117. package/lib/typescript/commonjs/ui/screens/linkFormat.d.ts.map +1 -0
  118. package/lib/typescript/commonjs/ui/session/authStore.d.ts +33 -0
  119. package/lib/typescript/commonjs/ui/session/authStore.d.ts.map +1 -0
  120. package/lib/typescript/commonjs/ui/session/createSessionClient.d.ts +19 -15
  121. package/lib/typescript/commonjs/ui/session/createSessionClient.d.ts.map +1 -1
  122. package/lib/typescript/commonjs/ui/session/index.d.ts +9 -9
  123. package/lib/typescript/commonjs/ui/session/index.d.ts.map +1 -1
  124. package/lib/typescript/commonjs/ui/session/tokenTransport.d.ts +13 -19
  125. package/lib/typescript/commonjs/ui/session/tokenTransport.d.ts.map +1 -1
  126. package/lib/typescript/commonjs/ui/utils/isWebBrowser.d.ts +9 -0
  127. package/lib/typescript/commonjs/ui/utils/isWebBrowser.d.ts.map +1 -0
  128. package/lib/typescript/commonjs/utils/deviceFlowSignIn.d.ts +15 -13
  129. package/lib/typescript/commonjs/utils/deviceFlowSignIn.d.ts.map +1 -1
  130. package/lib/typescript/module/index.d.ts +0 -1
  131. package/lib/typescript/module/index.d.ts.map +1 -1
  132. package/lib/typescript/module/ui/components/SignInAccountChooser.d.ts +29 -0
  133. package/lib/typescript/module/ui/components/SignInAccountChooser.d.ts.map +1 -0
  134. package/lib/typescript/module/ui/components/SignInModal.d.ts +15 -10
  135. package/lib/typescript/module/ui/components/SignInModal.d.ts.map +1 -1
  136. package/lib/typescript/module/ui/context/OxyContext.d.ts +36 -42
  137. package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
  138. package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts +18 -23
  139. package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  140. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts +0 -2
  141. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  142. package/lib/typescript/module/ui/hooks/useAuth.d.ts.map +1 -1
  143. package/lib/typescript/module/ui/hooks/usePasswordSignIn.d.ts +31 -0
  144. package/lib/typescript/module/ui/hooks/usePasswordSignIn.d.ts.map +1 -0
  145. package/lib/typescript/module/ui/hooks/useProfileEditing.d.ts +0 -1
  146. package/lib/typescript/module/ui/hooks/useProfileEditing.d.ts.map +1 -1
  147. package/lib/typescript/module/ui/navigation/routes.d.ts +1 -1
  148. package/lib/typescript/module/ui/navigation/routes.d.ts.map +1 -1
  149. package/lib/typescript/module/ui/screens/EditProfileFieldScreen.d.ts.map +1 -1
  150. package/lib/typescript/module/ui/screens/EditProfileScreen.d.ts +14 -0
  151. package/lib/typescript/module/ui/screens/EditProfileScreen.d.ts.map +1 -0
  152. package/lib/typescript/module/ui/screens/ManageAccountScreen.d.ts +1 -1
  153. package/lib/typescript/module/ui/screens/ManageAccountScreen.d.ts.map +1 -1
  154. package/lib/typescript/module/ui/screens/OxyAuthScreen.d.ts +13 -12
  155. package/lib/typescript/module/ui/screens/OxyAuthScreen.d.ts.map +1 -1
  156. package/lib/typescript/module/ui/screens/ProfileScreen.d.ts.map +1 -1
  157. package/lib/typescript/module/ui/screens/linkFormat.d.ts +23 -0
  158. package/lib/typescript/module/ui/screens/linkFormat.d.ts.map +1 -0
  159. package/lib/typescript/module/ui/session/authStore.d.ts +33 -0
  160. package/lib/typescript/module/ui/session/authStore.d.ts.map +1 -0
  161. package/lib/typescript/module/ui/session/createSessionClient.d.ts +19 -15
  162. package/lib/typescript/module/ui/session/createSessionClient.d.ts.map +1 -1
  163. package/lib/typescript/module/ui/session/index.d.ts +9 -9
  164. package/lib/typescript/module/ui/session/index.d.ts.map +1 -1
  165. package/lib/typescript/module/ui/session/tokenTransport.d.ts +13 -19
  166. package/lib/typescript/module/ui/session/tokenTransport.d.ts.map +1 -1
  167. package/lib/typescript/module/ui/utils/isWebBrowser.d.ts +9 -0
  168. package/lib/typescript/module/ui/utils/isWebBrowser.d.ts.map +1 -0
  169. package/lib/typescript/module/utils/deviceFlowSignIn.d.ts +15 -13
  170. package/lib/typescript/module/utils/deviceFlowSignIn.d.ts.map +1 -1
  171. package/package.json +4 -4
  172. package/src/index.ts +0 -4
  173. package/src/ui/components/SignInAccountChooser.tsx +162 -0
  174. package/src/ui/components/SignInModal.tsx +309 -139
  175. package/src/ui/context/OxyContext.tsx +532 -1785
  176. package/src/ui/context/hooks/useAuthOperations.ts +65 -76
  177. package/src/ui/hooks/useAuth.ts +14 -35
  178. package/src/ui/hooks/usePasswordSignIn.ts +207 -0
  179. package/src/ui/hooks/useProfileEditing.ts +2 -8
  180. package/src/ui/hooks/useSessionManagement.ts +1 -1
  181. package/src/ui/navigation/routes.ts +2 -0
  182. package/src/ui/screens/EditProfileFieldScreen.tsx +102 -66
  183. package/src/ui/screens/EditProfileScreen.tsx +155 -0
  184. package/src/ui/screens/ManageAccountScreen.tsx +3 -7
  185. package/src/ui/screens/OxyAuthScreen.tsx +259 -112
  186. package/src/ui/screens/ProfileScreen.tsx +15 -2
  187. package/src/ui/screens/__tests__/linkFormat.test.ts +73 -0
  188. package/src/ui/screens/linkFormat.ts +37 -0
  189. package/src/ui/session/__tests__/createSessionClient.test.ts +22 -7
  190. package/src/ui/session/__tests__/tokenTransport.test.ts +37 -49
  191. package/src/ui/session/authStore.ts +164 -0
  192. package/src/ui/session/createSessionClient.ts +30 -17
  193. package/src/ui/session/index.ts +10 -9
  194. package/src/ui/session/tokenTransport.ts +26 -36
  195. package/src/ui/utils/isWebBrowser.ts +8 -0
  196. package/src/utils/__tests__/deviceFlowSignIn.test.ts +50 -131
  197. package/src/utils/deviceFlowSignIn.ts +19 -26
  198. package/lib/commonjs/ui/context/inSessionTokenRefresh.js +0 -243
  199. package/lib/commonjs/ui/context/inSessionTokenRefresh.js.map +0 -1
  200. package/lib/commonjs/ui/context/silentSessionRestore.js +0 -56
  201. package/lib/commonjs/ui/context/silentSessionRestore.js.map +0 -1
  202. package/lib/commonjs/ui/hooks/useWebSSO.js +0 -28
  203. package/lib/commonjs/ui/hooks/useWebSSO.js.map +0 -1
  204. package/lib/commonjs/ui/session/projectSessionState.js +0 -86
  205. package/lib/commonjs/ui/session/projectSessionState.js.map +0 -1
  206. package/lib/commonjs/ui/session/sessionClientHost.js +0 -30
  207. package/lib/commonjs/ui/session/sessionClientHost.js.map +0 -1
  208. package/lib/commonjs/ui/utils/activeAuthuser.js +0 -142
  209. package/lib/commonjs/ui/utils/activeAuthuser.js.map +0 -1
  210. package/lib/commonjs/utils/crossApex.js +0 -74
  211. package/lib/commonjs/utils/crossApex.js.map +0 -1
  212. package/lib/module/ui/context/inSessionTokenRefresh.js +0 -238
  213. package/lib/module/ui/context/inSessionTokenRefresh.js.map +0 -1
  214. package/lib/module/ui/context/silentSessionRestore.js +0 -53
  215. package/lib/module/ui/context/silentSessionRestore.js.map +0 -1
  216. package/lib/module/ui/hooks/useWebSSO.js +0 -24
  217. package/lib/module/ui/hooks/useWebSSO.js.map +0 -1
  218. package/lib/module/ui/session/projectSessionState.js +0 -79
  219. package/lib/module/ui/session/projectSessionState.js.map +0 -1
  220. package/lib/module/ui/session/sessionClientHost.js +0 -26
  221. package/lib/module/ui/session/sessionClientHost.js.map +0 -1
  222. package/lib/module/ui/utils/activeAuthuser.js +0 -134
  223. package/lib/module/ui/utils/activeAuthuser.js.map +0 -1
  224. package/lib/module/utils/crossApex.js +0 -69
  225. package/lib/module/utils/crossApex.js.map +0 -1
  226. package/lib/typescript/commonjs/ui/context/inSessionTokenRefresh.d.ts +0 -106
  227. package/lib/typescript/commonjs/ui/context/inSessionTokenRefresh.d.ts.map +0 -1
  228. package/lib/typescript/commonjs/ui/context/silentSessionRestore.d.ts +0 -36
  229. package/lib/typescript/commonjs/ui/context/silentSessionRestore.d.ts.map +0 -1
  230. package/lib/typescript/commonjs/ui/hooks/useWebSSO.d.ts +0 -19
  231. package/lib/typescript/commonjs/ui/hooks/useWebSSO.d.ts.map +0 -1
  232. package/lib/typescript/commonjs/ui/session/projectSessionState.d.ts +0 -45
  233. package/lib/typescript/commonjs/ui/session/projectSessionState.d.ts.map +0 -1
  234. package/lib/typescript/commonjs/ui/session/sessionClientHost.d.ts +0 -14
  235. package/lib/typescript/commonjs/ui/session/sessionClientHost.d.ts.map +0 -1
  236. package/lib/typescript/commonjs/ui/utils/activeAuthuser.d.ts +0 -67
  237. package/lib/typescript/commonjs/ui/utils/activeAuthuser.d.ts.map +0 -1
  238. package/lib/typescript/commonjs/utils/crossApex.d.ts +0 -55
  239. package/lib/typescript/commonjs/utils/crossApex.d.ts.map +0 -1
  240. package/lib/typescript/module/ui/context/inSessionTokenRefresh.d.ts +0 -106
  241. package/lib/typescript/module/ui/context/inSessionTokenRefresh.d.ts.map +0 -1
  242. package/lib/typescript/module/ui/context/silentSessionRestore.d.ts +0 -36
  243. package/lib/typescript/module/ui/context/silentSessionRestore.d.ts.map +0 -1
  244. package/lib/typescript/module/ui/hooks/useWebSSO.d.ts +0 -19
  245. package/lib/typescript/module/ui/hooks/useWebSSO.d.ts.map +0 -1
  246. package/lib/typescript/module/ui/session/projectSessionState.d.ts +0 -45
  247. package/lib/typescript/module/ui/session/projectSessionState.d.ts.map +0 -1
  248. package/lib/typescript/module/ui/session/sessionClientHost.d.ts +0 -14
  249. package/lib/typescript/module/ui/session/sessionClientHost.d.ts.map +0 -1
  250. package/lib/typescript/module/ui/utils/activeAuthuser.d.ts +0 -67
  251. package/lib/typescript/module/ui/utils/activeAuthuser.d.ts.map +0 -1
  252. package/lib/typescript/module/utils/crossApex.d.ts +0 -55
  253. package/lib/typescript/module/utils/crossApex.d.ts.map +0 -1
  254. package/src/ui/context/inSessionTokenRefresh.ts +0 -264
  255. package/src/ui/context/silentSessionRestore.ts +0 -53
  256. package/src/ui/hooks/useWebSSO.ts +0 -23
  257. package/src/ui/session/__tests__/projectSessionState.test.ts +0 -114
  258. package/src/ui/session/__tests__/sessionClientHost.test.ts +0 -43
  259. package/src/ui/session/projectSessionState.ts +0 -85
  260. package/src/ui/session/sessionClientHost.ts +0 -27
  261. package/src/ui/utils/activeAuthuser.ts +0 -142
  262. package/src/utils/crossApex.ts +0 -75
@@ -1,19 +1,22 @@
1
1
  import type { DeviceSessionState } from '@oxyhq/contracts';
2
- import { logger } from '@oxyhq/core';
2
+ import { logger, createMemoryAuthStateStore, refreshPersistedSession } from '@oxyhq/core';
3
3
  import { createTokenTransport } from '../tokenTransport';
4
- import { isWebBrowser } from '../../hooks/useWebSSO';
5
4
 
6
- jest.mock('../../hooks/useWebSSO', () => ({
7
- isWebBrowser: jest.fn(),
8
- }));
5
+ // The device-first transport mints a fallback token by rotating the persisted
6
+ // refresh family via `refreshPersistedSession` (the ONE unified refresh path)
7
+ // there is no `silentSignIn`/`signInWithSharedIdentity` arm anymore. Mock that
8
+ // one core function so the transport's own coalescing / logging contract can be
9
+ // exercised in isolation; keep the real `logger` + memory store.
10
+ jest.mock('@oxyhq/core', () => {
11
+ const actual = jest.requireActual('@oxyhq/core');
12
+ return { __esModule: true, ...actual, refreshPersistedSession: jest.fn() };
13
+ });
9
14
 
10
- const mockedIsWebBrowser = isWebBrowser as jest.MockedFunction<typeof isWebBrowser>;
15
+ const mockedRefresh = refreshPersistedSession as jest.MockedFunction<typeof refreshPersistedSession>;
11
16
 
12
17
  function fakeOxy(accessToken: string | null) {
13
18
  return {
14
19
  getAccessToken: jest.fn().mockReturnValue(accessToken),
15
- silentSignIn: jest.fn().mockResolvedValue(null),
16
- signInWithSharedIdentity: jest.fn().mockResolvedValue(null),
17
20
  };
18
21
  }
19
22
 
@@ -28,51 +31,39 @@ const state: DeviceSessionState = {
28
31
  describe('createTokenTransport', () => {
29
32
  beforeEach(() => {
30
33
  jest.clearAllMocks();
34
+ mockedRefresh.mockResolvedValue('minted-token');
31
35
  });
32
36
 
33
37
  test('no-ops when a token is already present', async () => {
34
38
  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);
39
+ const transport = createTokenTransport(oxy as never, createMemoryAuthStateStore());
47
40
 
48
41
  await transport.ensureActiveToken(state);
49
42
 
50
- expect(oxy.silentSignIn).toHaveBeenCalledTimes(1);
51
- expect(oxy.signInWithSharedIdentity).not.toHaveBeenCalled();
43
+ expect(mockedRefresh).not.toHaveBeenCalled();
52
44
  });
53
45
 
54
- test('mints via signInWithSharedIdentity off web when no token is present', async () => {
55
- mockedIsWebBrowser.mockReturnValue(false);
46
+ test('mints via refreshPersistedSession when no token is present', async () => {
56
47
  const oxy = fakeOxy(null);
57
- const transport = createTokenTransport(oxy as never);
48
+ const store = createMemoryAuthStateStore();
49
+ const transport = createTokenTransport(oxy as never, store);
58
50
 
59
51
  await transport.ensureActiveToken(state);
60
52
 
61
- expect(oxy.signInWithSharedIdentity).toHaveBeenCalledTimes(1);
62
- expect(oxy.silentSignIn).not.toHaveBeenCalled();
53
+ expect(mockedRefresh).toHaveBeenCalledTimes(1);
54
+ expect(mockedRefresh).toHaveBeenCalledWith({ oxy, store });
63
55
  });
64
56
 
65
- test('resolves (never rejects) and logs a warning when the mint throws', async () => {
66
- mockedIsWebBrowser.mockReturnValue(true);
57
+ test('resolves (never rejects) and logs a warning when the refresh throws', async () => {
67
58
  const oxy = fakeOxy(null);
68
- oxy.silentSignIn.mockRejectedValue(new Error('mint boom'));
69
- const transport = createTokenTransport(oxy as never);
59
+ mockedRefresh.mockRejectedValue(new Error('mint boom'));
60
+ const transport = createTokenTransport(oxy as never, createMemoryAuthStateStore());
70
61
  const warnSpy = jest.spyOn(logger, 'warn').mockImplementation(() => undefined);
71
62
 
72
63
  await expect(transport.ensureActiveToken(state)).resolves.toBeUndefined();
73
64
 
74
65
  expect(warnSpy).toHaveBeenCalledWith(
75
- 'ensureActiveToken: mint failed',
66
+ 'ensureActiveToken: refresh failed',
76
67
  { component: 'TokenTransport' },
77
68
  expect.any(Error),
78
69
  );
@@ -80,52 +71,49 @@ describe('createTokenTransport', () => {
80
71
  warnSpy.mockRestore();
81
72
  });
82
73
 
83
- test('resolves cleanly when the mint returns null', async () => {
84
- mockedIsWebBrowser.mockReturnValue(true);
74
+ test('resolves cleanly when the refresh produces no session', async () => {
85
75
  const oxy = fakeOxy(null);
86
- const transport = createTokenTransport(oxy as never);
76
+ mockedRefresh.mockResolvedValue(null);
77
+ const transport = createTokenTransport(oxy as never, createMemoryAuthStateStore());
87
78
 
88
79
  await expect(transport.ensureActiveToken(state)).resolves.toBeUndefined();
89
- expect(oxy.silentSignIn).toHaveBeenCalledTimes(1);
80
+ expect(mockedRefresh).toHaveBeenCalledTimes(1);
90
81
  });
91
82
 
92
83
  test('coalesces concurrent calls into a single mint', async () => {
93
- mockedIsWebBrowser.mockReturnValue(true);
94
84
  const oxy = fakeOxy(null);
95
- let resolveMint: ((value: null) => void) | null = null;
96
- oxy.silentSignIn.mockImplementation(
85
+ let resolveMint: ((value: string | null) => void) | null = null;
86
+ mockedRefresh.mockImplementation(
97
87
  () =>
98
- new Promise((resolve) => {
88
+ new Promise<string | null>((resolve) => {
99
89
  resolveMint = resolve;
100
90
  }),
101
91
  );
102
- const transport = createTokenTransport(oxy as never);
92
+ const transport = createTokenTransport(oxy as never, createMemoryAuthStateStore());
103
93
 
104
94
  const first = transport.ensureActiveToken(state);
105
95
  const second = transport.ensureActiveToken(state);
106
96
 
107
- expect(oxy.silentSignIn).toHaveBeenCalledTimes(1);
97
+ expect(mockedRefresh).toHaveBeenCalledTimes(1);
108
98
 
109
99
  resolveMint?.(null);
110
100
  await Promise.all([first, second]);
111
101
 
112
- expect(oxy.silentSignIn).toHaveBeenCalledTimes(1);
102
+ expect(mockedRefresh).toHaveBeenCalledTimes(1);
113
103
 
114
- // A later call after the in-flight mint settled starts a fresh mint
115
- // (the mock reassigns `resolveMint` on the second silentSignIn call).
104
+ // A later call after the in-flight mint settled starts a fresh mint.
116
105
  const third = transport.ensureActiveToken(state);
117
- expect(oxy.silentSignIn).toHaveBeenCalledTimes(2);
106
+ expect(mockedRefresh).toHaveBeenCalledTimes(2);
118
107
  resolveMint?.(null);
119
108
  await third;
120
109
  });
121
110
 
122
111
  test('treats a throwing getAccessToken as no-token and still mints', async () => {
123
- mockedIsWebBrowser.mockReturnValue(true);
124
112
  const oxy = fakeOxy(null);
125
113
  oxy.getAccessToken.mockImplementation(() => {
126
114
  throw new Error('storage unavailable');
127
115
  });
128
- const transport = createTokenTransport(oxy as never);
116
+ const transport = createTokenTransport(oxy as never, createMemoryAuthStateStore());
129
117
  const warnSpy = jest.spyOn(logger, 'warn').mockImplementation(() => undefined);
130
118
 
131
119
  await expect(transport.ensureActiveToken(state)).resolves.toBeUndefined();
@@ -135,7 +123,7 @@ describe('createTokenTransport', () => {
135
123
  { component: 'TokenTransport' },
136
124
  expect.any(Error),
137
125
  );
138
- expect(oxy.silentSignIn).toHaveBeenCalledTimes(1);
126
+ expect(mockedRefresh).toHaveBeenCalledTimes(1);
139
127
 
140
128
  warnSpy.mockRestore();
141
129
  });
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Platform `AuthStateStore` for `@oxyhq/services`.
3
+ *
4
+ * The device-first session model persists the rotating refresh-token family
5
+ * head per origin (web) / per device (native). `@oxyhq/core` owns the store
6
+ * shape + logic (`createWebAuthStateStore` / `createNativeAuthStateStore`); this
7
+ * module only supplies the platform storage seam and wires the native device
8
+ * token to the SHARED keychain so every native Oxy app joins one DeviceSession:
9
+ *
10
+ * - web → `createWebAuthStateStore()` (localStorage, in-memory fallback).
11
+ * - native → `createNativeAuthStateStore(secureKV)` for the SESSION blob
12
+ * (refresh token) over `expo-secure-store` (AsyncStorage fallback), with the
13
+ * long-lived DEVICE token delegated to `KeyManager`'s shared keychain
14
+ * (`group.so.oxy.shared`) instead of per-app storage — so an in-app login on
15
+ * any native Oxy app attributes to the SAME server-side DeviceSession.
16
+ *
17
+ * `expo-secure-store` is loaded via a runtime-computed dynamic import (the same
18
+ * optional-native-module pattern `OxyProvider` uses for netinfo / keyboard
19
+ * controller), so the web bundle never pulls it and a device without it falls
20
+ * back to AsyncStorage rather than crashing.
21
+ */
22
+ import {
23
+ KeyManager,
24
+ createWebAuthStateStore,
25
+ createNativeAuthStateStore,
26
+ type AuthStateStore,
27
+ type NativeKeyValueStorage,
28
+ logger,
29
+ } from '@oxyhq/core';
30
+ import { createPlatformStorage, isReactNative } from '../utils/storageHelpers';
31
+
32
+ // Variable indirection so Metro's static analyzer never traces expo-secure-store
33
+ // into the web bundle; the module is native-only and optional.
34
+ const SECURE_STORE_MODULE = 'expo-secure-store';
35
+
36
+ interface SecureStoreLike {
37
+ getItemAsync(key: string): Promise<string | null>;
38
+ setItemAsync(key: string, value: string): Promise<void>;
39
+ deleteItemAsync(key: string): Promise<void>;
40
+ }
41
+
42
+ let secureStorePromise: Promise<SecureStoreLike | null> | null = null;
43
+
44
+ /**
45
+ * Resolve the `expo-secure-store` module once (memoised), or `null` when it is
46
+ * unavailable — callers then fall back to AsyncStorage. Never throws.
47
+ */
48
+ function loadSecureStore(): Promise<SecureStoreLike | null> {
49
+ if (!secureStorePromise) {
50
+ const moduleName = SECURE_STORE_MODULE;
51
+ secureStorePromise = import(moduleName)
52
+ .then((mod: Partial<SecureStoreLike>) => {
53
+ if (
54
+ typeof mod.getItemAsync === 'function' &&
55
+ typeof mod.setItemAsync === 'function' &&
56
+ typeof mod.deleteItemAsync === 'function'
57
+ ) {
58
+ return {
59
+ getItemAsync: mod.getItemAsync.bind(mod),
60
+ setItemAsync: mod.setItemAsync.bind(mod),
61
+ deleteItemAsync: mod.deleteItemAsync.bind(mod),
62
+ } satisfies SecureStoreLike;
63
+ }
64
+ return null;
65
+ })
66
+ .catch(() => null);
67
+ }
68
+ return secureStorePromise;
69
+ }
70
+
71
+ /**
72
+ * A native `NativeKeyValueStorage` that prefers `expo-secure-store` (encrypted
73
+ * at rest) and falls back to AsyncStorage when SecureStore is not installed.
74
+ * The SecureStore module is resolved lazily on first access so construction
75
+ * stays synchronous.
76
+ */
77
+ function createNativeSecureKeyValueStorage(): NativeKeyValueStorage {
78
+ let asyncStorage: Awaited<ReturnType<typeof createPlatformStorage>> | null = null;
79
+ const getAsyncStorage = async () => {
80
+ if (!asyncStorage) {
81
+ asyncStorage = await createPlatformStorage();
82
+ }
83
+ return asyncStorage;
84
+ };
85
+ return {
86
+ getItem: async (key) => {
87
+ const secure = await loadSecureStore();
88
+ if (secure) {
89
+ return secure.getItemAsync(key);
90
+ }
91
+ return (await getAsyncStorage()).getItem(key);
92
+ },
93
+ setItem: async (key, value) => {
94
+ const secure = await loadSecureStore();
95
+ if (secure) {
96
+ await secure.setItemAsync(key, value);
97
+ return;
98
+ }
99
+ await (await getAsyncStorage()).setItem(key, value);
100
+ },
101
+ removeItem: async (key) => {
102
+ const secure = await loadSecureStore();
103
+ if (secure) {
104
+ await secure.deleteItemAsync(key);
105
+ return;
106
+ }
107
+ await (await getAsyncStorage()).removeItem(key);
108
+ },
109
+ };
110
+ }
111
+
112
+ /**
113
+ * Build the platform {@link AuthStateStore} for this runtime.
114
+ *
115
+ * Native additionally routes the device token through the shared keychain so
116
+ * every native Oxy app shares one server-side DeviceSession; the session blob
117
+ * (refresh token) stays per-app in SecureStore. Best-effort keychain access —
118
+ * a locked/failed shared-keychain read degrades to "no device token" rather
119
+ * than throwing out of cold boot / refresh.
120
+ */
121
+ export function createPlatformAuthStateStore(): AuthStateStore {
122
+ if (!isReactNative()) {
123
+ return createWebAuthStateStore();
124
+ }
125
+
126
+ const base = createNativeAuthStateStore(createNativeSecureKeyValueStorage());
127
+ return {
128
+ ...base,
129
+ loadDeviceToken: async () => {
130
+ try {
131
+ return await KeyManager.getSharedDeviceToken();
132
+ } catch (error) {
133
+ logger.debug(
134
+ 'Shared device-token read failed (treating as none)',
135
+ { component: 'authStore', method: 'loadDeviceToken' },
136
+ error,
137
+ );
138
+ return null;
139
+ }
140
+ },
141
+ saveDeviceToken: async (token) => {
142
+ try {
143
+ await KeyManager.setSharedDeviceToken(token);
144
+ } catch (error) {
145
+ logger.debug(
146
+ 'Shared device-token write failed (non-fatal)',
147
+ { component: 'authStore', method: 'saveDeviceToken' },
148
+ error,
149
+ );
150
+ }
151
+ },
152
+ clearDeviceToken: async () => {
153
+ try {
154
+ await KeyManager.clearSharedDeviceToken();
155
+ } catch (error) {
156
+ logger.debug(
157
+ 'Shared device-token clear failed (non-fatal)',
158
+ { component: 'authStore', method: 'clearDeviceToken' },
159
+ error,
160
+ );
161
+ }
162
+ },
163
+ };
164
+ }
@@ -1,29 +1,42 @@
1
- import type { OxyServices } from '@oxyhq/core';
2
- import { SessionClient } from '@oxyhq/core';
3
1
  import { io } from 'socket.io-client';
4
- import { createSessionClientHost } from './sessionClientHost';
2
+ import {
3
+ SessionClient,
4
+ createSessionClientHost,
5
+ type AuthStateStore,
6
+ type OxyServices,
7
+ } from '@oxyhq/core';
5
8
  import { createTokenTransport } from './tokenTransport';
6
9
 
7
10
  /**
8
- * Wires a `SessionClient` for `@oxyhq/services`: builds the `SessionClientHost`
9
- * adapter (Task 1) and the platform `TokenTransport` (Task 3) over the given
10
- * `OxyServices` instance, and returns both the client and the host — the host
11
- * is returned (not just the client) so `OxyContext` (Fase 3-B) can call
12
- * `host.setCurrentAccountId(...)` as the active account changes.
11
+ * Wire a `SessionClient` for `@oxyhq/services`.
13
12
  *
14
- * `socket.io-client`'s `io` is STATICALLY imported and injected as the
15
- * `SessionClient` `socketFactory` (socket.io-client is a real dependency of
16
- * `@oxyhq/services`). This is what keeps realtime session sync working in the
17
- * Metro/Expo-web bundle: core's lazy dynamic `import('socket.io-client')` of a
18
- * bare specifier does not resolve reliably against the published core dist, so
19
- * the app-bundled static import is the reliable source of the factory.
13
+ * The platform-agnostic parts (host adapter, client, projection helpers) live
14
+ * ONCE in `@oxyhq/core`; this thin factory only injects the pieces specific to
15
+ * the RN/Expo SDK:
16
+ *
17
+ * - `socket.io-client`'s `io`, STATICALLY imported (socket.io-client is a real
18
+ * dependency of `@oxyhq/services`) so realtime session sync never relies on
19
+ * core's lazy dynamic import of a bare specifier — bundler-fragile in
20
+ * Metro/Expo-web against the published core dist.
21
+ * - the device-first {@link createTokenTransport}, which mints a fallback token
22
+ * by rotating the persisted refresh family (`store`).
23
+ *
24
+ * `onUnauthenticated` fires when an applied device state has zero accounts (a
25
+ * device signout-all): the provider clears the persisted store + local state so
26
+ * a reload does not try to restore a dead session. The host is returned
27
+ * alongside the client so the caller can call `host.setCurrentAccountId(...)`
28
+ * as the active account changes.
20
29
  */
21
- export function createSessionClient(oxyServices: OxyServices): {
30
+ export function createSessionClient(
31
+ oxyServices: OxyServices,
32
+ store: AuthStateStore,
33
+ onUnauthenticated?: () => void,
34
+ ): {
22
35
  client: SessionClient;
23
36
  host: ReturnType<typeof createSessionClientHost>;
24
37
  } {
25
38
  const host = createSessionClientHost(oxyServices);
26
- const transport = createTokenTransport(oxyServices);
27
- const client = new SessionClient(host, { transport, socketFactory: io });
39
+ const transport = createTokenTransport(oxyServices, store);
40
+ const client = new SessionClient(host, { transport, socketFactory: io, onUnauthenticated });
28
41
  return { client, host };
29
42
  }
@@ -1,19 +1,20 @@
1
1
  /**
2
- * Session-sync integration layer (Fase 3-A) — intra-package use only.
2
+ * Session-sync integration layer — intra-package use only.
3
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.
4
+ * `@oxyhq/core` owns the platform-agnostic `SessionClient`, its host adapter,
5
+ * and the pure `DeviceSessionState services` projection helpers; those are
6
+ * re-exported straight from core here so `OxyContext` has one import site.
7
+ * `@oxyhq/services` supplies only the two platform pieces: the thin
8
+ * `createSessionClient` factory (injects socket.io-client `io` + the
9
+ * device-first token transport) and the `AuthStateStore` factory.
10
10
  */
11
11
  export { createSessionClient } from './createSessionClient';
12
- export { createSessionClientHost } from './sessionClientHost';
13
12
  export { createTokenTransport } from './tokenTransport';
13
+ export { createPlatformAuthStateStore } from './authStore';
14
14
  export {
15
+ createSessionClientHost,
15
16
  accountIdsOf,
16
17
  activeSessionIdOf,
17
18
  activeUserOf,
18
19
  deviceStateToClientSessions,
19
- } from './projectSessionState';
20
+ } from '@oxyhq/core';
@@ -1,42 +1,35 @@
1
+ import {
2
+ refreshPersistedSession,
3
+ logger,
4
+ type AuthStateStore,
5
+ type OxyServices,
6
+ type TokenTransport,
7
+ } from '@oxyhq/core';
1
8
  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
9
 
6
10
  /**
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):
11
+ * Platform `TokenTransport` for `SessionClient` (device-first model).
10
12
  *
11
- * - web: `oxyServices.silentSignIn()` (per-apex `/auth/silent` iframe).
12
- * - native: `oxyServices.signInWithSharedIdentity()` (app-group keychain).
13
+ * `ensureActiveToken` is the fallback the client uses when a `session_state`
14
+ * push arrived WITHOUT an embedded `activeToken` and the app currently holds no
15
+ * bearer. It mints one through the ONE unified refresh path
16
+ * (`refreshPersistedSession`): rotate the persisted refresh-token family
17
+ * (`POST /auth/refresh-token`) and, on native, fall back to the shared-keychain
18
+ * re-mint. There is no FedCM/silent-iframe arm anymore — the per-origin
19
+ * persisted refresh token is the durable web credential.
13
20
  *
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).
21
+ * Concurrent pushes coalesce onto one in-flight mint. A failure is logged and
22
+ * swallowed: this method must never throw out (it runs inside the socket state
23
+ * handler).
26
24
  */
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.
25
+ export function createTokenTransport(
26
+ oxyServices: OxyServices,
27
+ store: AuthStateStore,
28
+ ): TokenTransport {
32
29
  let inFlightMint: Promise<void> | null = null;
33
30
 
34
31
  return {
35
32
  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
33
  try {
41
34
  if (oxyServices.getAccessToken()) {
42
35
  return;
@@ -51,18 +44,15 @@ export function createTokenTransport(oxyServices: OxyServices): TokenTransport {
51
44
 
52
45
  inFlightMint = (async () => {
53
46
  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', {
47
+ const token = await refreshPersistedSession({ oxy: oxyServices, store });
48
+ if (!token) {
49
+ logger.debug('ensureActiveToken: refresh produced no session', {
60
50
  component: 'TokenTransport',
61
51
  deviceId: state.deviceId,
62
52
  });
63
53
  }
64
54
  } catch (error) {
65
- logger.warn('ensureActiveToken: mint failed', { component: 'TokenTransport' }, error);
55
+ logger.warn('ensureActiveToken: refresh failed', { component: 'TokenTransport' }, error);
66
56
  }
67
57
  })();
68
58
 
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Web-browser detection for `@oxyhq/services`.
3
+ *
4
+ * The predicate now lives ONCE in `@oxyhq/core` (`isWebBrowser`) so services and
5
+ * auth-sdk share the exact same DOM probe. This module re-exposes it under the
6
+ * existing internal import path so consumers stay unchanged.
7
+ */
8
+ export { isWebBrowser } from '@oxyhq/core';