@oxyhq/services 28.0.0 → 29.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 (324) hide show
  1. package/NOTICE +10 -9
  2. package/README.md +65 -2
  3. package/android/src/main/java/so/oxy/devicesession/OxyDeviceSessionModule.kt +178 -0
  4. package/android/src/main/java/so/oxy/devicesession/OxyDeviceSessionProvider.kt +99 -0
  5. package/android/src/main/java/so/oxy/devicesession/OxyDeviceSessionStore.kt +115 -0
  6. package/expo-module.config.json +1 -0
  7. package/lib/commonjs/index.js +60 -16
  8. package/lib/commonjs/index.js.map +1 -1
  9. package/lib/commonjs/ui/boot/runProviderColdBoot.js +7 -0
  10. package/lib/commonjs/ui/boot/runProviderColdBoot.js.map +1 -1
  11. package/lib/commonjs/ui/client.js +12 -7
  12. package/lib/commonjs/ui/client.js.map +1 -1
  13. package/lib/commonjs/ui/components/OxyAccountDialogScreen.js +10 -27
  14. package/lib/commonjs/ui/components/OxyAccountDialogScreen.js.map +1 -1
  15. package/lib/commonjs/ui/components/OxyAuthChooser.js +132 -44
  16. package/lib/commonjs/ui/components/OxyAuthChooser.js.map +1 -1
  17. package/lib/commonjs/ui/components/OxyProvider.js +4 -2
  18. package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
  19. package/lib/commonjs/ui/components/SurfaceScreen.js +31 -3
  20. package/lib/commonjs/ui/components/SurfaceScreen.js.map +1 -1
  21. package/lib/commonjs/ui/components/authChooser/AccountsMenuView.js +209 -84
  22. package/lib/commonjs/ui/components/authChooser/AccountsMenuView.js.map +1 -1
  23. package/lib/commonjs/ui/components/authChooser/SignInEntryView.js +25 -7
  24. package/lib/commonjs/ui/components/authChooser/SignInEntryView.js.map +1 -1
  25. package/lib/commonjs/ui/components/authChooser/primitives.js +35 -22
  26. package/lib/commonjs/ui/components/authChooser/primitives.js.map +1 -1
  27. package/lib/commonjs/ui/components/authChooser/types.js +11 -0
  28. package/lib/commonjs/ui/components/authChooser/types.js.map +1 -1
  29. package/lib/commonjs/ui/context/OxyContext.js +257 -306
  30. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  31. package/lib/commonjs/ui/context/hooks/useAuthOperations.js +27 -37
  32. package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
  33. package/lib/commonjs/ui/context/oxyContextHelpers.js +0 -29
  34. package/lib/commonjs/ui/context/oxyContextHelpers.js.map +1 -1
  35. package/lib/commonjs/ui/context/useOxyAccountGraph.js +6 -17
  36. package/lib/commonjs/ui/context/useOxyAccountGraph.js.map +1 -1
  37. package/lib/commonjs/ui/hooks/accountDialogSnapshot.js +42 -0
  38. package/lib/commonjs/ui/hooks/accountDialogSnapshot.js.map +1 -0
  39. package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js +0 -19
  40. package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js.map +1 -1
  41. package/lib/commonjs/ui/hooks/useDeviceSwitcher.js +57 -0
  42. package/lib/commonjs/ui/hooks/useDeviceSwitcher.js.map +1 -0
  43. package/lib/commonjs/ui/hooks/useI18n.js +22 -3
  44. package/lib/commonjs/ui/hooks/useI18n.js.map +1 -1
  45. package/lib/commonjs/ui/hooks/useSessionManagement.js +46 -153
  46. package/lib/commonjs/ui/hooks/useSessionManagement.js.map +1 -1
  47. package/lib/commonjs/ui/index.js +12 -7
  48. package/lib/commonjs/ui/index.js.map +1 -1
  49. package/lib/commonjs/ui/navigation/routes.js +85 -73
  50. package/lib/commonjs/ui/navigation/routes.js.map +1 -1
  51. package/lib/commonjs/ui/navigation/surfaces.js +8 -7
  52. package/lib/commonjs/ui/navigation/surfaces.js.map +1 -1
  53. package/lib/commonjs/ui/runtime/createOxyRuntime.js +433 -0
  54. package/lib/commonjs/ui/runtime/createOxyRuntime.js.map +1 -0
  55. package/lib/commonjs/ui/runtime/index.js +63 -0
  56. package/lib/commonjs/ui/runtime/index.js.map +1 -0
  57. package/lib/commonjs/ui/runtime/runtimeContext.js +83 -0
  58. package/lib/commonjs/ui/runtime/runtimeContext.js.map +1 -0
  59. package/lib/commonjs/ui/runtime/types.js +6 -0
  60. package/lib/commonjs/ui/runtime/types.js.map +1 -0
  61. package/lib/commonjs/ui/runtime/useRuntimeSnapshot.js +57 -0
  62. package/lib/commonjs/ui/runtime/useRuntimeSnapshot.js.map +1 -0
  63. package/lib/commonjs/ui/server.js +1 -2
  64. package/lib/commonjs/ui/server.js.map +1 -1
  65. package/lib/commonjs/ui/session/authStore.js +45 -9
  66. package/lib/commonjs/ui/session/authStore.js.map +1 -1
  67. package/lib/commonjs/ui/session/sharedDeviceCredentialStore.js +306 -0
  68. package/lib/commonjs/ui/session/sharedDeviceCredentialStore.js.map +1 -0
  69. package/lib/commonjs/ui/stores/authStore.js +154 -55
  70. package/lib/commonjs/ui/stores/authStore.js.map +1 -1
  71. package/lib/commonjs/ui/utils/avatarUtils.js +2 -36
  72. package/lib/commonjs/ui/utils/avatarUtils.js.map +1 -1
  73. package/lib/module/index.js +16 -8
  74. package/lib/module/index.js.map +1 -1
  75. package/lib/module/ui/boot/runProviderColdBoot.js +7 -0
  76. package/lib/module/ui/boot/runProviderColdBoot.js.map +1 -1
  77. package/lib/module/ui/client.js +1 -2
  78. package/lib/module/ui/client.js.map +1 -1
  79. package/lib/module/ui/components/OxyAccountDialogScreen.js +10 -27
  80. package/lib/module/ui/components/OxyAccountDialogScreen.js.map +1 -1
  81. package/lib/module/ui/components/OxyAuthChooser.js +134 -44
  82. package/lib/module/ui/components/OxyAuthChooser.js.map +1 -1
  83. package/lib/module/ui/components/OxyProvider.js +5 -3
  84. package/lib/module/ui/components/OxyProvider.js.map +1 -1
  85. package/lib/module/ui/components/SurfaceScreen.js +33 -5
  86. package/lib/module/ui/components/SurfaceScreen.js.map +1 -1
  87. package/lib/module/ui/components/authChooser/AccountsMenuView.js +210 -85
  88. package/lib/module/ui/components/authChooser/AccountsMenuView.js.map +1 -1
  89. package/lib/module/ui/components/authChooser/SignInEntryView.js +25 -7
  90. package/lib/module/ui/components/authChooser/SignInEntryView.js.map +1 -1
  91. package/lib/module/ui/components/authChooser/primitives.js +35 -22
  92. package/lib/module/ui/components/authChooser/primitives.js.map +1 -1
  93. package/lib/module/ui/components/authChooser/types.js +11 -0
  94. package/lib/module/ui/components/authChooser/types.js.map +1 -1
  95. package/lib/module/ui/context/OxyContext.js +251 -303
  96. package/lib/module/ui/context/OxyContext.js.map +1 -1
  97. package/lib/module/ui/context/hooks/useAuthOperations.js +27 -37
  98. package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
  99. package/lib/module/ui/context/oxyContextHelpers.js +0 -28
  100. package/lib/module/ui/context/oxyContextHelpers.js.map +1 -1
  101. package/lib/module/ui/context/useOxyAccountGraph.js +6 -17
  102. package/lib/module/ui/context/useOxyAccountGraph.js.map +1 -1
  103. package/lib/module/ui/hooks/accountDialogSnapshot.js +38 -0
  104. package/lib/module/ui/hooks/accountDialogSnapshot.js.map +1 -0
  105. package/lib/module/ui/hooks/mutations/useAccountMutations.js +0 -19
  106. package/lib/module/ui/hooks/mutations/useAccountMutations.js.map +1 -1
  107. package/lib/module/ui/hooks/useDeviceSwitcher.js +53 -0
  108. package/lib/module/ui/hooks/useDeviceSwitcher.js.map +1 -0
  109. package/lib/module/ui/hooks/useI18n.js +24 -4
  110. package/lib/module/ui/hooks/useI18n.js.map +1 -1
  111. package/lib/module/ui/hooks/useSessionManagement.js +47 -155
  112. package/lib/module/ui/hooks/useSessionManagement.js.map +1 -1
  113. package/lib/module/ui/index.js +1 -2
  114. package/lib/module/ui/index.js.map +1 -1
  115. package/lib/module/ui/navigation/routes.js +84 -72
  116. package/lib/module/ui/navigation/routes.js.map +1 -1
  117. package/lib/module/ui/navigation/surfaces.js +6 -6
  118. package/lib/module/ui/navigation/surfaces.js.map +1 -1
  119. package/lib/module/ui/runtime/createOxyRuntime.js +430 -0
  120. package/lib/module/ui/runtime/createOxyRuntime.js.map +1 -0
  121. package/lib/module/ui/runtime/index.js +15 -0
  122. package/lib/module/ui/runtime/index.js.map +1 -0
  123. package/lib/module/ui/runtime/runtimeContext.js +75 -0
  124. package/lib/module/ui/runtime/runtimeContext.js.map +1 -0
  125. package/lib/module/ui/runtime/types.js +4 -0
  126. package/lib/module/ui/runtime/types.js.map +1 -0
  127. package/lib/module/ui/runtime/useRuntimeSnapshot.js +52 -0
  128. package/lib/module/ui/runtime/useRuntimeSnapshot.js.map +1 -0
  129. package/lib/module/ui/server.js +0 -1
  130. package/lib/module/ui/server.js.map +1 -1
  131. package/lib/module/ui/session/authStore.js +46 -11
  132. package/lib/module/ui/session/authStore.js.map +1 -1
  133. package/lib/module/ui/session/sharedDeviceCredentialStore.js +302 -0
  134. package/lib/module/ui/session/sharedDeviceCredentialStore.js.map +1 -0
  135. package/lib/module/ui/stores/authStore.js +154 -55
  136. package/lib/module/ui/stores/authStore.js.map +1 -1
  137. package/lib/module/ui/utils/avatarUtils.js +2 -35
  138. package/lib/module/ui/utils/avatarUtils.js.map +1 -1
  139. package/lib/typescript/commonjs/index.d.ts +5 -3
  140. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  141. package/lib/typescript/commonjs/ui/boot/runProviderColdBoot.d.ts.map +1 -1
  142. package/lib/typescript/commonjs/ui/client.d.ts +1 -2
  143. package/lib/typescript/commonjs/ui/client.d.ts.map +1 -1
  144. package/lib/typescript/commonjs/ui/components/OxyAccountDialogScreen.d.ts.map +1 -1
  145. package/lib/typescript/commonjs/ui/components/OxyAuthChooser.d.ts.map +1 -1
  146. package/lib/typescript/commonjs/ui/components/OxyProvider.d.ts.map +1 -1
  147. package/lib/typescript/commonjs/ui/components/SurfaceScreen.d.ts.map +1 -1
  148. package/lib/typescript/commonjs/ui/components/authChooser/AccountsMenuView.d.ts +11 -4
  149. package/lib/typescript/commonjs/ui/components/authChooser/AccountsMenuView.d.ts.map +1 -1
  150. package/lib/typescript/commonjs/ui/components/authChooser/SignInEntryView.d.ts +3 -1
  151. package/lib/typescript/commonjs/ui/components/authChooser/SignInEntryView.d.ts.map +1 -1
  152. package/lib/typescript/commonjs/ui/components/authChooser/primitives.d.ts +18 -6
  153. package/lib/typescript/commonjs/ui/components/authChooser/primitives.d.ts.map +1 -1
  154. package/lib/typescript/commonjs/ui/components/authChooser/types.d.ts +33 -1
  155. package/lib/typescript/commonjs/ui/components/authChooser/types.d.ts.map +1 -1
  156. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts +35 -4
  157. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
  158. package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts +8 -12
  159. package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  160. package/lib/typescript/commonjs/ui/context/oxyContextHelpers.d.ts +0 -2
  161. package/lib/typescript/commonjs/ui/context/oxyContextHelpers.d.ts.map +1 -1
  162. package/lib/typescript/commonjs/ui/context/oxyContextTypes.d.ts +9 -1
  163. package/lib/typescript/commonjs/ui/context/oxyContextTypes.d.ts.map +1 -1
  164. package/lib/typescript/commonjs/ui/context/useOxyAccountGraph.d.ts +1 -3
  165. package/lib/typescript/commonjs/ui/context/useOxyAccountGraph.d.ts.map +1 -1
  166. package/lib/typescript/commonjs/ui/hooks/accountDialogSnapshot.d.ts +15 -0
  167. package/lib/typescript/commonjs/ui/hooks/accountDialogSnapshot.d.ts.map +1 -0
  168. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  169. package/lib/typescript/commonjs/ui/hooks/useDeviceSwitcher.d.ts +49 -0
  170. package/lib/typescript/commonjs/ui/hooks/useDeviceSwitcher.d.ts.map +1 -0
  171. package/lib/typescript/commonjs/ui/hooks/useI18n.d.ts +13 -0
  172. package/lib/typescript/commonjs/ui/hooks/useI18n.d.ts.map +1 -1
  173. package/lib/typescript/commonjs/ui/hooks/useSessionManagement.d.ts +23 -18
  174. package/lib/typescript/commonjs/ui/hooks/useSessionManagement.d.ts.map +1 -1
  175. package/lib/typescript/commonjs/ui/index.d.ts +1 -2
  176. package/lib/typescript/commonjs/ui/index.d.ts.map +1 -1
  177. package/lib/typescript/commonjs/ui/navigation/routes.d.ts +15 -1
  178. package/lib/typescript/commonjs/ui/navigation/routes.d.ts.map +1 -1
  179. package/lib/typescript/commonjs/ui/navigation/surfaces.d.ts.map +1 -1
  180. package/lib/typescript/commonjs/ui/runtime/createOxyRuntime.d.ts +144 -0
  181. package/lib/typescript/commonjs/ui/runtime/createOxyRuntime.d.ts.map +1 -0
  182. package/lib/typescript/commonjs/ui/runtime/index.d.ts +16 -0
  183. package/lib/typescript/commonjs/ui/runtime/index.d.ts.map +1 -0
  184. package/lib/typescript/commonjs/ui/runtime/runtimeContext.d.ts +43 -0
  185. package/lib/typescript/commonjs/ui/runtime/runtimeContext.d.ts.map +1 -0
  186. package/lib/typescript/commonjs/ui/runtime/types.d.ts +89 -0
  187. package/lib/typescript/commonjs/ui/runtime/types.d.ts.map +1 -0
  188. package/lib/typescript/commonjs/ui/runtime/useRuntimeSnapshot.d.ts +23 -0
  189. package/lib/typescript/commonjs/ui/runtime/useRuntimeSnapshot.d.ts.map +1 -0
  190. package/lib/typescript/commonjs/ui/server.d.ts +0 -1
  191. package/lib/typescript/commonjs/ui/server.d.ts.map +1 -1
  192. package/lib/typescript/commonjs/ui/session/authStore.d.ts +60 -9
  193. package/lib/typescript/commonjs/ui/session/authStore.d.ts.map +1 -1
  194. package/lib/typescript/commonjs/ui/session/sharedDeviceCredentialStore.d.ts +32 -0
  195. package/lib/typescript/commonjs/ui/session/sharedDeviceCredentialStore.d.ts.map +1 -0
  196. package/lib/typescript/commonjs/ui/stores/authStore.d.ts +49 -5
  197. package/lib/typescript/commonjs/ui/stores/authStore.d.ts.map +1 -1
  198. package/lib/typescript/commonjs/ui/types/navigation.d.ts +26 -0
  199. package/lib/typescript/commonjs/ui/types/navigation.d.ts.map +1 -1
  200. package/lib/typescript/commonjs/ui/utils/avatarUtils.d.ts +2 -10
  201. package/lib/typescript/commonjs/ui/utils/avatarUtils.d.ts.map +1 -1
  202. package/lib/typescript/module/index.d.ts +5 -3
  203. package/lib/typescript/module/index.d.ts.map +1 -1
  204. package/lib/typescript/module/ui/boot/runProviderColdBoot.d.ts.map +1 -1
  205. package/lib/typescript/module/ui/client.d.ts +1 -2
  206. package/lib/typescript/module/ui/client.d.ts.map +1 -1
  207. package/lib/typescript/module/ui/components/OxyAccountDialogScreen.d.ts.map +1 -1
  208. package/lib/typescript/module/ui/components/OxyAuthChooser.d.ts.map +1 -1
  209. package/lib/typescript/module/ui/components/OxyProvider.d.ts.map +1 -1
  210. package/lib/typescript/module/ui/components/SurfaceScreen.d.ts.map +1 -1
  211. package/lib/typescript/module/ui/components/authChooser/AccountsMenuView.d.ts +11 -4
  212. package/lib/typescript/module/ui/components/authChooser/AccountsMenuView.d.ts.map +1 -1
  213. package/lib/typescript/module/ui/components/authChooser/SignInEntryView.d.ts +3 -1
  214. package/lib/typescript/module/ui/components/authChooser/SignInEntryView.d.ts.map +1 -1
  215. package/lib/typescript/module/ui/components/authChooser/primitives.d.ts +18 -6
  216. package/lib/typescript/module/ui/components/authChooser/primitives.d.ts.map +1 -1
  217. package/lib/typescript/module/ui/components/authChooser/types.d.ts +33 -1
  218. package/lib/typescript/module/ui/components/authChooser/types.d.ts.map +1 -1
  219. package/lib/typescript/module/ui/context/OxyContext.d.ts +35 -4
  220. package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
  221. package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts +8 -12
  222. package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  223. package/lib/typescript/module/ui/context/oxyContextHelpers.d.ts +0 -2
  224. package/lib/typescript/module/ui/context/oxyContextHelpers.d.ts.map +1 -1
  225. package/lib/typescript/module/ui/context/oxyContextTypes.d.ts +9 -1
  226. package/lib/typescript/module/ui/context/oxyContextTypes.d.ts.map +1 -1
  227. package/lib/typescript/module/ui/context/useOxyAccountGraph.d.ts +1 -3
  228. package/lib/typescript/module/ui/context/useOxyAccountGraph.d.ts.map +1 -1
  229. package/lib/typescript/module/ui/hooks/accountDialogSnapshot.d.ts +15 -0
  230. package/lib/typescript/module/ui/hooks/accountDialogSnapshot.d.ts.map +1 -0
  231. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  232. package/lib/typescript/module/ui/hooks/useDeviceSwitcher.d.ts +49 -0
  233. package/lib/typescript/module/ui/hooks/useDeviceSwitcher.d.ts.map +1 -0
  234. package/lib/typescript/module/ui/hooks/useI18n.d.ts +13 -0
  235. package/lib/typescript/module/ui/hooks/useI18n.d.ts.map +1 -1
  236. package/lib/typescript/module/ui/hooks/useSessionManagement.d.ts +23 -18
  237. package/lib/typescript/module/ui/hooks/useSessionManagement.d.ts.map +1 -1
  238. package/lib/typescript/module/ui/index.d.ts +1 -2
  239. package/lib/typescript/module/ui/index.d.ts.map +1 -1
  240. package/lib/typescript/module/ui/navigation/routes.d.ts +15 -1
  241. package/lib/typescript/module/ui/navigation/routes.d.ts.map +1 -1
  242. package/lib/typescript/module/ui/navigation/surfaces.d.ts.map +1 -1
  243. package/lib/typescript/module/ui/runtime/createOxyRuntime.d.ts +144 -0
  244. package/lib/typescript/module/ui/runtime/createOxyRuntime.d.ts.map +1 -0
  245. package/lib/typescript/module/ui/runtime/index.d.ts +16 -0
  246. package/lib/typescript/module/ui/runtime/index.d.ts.map +1 -0
  247. package/lib/typescript/module/ui/runtime/runtimeContext.d.ts +43 -0
  248. package/lib/typescript/module/ui/runtime/runtimeContext.d.ts.map +1 -0
  249. package/lib/typescript/module/ui/runtime/types.d.ts +89 -0
  250. package/lib/typescript/module/ui/runtime/types.d.ts.map +1 -0
  251. package/lib/typescript/module/ui/runtime/useRuntimeSnapshot.d.ts +23 -0
  252. package/lib/typescript/module/ui/runtime/useRuntimeSnapshot.d.ts.map +1 -0
  253. package/lib/typescript/module/ui/server.d.ts +0 -1
  254. package/lib/typescript/module/ui/server.d.ts.map +1 -1
  255. package/lib/typescript/module/ui/session/authStore.d.ts +60 -9
  256. package/lib/typescript/module/ui/session/authStore.d.ts.map +1 -1
  257. package/lib/typescript/module/ui/session/sharedDeviceCredentialStore.d.ts +32 -0
  258. package/lib/typescript/module/ui/session/sharedDeviceCredentialStore.d.ts.map +1 -0
  259. package/lib/typescript/module/ui/stores/authStore.d.ts +49 -5
  260. package/lib/typescript/module/ui/stores/authStore.d.ts.map +1 -1
  261. package/lib/typescript/module/ui/types/navigation.d.ts +26 -0
  262. package/lib/typescript/module/ui/types/navigation.d.ts.map +1 -1
  263. package/lib/typescript/module/ui/utils/avatarUtils.d.ts +2 -10
  264. package/lib/typescript/module/ui/utils/avatarUtils.d.ts.map +1 -1
  265. package/package.json +6 -7
  266. package/plugins/withSharedDeviceSessionProvider.js +102 -0
  267. package/plugins/withSharedDeviceSessionReader.js +57 -0
  268. package/src/__tests__/esmSourcePurity.test.ts +61 -0
  269. package/src/index.ts +31 -9
  270. package/src/ui/boot/runProviderColdBoot.ts +7 -0
  271. package/src/ui/client.ts +1 -2
  272. package/src/ui/components/OxyAccountDialogScreen.tsx +10 -29
  273. package/src/ui/components/OxyAuthChooser.tsx +149 -50
  274. package/src/ui/components/OxyProvider.tsx +7 -5
  275. package/src/ui/components/SurfaceScreen.tsx +28 -4
  276. package/src/ui/components/authChooser/AccountsMenuView.tsx +246 -96
  277. package/src/ui/components/authChooser/SignInEntryView.tsx +31 -8
  278. package/src/ui/components/authChooser/primitives.tsx +37 -23
  279. package/src/ui/components/authChooser/types.ts +34 -1
  280. package/src/ui/context/OxyContext.tsx +269 -324
  281. package/src/ui/context/hooks/useAuthOperations.ts +32 -35
  282. package/src/ui/context/oxyContextHelpers.ts +0 -30
  283. package/src/ui/context/oxyContextTypes.ts +9 -1
  284. package/src/ui/context/useOxyAccountGraph.ts +5 -19
  285. package/src/ui/hooks/accountDialogSnapshot.ts +37 -0
  286. package/src/ui/hooks/mutations/useAccountMutations.ts +0 -19
  287. package/src/ui/hooks/queries/__tests__/userCache.test.ts +12 -1
  288. package/src/ui/hooks/useDeviceSwitcher.ts +110 -0
  289. package/src/ui/hooks/useI18n.ts +23 -3
  290. package/src/ui/hooks/useSessionManagement.ts +62 -211
  291. package/src/ui/index.ts +1 -2
  292. package/src/ui/navigation/routes.ts +85 -75
  293. package/src/ui/navigation/surfaces.ts +6 -9
  294. package/src/ui/runtime/createOxyRuntime.ts +623 -0
  295. package/src/ui/runtime/index.ts +34 -0
  296. package/src/ui/runtime/runtimeContext.tsx +111 -0
  297. package/src/ui/runtime/types.ts +110 -0
  298. package/src/ui/runtime/useRuntimeSnapshot.ts +52 -0
  299. package/src/ui/server.ts +0 -1
  300. package/src/ui/session/__tests__/authStore.test.ts +90 -0
  301. package/src/ui/session/__tests__/sharedDeviceCredentialStore.test.ts +289 -0
  302. package/src/ui/session/authStore.ts +78 -9
  303. package/src/ui/session/sharedDeviceCredentialStore.ts +318 -0
  304. package/src/ui/stores/authStore.ts +171 -43
  305. package/src/ui/types/navigation.ts +26 -0
  306. package/src/ui/utils/avatarUtils.ts +2 -37
  307. package/lib/commonjs/ui/hooks/useSwitchableAccounts.js +0 -64
  308. package/lib/commonjs/ui/hooks/useSwitchableAccounts.js.map +0 -1
  309. package/lib/commonjs/ui/stores/accountStore.js +0 -221
  310. package/lib/commonjs/ui/stores/accountStore.js.map +0 -1
  311. package/lib/module/ui/hooks/useSwitchableAccounts.js +0 -60
  312. package/lib/module/ui/hooks/useSwitchableAccounts.js.map +0 -1
  313. package/lib/module/ui/stores/accountStore.js +0 -213
  314. package/lib/module/ui/stores/accountStore.js.map +0 -1
  315. package/lib/typescript/commonjs/ui/hooks/useSwitchableAccounts.d.ts +0 -23
  316. package/lib/typescript/commonjs/ui/hooks/useSwitchableAccounts.d.ts.map +0 -1
  317. package/lib/typescript/commonjs/ui/stores/accountStore.d.ts +0 -27
  318. package/lib/typescript/commonjs/ui/stores/accountStore.d.ts.map +0 -1
  319. package/lib/typescript/module/ui/hooks/useSwitchableAccounts.d.ts +0 -23
  320. package/lib/typescript/module/ui/hooks/useSwitchableAccounts.d.ts.map +0 -1
  321. package/lib/typescript/module/ui/stores/accountStore.d.ts +0 -27
  322. package/lib/typescript/module/ui/stores/accountStore.d.ts.map +0 -1
  323. package/src/ui/hooks/useSwitchableAccounts.ts +0 -75
  324. package/src/ui/stores/accountStore.ts +0 -264
@@ -1,25 +1,66 @@
1
1
  /**
2
2
  * Platform `AuthStateStore` for `@oxyhq/services`.
3
3
  *
4
- * The zero-cookie device model persists the durable device credential
5
- * (`deviceId` + `deviceSecret`) per origin (web) / per device (native), and the
6
- * SDK re-mints the access token from that credential on cold boot. `@oxyhq/core`
7
- * owns the store shape + logic (`createWebAuthStateStore` /
8
- * `createNativeAuthStateStore`); this module only selects the platform storage
9
- * seam:
4
+ * The device model persists the durable device credential (`deviceId` +
5
+ * `deviceSecret`) per origin (web) / per device (native), and the SDK re-mints
6
+ * the access token from that credential on cold boot. `@oxyhq/core` owns the
7
+ * store shape + logic (`createWebAuthStateStore` / `createNativeAuthStateStore` /
8
+ * `createMemoryAuthStateStore`); this module only selects the seam:
10
9
  *
10
+ * - `storage: 'ephemeral'` → `createMemoryAuthStateStore()`, on EVERY platform.
11
11
  * - web → `createWebAuthStateStore()` (localStorage, in-memory fallback).
12
12
  * - native → `createNativeAuthStateStore(secureKV)` for the SESSION blob over
13
13
  * the shared `createNativeSecureKeyValueStorage()` adapter
14
- * (`expo-secure-store`, AsyncStorage fallback).
14
+ * (`expo-secure-store`, AsyncStorage fallback), WRAPPED so every credential
15
+ * it persists is also mirrored into the device-wide shared slot.
16
+ *
17
+ * The native wrapper is how several official apps end up on ONE `DeviceSession`
18
+ * (and therefore one active context): whichever app proves a credential publishes
19
+ * it, and a later-installed sibling adopts it during cold boot instead of asking
20
+ * for the Commons identity key. The mirror is additive — it can neither change
21
+ * nor fail the app's own durable write.
15
22
  */
16
23
  import {
17
24
  createWebAuthStateStore,
18
25
  createNativeAuthStateStore,
26
+ createMemoryAuthStateStore,
27
+ createSharedMirroringAuthStateStore,
19
28
  type AuthStateStore,
29
+ type SessionMode,
20
30
  } from '@oxyhq/core';
21
31
  import { isReactNative } from '../utils/storageHelpers';
22
32
  import { createNativeSecureKeyValueStorage } from './nativeSecureStorage';
33
+ import { createPlatformSharedDeviceCredentialStore } from './sharedDeviceCredentialStore';
34
+
35
+ export interface PlatformAuthStateStoreOptions {
36
+ /**
37
+ * Who owns the session this provider resolves. Only `'account'` mirrors into
38
+ * the device-wide shared slot.
39
+ *
40
+ * `'identity'` (the vault) is deliberately excluded, which keeps its storage
41
+ * behaviour byte-for-byte what it is today. The vault's credential is a
42
+ * perfectly valid device credential, so publishing it would arguably be
43
+ * correct — but it would also let every ordinary app on the device join the
44
+ * vault's device session as a side effect of the vault persisting a token, and
45
+ * "a background write changes which session five other apps boot into" is not
46
+ * a decision a storage seam gets to make.
47
+ */
48
+ sessionMode?: SessionMode;
49
+ /**
50
+ * Whether this origin/app may persist a durable device credential AT ALL.
51
+ *
52
+ * `'ephemeral'` stores nothing durable and mirrors nothing: the session lives
53
+ * in memory for the lifetime of the page and is gone on reload. Its one caller
54
+ * is `auth.oxy.so` with the browser hub enabled (issue #937 Phase 5, ADR 0003),
55
+ * where the durable credential for the browser profile is the server-side
56
+ * DeviceSession behind the `__Host-oxy-device` handle.
57
+ *
58
+ * See `OxyProviderProps.deviceCredentialStorage` for why the answer to "let the
59
+ * hub be authoritative" is to stop persisting the other credential rather than
60
+ * to consult it second.
61
+ */
62
+ storage?: 'persistent' | 'ephemeral';
63
+ }
23
64
 
24
65
  /**
25
66
  * Build the platform {@link AuthStateStore} for this runtime.
@@ -27,11 +68,39 @@ import { createNativeSecureKeyValueStorage } from './nativeSecureStorage';
27
68
  * Native persists the SESSION blob (`deviceId` + `deviceSecret` + cached access
28
69
  * token) per-app in SecureStore; the SDK re-mints the access token from the
29
70
  * device credential on the next cold boot.
71
+ *
72
+ * ## The ephemeral check is FIRST, and the order is load-bearing
73
+ *
74
+ * It short-circuits above the `isReactNative()` branch so an ephemeral caller can
75
+ * never reach the native path — and therefore can never reach the shared-slot
76
+ * mirroring wrapper. Checked after that branch instead, an origin that has
77
+ * declined to persist a credential would publish one into the device-wide slot
78
+ * every sibling app adopts on cold boot: it would keep nothing itself while
79
+ * deciding which session five other apps boot into.
80
+ *
81
+ * That is unreachable today because the only ephemeral caller is a web origin, so
82
+ * `isReactNative()` is false there anyway. Unreachable today is exactly the kind
83
+ * of guarantee that stops holding when someone adds the second caller, so it is
84
+ * structural here rather than incidental, and
85
+ * `__tests__/session/authStoreEphemeral.test.ts` holds the ordering directly.
30
86
  */
31
- export function createPlatformAuthStateStore(): AuthStateStore {
87
+ export function createPlatformAuthStateStore(
88
+ options: PlatformAuthStateStoreOptions = {},
89
+ ): AuthStateStore {
90
+ // FIRST, above every platform branch — see the docblock for why the ordering
91
+ // is the guarantee and not a coincidence.
92
+ if (options.storage === 'ephemeral') {
93
+ return createMemoryAuthStateStore();
94
+ }
95
+
32
96
  if (!isReactNative()) {
33
97
  return createWebAuthStateStore();
34
98
  }
35
99
 
36
- return createNativeAuthStateStore(createNativeSecureKeyValueStorage());
100
+ const local = createNativeAuthStateStore(createNativeSecureKeyValueStorage());
101
+ if (options.sessionMode === 'identity') {
102
+ return local;
103
+ }
104
+ const shared = createPlatformSharedDeviceCredentialStore();
105
+ return shared ? createSharedMirroringAuthStateStore({ local, shared }) : local;
37
106
  }
@@ -0,0 +1,318 @@
1
+ /**
2
+ * The platform half of the shared native DeviceSession credential.
3
+ *
4
+ * `@oxyhq/core` owns the rules (`sharedDeviceCredential.ts`: when to adopt, when
5
+ * to publish, and above all that an unreadable slot is never an empty one). This
6
+ * module owns only where the bytes live, which is genuinely different per
7
+ * platform:
8
+ *
9
+ * - **iOS** — one `expo-secure-store` item in the approved Keychain Access Group
10
+ * `group.so.oxy.shared`, under its OWN `keychainService`
11
+ * ({@link IOS_DEVICE_SESSION_KEYCHAIN_SERVICE}). Every identity slot uses a
12
+ * different service (`oxy_identity`, `oxy_identity_backup`,
13
+ * `oxy_identity_mnemonic`) and the legacy cross-app identity keypair uses the
14
+ * same group with NO service at all — so this item cannot alias any of them.
15
+ * That separation is the point of the whole phase: the group-shared identity
16
+ * key and the group-shared device-session credential are different secrets
17
+ * with different blast radii and must not be conflated.
18
+ * - **Android** — the signature-protected `OxyDeviceSession` broker (its own
19
+ * EncryptedSharedPreferences file, its own ContentProvider, its own
20
+ * `signature`-level permission). It carries a session credential; it can NOT
21
+ * reach the identity keypair, which lives in a different file behind a
22
+ * different provider.
23
+ * - **web / anything else** — no shared slot. Each origin is its own device by
24
+ * design, so the factory returns `null` and the cold boot simply omits the
25
+ * lane.
26
+ *
27
+ * ## What is written here, and what is not
28
+ *
29
+ * Only `{deviceId, deviceSecret}`. No access token, no account id, no user id,
30
+ * and never any identity key material.
31
+ *
32
+ * ## Accessibility
33
+ *
34
+ * `WHEN_UNLOCKED_THIS_DEVICE_ONLY`: a device credential must not travel to
35
+ * another handset in an iCloud/encrypted backup restore — the restored device is
36
+ * a different device and has to establish its own session. It also keeps the item
37
+ * out of reach while the phone is locked, which is why a read can legitimately
38
+ * fail and why the `unavailable` verdict exists at all.
39
+ */
40
+ import { Platform } from 'react-native';
41
+ import { requireOptionalNativeModule } from 'expo-modules-core';
42
+ import {
43
+ normalizeSharedDeviceSessionRead,
44
+ type SharedDeviceCredential,
45
+ type SharedDeviceCredentialRead,
46
+ type SharedDeviceCredentialStore,
47
+ } from '@oxyhq/core';
48
+
49
+ /**
50
+ * The iOS Keychain Access Group every official Oxy app declares in its
51
+ * entitlements. Shared with the identity slots — the group is the app-group
52
+ * boundary, not the secret's identity.
53
+ */
54
+ export const IOS_KEYCHAIN_ACCESS_GROUP = 'group.so.oxy.shared';
55
+
56
+ /**
57
+ * The iOS `keychainService` for the DEVICE SESSION credential. Deliberately
58
+ * distinct from every identity and recovery service; do not reuse one of those
59
+ * here, and do not drop it (an item written with no service shares the legacy
60
+ * identity slot's namespace inside the group).
61
+ */
62
+ export const IOS_DEVICE_SESSION_KEYCHAIN_SERVICE = 'oxy_device_session';
63
+
64
+ /** The single key holding the JSON-encoded `{deviceId, deviceSecret}` pair. */
65
+ export const SHARED_DEVICE_SESSION_STORAGE_KEY = 'oxy_shared_device_session_v1';
66
+
67
+ // Variable indirection so Metro's static analyzer never traces expo-secure-store
68
+ // into the web bundle; the module is native-only and optional.
69
+ const SECURE_STORE_MODULE = 'expo-secure-store';
70
+
71
+ /**
72
+ * The subset of `expo-secure-store` this module needs. Unlike the adapter in
73
+ * `nativeSecureStorage.ts` it must pass OPTIONS — the access group and the
74
+ * dedicated service are the entire mechanism here, so an options-less wrapper
75
+ * would silently write a package-private item that no sibling app can see.
76
+ */
77
+ interface KeychainGroupStore {
78
+ getItemAsync(key: string, options?: object): Promise<string | null>;
79
+ setItemAsync(key: string, value: string, options?: object): Promise<void>;
80
+ deleteItemAsync(key: string, options?: object): Promise<void>;
81
+ readonly WHEN_UNLOCKED_THIS_DEVICE_ONLY: number;
82
+ }
83
+
84
+ let keychainStorePromise: Promise<KeychainGroupStore | null> | null = null;
85
+
86
+ function loadKeychainGroupStore(): Promise<KeychainGroupStore | null> {
87
+ if (!keychainStorePromise) {
88
+ const moduleName = SECURE_STORE_MODULE;
89
+ keychainStorePromise = import(moduleName)
90
+ .then((mod: Partial<KeychainGroupStore>) => {
91
+ if (
92
+ typeof mod.getItemAsync === 'function' &&
93
+ typeof mod.setItemAsync === 'function' &&
94
+ typeof mod.deleteItemAsync === 'function' &&
95
+ typeof mod.WHEN_UNLOCKED_THIS_DEVICE_ONLY === 'number'
96
+ ) {
97
+ return {
98
+ getItemAsync: mod.getItemAsync.bind(mod),
99
+ setItemAsync: mod.setItemAsync.bind(mod),
100
+ deleteItemAsync: mod.deleteItemAsync.bind(mod),
101
+ WHEN_UNLOCKED_THIS_DEVICE_ONLY: mod.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
102
+ } satisfies KeychainGroupStore;
103
+ }
104
+ return null;
105
+ })
106
+ .catch(() => null);
107
+ }
108
+ return keychainStorePromise;
109
+ }
110
+
111
+ /** Parse the stored JSON into a credential, or `null` for anything malformed. */
112
+ function parseStoredCredential(raw: string | null): SharedDeviceCredential | null {
113
+ if (!raw) {
114
+ return null;
115
+ }
116
+ let parsed: unknown;
117
+ try {
118
+ parsed = JSON.parse(raw);
119
+ } catch {
120
+ return null;
121
+ }
122
+ if (!parsed || typeof parsed !== 'object') {
123
+ return null;
124
+ }
125
+ const candidate = parsed as Record<string, unknown>;
126
+ if (
127
+ typeof candidate.deviceId !== 'string' ||
128
+ candidate.deviceId.length === 0 ||
129
+ typeof candidate.deviceSecret !== 'string' ||
130
+ candidate.deviceSecret.length === 0
131
+ ) {
132
+ return null;
133
+ }
134
+ return { deviceId: candidate.deviceId, deviceSecret: candidate.deviceSecret };
135
+ }
136
+
137
+ /**
138
+ * The iOS store: one keychain item in the shared access group.
139
+ *
140
+ * A THROWN read is `unavailable`, a `null` read is `absent`. That mapping is the
141
+ * whole safety contract — `expo-secure-store` returns `null` for a genuinely
142
+ * missing item and throws when the keychain itself refuses, and collapsing the
143
+ * two is what would let a locked phone look like a device that never had a
144
+ * session.
145
+ */
146
+ function createKeychainGroupCredentialStore(): SharedDeviceCredentialStore {
147
+ const readOptions = {
148
+ keychainService: IOS_DEVICE_SESSION_KEYCHAIN_SERVICE,
149
+ keychainAccessGroup: IOS_KEYCHAIN_ACCESS_GROUP,
150
+ };
151
+
152
+ return {
153
+ read: async (): Promise<SharedDeviceCredentialRead> => {
154
+ const store = await loadKeychainGroupStore();
155
+ if (!store) {
156
+ return { state: 'unsupported' };
157
+ }
158
+ let raw: string | null;
159
+ try {
160
+ raw = await store.getItemAsync(SHARED_DEVICE_SESSION_STORAGE_KEY, readOptions);
161
+ } catch (error) {
162
+ return { state: 'unavailable', cause: error };
163
+ }
164
+ if (raw === null) {
165
+ return { state: 'absent' };
166
+ }
167
+ const credential = parseStoredCredential(raw);
168
+ if (!credential) {
169
+ // Something IS stored and we cannot read it. Reporting `absent` would
170
+ // authorise overwriting whatever a newer build put there.
171
+ return {
172
+ state: 'unavailable',
173
+ cause: new Error('shared device session item is present but not parseable'),
174
+ };
175
+ }
176
+ return { state: 'present', credential };
177
+ },
178
+
179
+ publish: async (credential): Promise<boolean> => {
180
+ const store = await loadKeychainGroupStore();
181
+ if (!store) {
182
+ return false;
183
+ }
184
+ const value = JSON.stringify(credential);
185
+ try {
186
+ await store.setItemAsync(SHARED_DEVICE_SESSION_STORAGE_KEY, value, {
187
+ ...readOptions,
188
+ keychainAccessible: store.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
189
+ });
190
+ // Read-back verify. A keychain write can resolve without the item having
191
+ // landed under the access group (a missing entitlement is the usual
192
+ // cause), and a phantom credential would send a fresh install into a mint
193
+ // that can never succeed.
194
+ return (await store.getItemAsync(SHARED_DEVICE_SESSION_STORAGE_KEY, readOptions)) === value;
195
+ } catch {
196
+ return false;
197
+ }
198
+ },
199
+
200
+ clear: async (): Promise<void> => {
201
+ const store = await loadKeychainGroupStore();
202
+ if (!store) {
203
+ return;
204
+ }
205
+ try {
206
+ await store.deleteItemAsync(SHARED_DEVICE_SESSION_STORAGE_KEY, readOptions);
207
+ } catch {
208
+ // Best-effort: the slot is a join point, not a source of truth.
209
+ }
210
+ },
211
+ };
212
+ }
213
+
214
+ /**
215
+ * The Android broker's JS surface. `read` returns an UNTRUSTED payload that
216
+ * {@link normalizeSharedDeviceSessionRead} narrows; `write` reports whether the
217
+ * native read-back confirmed the bytes.
218
+ */
219
+ interface OxyDeviceSessionNativeModule {
220
+ read(): Promise<unknown>;
221
+ write(deviceId: string, deviceSecret: string): Promise<boolean>;
222
+ clear(): Promise<void>;
223
+ }
224
+
225
+ let brokerModule: OxyDeviceSessionNativeModule | null | undefined;
226
+
227
+ /**
228
+ * Resolve the `OxyDeviceSession` broker, or `null` when it is not autolinked.
229
+ *
230
+ * `requireOptionalNativeModule` (a static import Metro always resolves) rather
231
+ * than a dynamic `import(moduleName)`: the latter compiles to `require(variable)`
232
+ * in the CJS build, which Metro cannot resolve inside a consuming app — it
233
+ * silently returns `null` there, which is exactly how the cross-app identity
234
+ * bridge broke once. Same reasoning as `backgroundSession.ts` next door.
235
+ */
236
+ function loadBrokerModule(): OxyDeviceSessionNativeModule | null {
237
+ if (brokerModule === undefined) {
238
+ const native = requireOptionalNativeModule<Partial<OxyDeviceSessionNativeModule>>('OxyDeviceSession');
239
+ brokerModule =
240
+ native && typeof native.read === 'function' && typeof native.write === 'function' && typeof native.clear === 'function'
241
+ ? {
242
+ read: native.read.bind(native),
243
+ write: native.write.bind(native),
244
+ clear: native.clear.bind(native),
245
+ }
246
+ : null;
247
+ }
248
+ return brokerModule;
249
+ }
250
+
251
+ /**
252
+ * The Android store: the signature-protected `OxyDeviceSession` broker.
253
+ *
254
+ * The broker already answers with an explicit status, so the only work here is
255
+ * narrowing an UNTRUSTED payload — `normalizeSharedDeviceSessionRead` resolves
256
+ * anything it does not recognise to `unavailable`, never `absent`.
257
+ */
258
+ function createBrokerCredentialStore(): SharedDeviceCredentialStore {
259
+ return {
260
+ read: async (): Promise<SharedDeviceCredentialRead> => {
261
+ const broker = loadBrokerModule();
262
+ if (!broker) {
263
+ return { state: 'unsupported' };
264
+ }
265
+ try {
266
+ return normalizeSharedDeviceSessionRead(await broker.read());
267
+ } catch (error) {
268
+ return { state: 'unavailable', cause: error };
269
+ }
270
+ },
271
+
272
+ publish: async (credential): Promise<boolean> => {
273
+ const broker = loadBrokerModule();
274
+ if (!broker) {
275
+ return false;
276
+ }
277
+ try {
278
+ // The broker read-back-verifies natively and returns the verdict.
279
+ return await broker.write(credential.deviceId, credential.deviceSecret);
280
+ } catch {
281
+ return false;
282
+ }
283
+ },
284
+
285
+ clear: async (): Promise<void> => {
286
+ const broker = loadBrokerModule();
287
+ if (!broker) {
288
+ return;
289
+ }
290
+ try {
291
+ await broker.clear();
292
+ } catch {
293
+ // Best-effort — see the iOS store.
294
+ }
295
+ },
296
+ };
297
+ }
298
+
299
+ /**
300
+ * The shared DeviceSession credential slot for this runtime, or `null` when
301
+ * there is none.
302
+ *
303
+ * The branch is on the PLATFORM, not on which module happens to load, because
304
+ * `expo-secure-store` loads perfectly well on Android — it would just quietly
305
+ * ignore `keychainAccessGroup` and read a package-private item, which looks
306
+ * exactly like an empty shared slot. This is the same iOS-vs-Android split
307
+ * `KeyManager` makes for the shared identity, for the same reason.
308
+ *
309
+ * Any other native platform gets the broker, which answers `unsupported` when
310
+ * the module is not linked — so the lane is simply off, which is the safe
311
+ * direction.
312
+ */
313
+ export function createPlatformSharedDeviceCredentialStore(): SharedDeviceCredentialStore | null {
314
+ if (Platform.OS === 'web') {
315
+ return null;
316
+ }
317
+ return Platform.OS === 'ios' ? createKeychainGroupCredentialStore() : createBrokerCredentialStore();
318
+ }
@@ -1,63 +1,191 @@
1
- import { create } from 'zustand';
2
- import { createLogger, normalizeUserIdentity } from '@oxyhq/core';
1
+ import { useCallback, useRef, useSyncExternalStore } from 'react';
3
2
  import type { User } from '@oxyhq/core';
3
+ import type { OxyRuntime } from '../runtime';
4
4
 
5
- const log = createLogger('AuthStore');
6
-
5
+ /**
6
+ * The signed-in user, as a zustand-shaped read/write surface.
7
+ *
8
+ * It used to BE a zustand store, and that made it a second owner of the
9
+ * session: the provider wrote `user`/`isAuthenticated` here, `SessionClient`
10
+ * wrote the same facts into its device state, and nothing reconciled them —
11
+ * when they disagreed, which one was right was not answerable. It is now a
12
+ * projection of the ONE `OxyRuntime` (ADR 0004): reads come off the runtime
13
+ * snapshot and writes go into the runtime, so there is nothing left to drift.
14
+ *
15
+ * The zustand SHAPE is kept deliberately — `useAuthStore(selector)`,
16
+ * `.getState()`, `.setState()`, `.subscribe()` — because a dozen call sites
17
+ * across this package and the Commons app read the user out of band, from
18
+ * mutation success handlers and cache helpers that are not inside a render.
19
+ * Rewriting them is Phase 8's cut, not this one's.
20
+ *
21
+ * `fetchUser` and `lastUserFetch` are GONE rather than projected: they were a
22
+ * five-minute profile cache with zero readers anywhere in the monorepo, and the
23
+ * Query cache has owned that job since long before this change.
24
+ */
7
25
  export interface AuthState {
8
26
  user: User | null;
9
27
  isAuthenticated: boolean;
10
28
  isLoading: boolean;
11
29
  error: string | null;
12
- lastUserFetch: number | null; // Timestamp of last user fetch for caching
13
-
30
+
14
31
  loginSuccess: (user: User) => void;
15
32
  loginFailure: (error: string) => void;
16
33
  logout: () => void;
17
- fetchUser: (oxyServices: { getCurrentUser: () => Promise<User> }, forceRefresh?: boolean) => Promise<void>;
18
- setUser: (user: User) => void; // Direct user setter for caching
34
+ /** Replace the signed-in profile after a write (a rename, a new avatar). */
35
+ setUser: (user: User) => void;
19
36
  }
20
37
 
21
- export const useAuthStore = create<AuthState>((set: (state: Partial<AuthState>) => void, get: () => AuthState) => ({
38
+ /** The subset of `AuthState` a caller may push back in through `setState`. */
39
+ export type AuthStatePatch = Partial<Pick<AuthState, 'user' | 'isLoading' | 'error'>>;
40
+
41
+ let boundRuntime: OxyRuntime | null = null;
42
+ let unbindRuntime: (() => void) | null = null;
43
+ const listeners = new Set<() => void>();
44
+
45
+ const loginSuccess = (user: User): void => {
46
+ boundRuntime?.setAccount(user);
47
+ };
48
+
49
+ const loginFailure = (error: string): void => {
50
+ boundRuntime?.batch(() => {
51
+ boundRuntime?.setLoading(false);
52
+ boundRuntime?.setError(error);
53
+ });
54
+ };
55
+
56
+ const logout = (): void => {
57
+ boundRuntime?.clearSession();
58
+ };
59
+
60
+ const setUser = (user: User): void => {
61
+ boundRuntime?.setAccount(user);
62
+ };
63
+
64
+ const SIGNED_OUT: AuthState = Object.freeze({
22
65
  user: null,
23
66
  isAuthenticated: false,
24
67
  isLoading: false,
25
68
  error: null,
26
- lastUserFetch: null,
27
-
28
- loginSuccess: (user: User) => set({
29
- isLoading: false,
30
- isAuthenticated: true,
31
- user: normalizeUserIdentity(user),
32
- lastUserFetch: Date.now(),
33
- }),
34
- loginFailure: (error: string) => set({ isLoading: false, error }),
35
- logout: () => set({
36
- user: null,
37
- isAuthenticated: false,
38
- lastUserFetch: null,
39
- }),
40
- setUser: (user: User) => set({ user: normalizeUserIdentity(user), lastUserFetch: Date.now() }),
41
- fetchUser: async (oxyServices, forceRefresh = false) => {
42
- const state = get();
43
- const now = Date.now();
44
- const cacheAge = state.lastUserFetch ? now - state.lastUserFetch : Number.POSITIVE_INFINITY;
45
- const cacheValid = cacheAge < 5 * 60 * 1000; // 5 minutes cache
46
-
47
- // Use cached data if available and not forcing refresh
48
- if (!forceRefresh && state.user && cacheValid) {
49
- log.info('Using cached user data', { cacheAge });
69
+ loginSuccess,
70
+ loginFailure,
71
+ logout,
72
+ setUser,
73
+ });
74
+
75
+ let state: AuthState = SIGNED_OUT;
76
+
77
+ function project(runtime: OxyRuntime | null): AuthState {
78
+ if (runtime === null) {
79
+ return SIGNED_OUT;
80
+ }
81
+ const snapshot = runtime.getSnapshot();
82
+ return Object.freeze({
83
+ user: snapshot.account,
84
+ isAuthenticated: snapshot.account !== null,
85
+ isLoading: snapshot.isLoading,
86
+ error: snapshot.error?.message ?? null,
87
+ loginSuccess,
88
+ loginFailure,
89
+ logout,
90
+ setUser,
91
+ });
92
+ }
93
+
94
+ function republish(): void {
95
+ const next = project(boundRuntime);
96
+ if (
97
+ next.user === state.user &&
98
+ next.isAuthenticated === state.isAuthenticated &&
99
+ next.isLoading === state.isLoading &&
100
+ next.error === state.error
101
+ ) {
102
+ return;
103
+ }
104
+ state = next;
105
+ for (const listener of [...listeners]) {
106
+ listener();
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Point this surface at the provider's runtime. Called once by
112
+ * `OxyRuntimeProvider`; the returned function detaches it again on unmount.
113
+ *
114
+ * With nothing bound, reads answer signed-out and writes are dropped. That is
115
+ * the honest answer for the two places it happens — the SSR shim, and a unit
116
+ * test poking the store with no provider mounted — and it is why binding is an
117
+ * explicit call rather than a lazily-created default runtime, which is exactly
118
+ * the fabricated-forever-loading runtime ADR 0004 deleted.
119
+ */
120
+ export function bindAuthStoreToRuntime(runtime: OxyRuntime): () => void {
121
+ unbindRuntime?.();
122
+ boundRuntime = runtime;
123
+ unbindRuntime = runtime.subscribe(republish);
124
+ republish();
125
+ return () => {
126
+ if (boundRuntime !== runtime) {
50
127
  return;
51
128
  }
129
+ unbindRuntime?.();
130
+ unbindRuntime = null;
131
+ boundRuntime = null;
132
+ republish();
133
+ };
134
+ }
52
135
 
53
- set({ isLoading: true, error: null });
54
- try {
55
- const user = normalizeUserIdentity(await oxyServices.getCurrentUser());
56
- set({ user, isLoading: false, isAuthenticated: true, lastUserFetch: now });
57
- } catch (error) {
58
- const errorMessage = error instanceof Error ? error.message : 'Failed to fetch user';
59
- log.error('Error fetching user', error);
60
- set({ error: errorMessage, isLoading: false });
136
+ const getState = (): AuthState => state;
137
+
138
+ const setState = (patch: AuthStatePatch): void => {
139
+ boundRuntime?.batch(() => {
140
+ if (patch.isLoading !== undefined) {
141
+ boundRuntime?.setLoading(patch.isLoading);
142
+ }
143
+ if (patch.error !== undefined) {
144
+ boundRuntime?.setError(patch.error);
145
+ }
146
+ if (patch.user !== undefined) {
147
+ if (patch.user === null) {
148
+ boundRuntime?.clearSession();
149
+ } else {
150
+ boundRuntime?.setAccount(patch.user);
151
+ }
61
152
  }
62
- },
63
- }));
153
+ });
154
+ };
155
+
156
+ const subscribe = (listener: () => void): (() => void) => {
157
+ listeners.add(listener);
158
+ return () => {
159
+ listeners.delete(listener);
160
+ };
161
+ };
162
+
163
+ function useAuthStoreHook<T>(selector: (state: AuthState) => T): T {
164
+ // The selection is cached against the state object it came from, so a
165
+ // selector returning a fresh object cannot make `useSyncExternalStore` see a
166
+ // new value on every call and loop.
167
+ const cache = useRef<{ state: AuthState; value: T } | null>(null);
168
+ const getSelection = useCallback((): T => {
169
+ const cached = cache.current;
170
+ if (cached !== null && cached.state === state) {
171
+ return cached.value;
172
+ }
173
+ const value = selector(state);
174
+ cache.current = { state, value };
175
+ return value;
176
+ }, [selector]);
177
+ return useSyncExternalStore(subscribe, getSelection, getSelection);
178
+ }
179
+
180
+ /**
181
+ * Read the signed-in user. Zustand-shaped, projected from the runtime.
182
+ *
183
+ * A selector that builds an object must be wrapped in `useShallow` (as every
184
+ * call site here already does) — otherwise a structurally-equal result reads as
185
+ * a change on every publish.
186
+ */
187
+ export const useAuthStore = Object.assign(useAuthStoreHook, {
188
+ getState,
189
+ setState,
190
+ subscribe,
191
+ });