@oxy.so/core 1.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 (673) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +16 -0
  3. package/README.md +221 -0
  4. package/dist/cjs/.tsbuildinfo +1 -0
  5. package/dist/cjs/HttpService.js +1270 -0
  6. package/dist/cjs/OxyServices.base.js +390 -0
  7. package/dist/cjs/OxyServices.errors.js +95 -0
  8. package/dist/cjs/OxyServices.js +72 -0
  9. package/dist/cjs/boot/sessionColdBoot.js +395 -0
  10. package/dist/cjs/constants/version.js +16 -0
  11. package/dist/cjs/crypto/aead.js +79 -0
  12. package/dist/cjs/crypto/ecdh.js +35 -0
  13. package/dist/cjs/crypto/identityMarker.js +255 -0
  14. package/dist/cjs/crypto/kdf.js +39 -0
  15. package/dist/cjs/crypto/keyManager.js +2234 -0
  16. package/dist/cjs/crypto/polyfill.js +131 -0
  17. package/dist/cjs/crypto/recoveryPhrase.js +242 -0
  18. package/dist/cjs/crypto/signatureService.js +273 -0
  19. package/dist/cjs/i18n/accountCategoryLabels.js +44 -0
  20. package/dist/cjs/i18n/accountRoleLabels.js +27 -0
  21. package/dist/cjs/i18n/index.js +90 -0
  22. package/dist/cjs/i18n/locales/ar-SA.json +282 -0
  23. package/dist/cjs/i18n/locales/ca-ES.json +282 -0
  24. package/dist/cjs/i18n/locales/de-DE.json +282 -0
  25. package/dist/cjs/i18n/locales/en-US.json +2161 -0
  26. package/dist/cjs/i18n/locales/es-ES.json +2161 -0
  27. package/dist/cjs/i18n/locales/fr-FR.json +282 -0
  28. package/dist/cjs/i18n/locales/it-IT.json +282 -0
  29. package/dist/cjs/i18n/locales/ja-JP.json +282 -0
  30. package/dist/cjs/i18n/locales/ko-KR.json +282 -0
  31. package/dist/cjs/i18n/locales/locales/ar-SA.json +282 -0
  32. package/dist/cjs/i18n/locales/locales/ca-ES.json +282 -0
  33. package/dist/cjs/i18n/locales/locales/de-DE.json +282 -0
  34. package/dist/cjs/i18n/locales/locales/en-US.json +2161 -0
  35. package/dist/cjs/i18n/locales/locales/es-ES.json +2161 -0
  36. package/dist/cjs/i18n/locales/locales/fr-FR.json +282 -0
  37. package/dist/cjs/i18n/locales/locales/it-IT.json +282 -0
  38. package/dist/cjs/i18n/locales/locales/ja-JP.json +282 -0
  39. package/dist/cjs/i18n/locales/locales/ko-KR.json +282 -0
  40. package/dist/cjs/i18n/locales/locales/pt-PT.json +282 -0
  41. package/dist/cjs/i18n/locales/locales/zh-CN.json +282 -0
  42. package/dist/cjs/i18n/locales/pt-PT.json +282 -0
  43. package/dist/cjs/i18n/locales/zh-CN.json +282 -0
  44. package/dist/cjs/i18n/reputationCategoryLabels.js +20 -0
  45. package/dist/cjs/i18n/trustTierLabels.js +19 -0
  46. package/dist/cjs/index.js +498 -0
  47. package/dist/cjs/inference/OxyInferenceClient.js +544 -0
  48. package/dist/cjs/logger/index.js +149 -0
  49. package/dist/cjs/mixins/OxyServices.accounts.js +555 -0
  50. package/dist/cjs/mixins/OxyServices.analytics.js +49 -0
  51. package/dist/cjs/mixins/OxyServices.appData.js +108 -0
  52. package/dist/cjs/mixins/OxyServices.assets.js +764 -0
  53. package/dist/cjs/mixins/OxyServices.auth.js +1254 -0
  54. package/dist/cjs/mixins/OxyServices.chains.js +73 -0
  55. package/dist/cjs/mixins/OxyServices.civic.js +611 -0
  56. package/dist/cjs/mixins/OxyServices.connectedApps.js +78 -0
  57. package/dist/cjs/mixins/OxyServices.contacts.js +50 -0
  58. package/dist/cjs/mixins/OxyServices.deviceBoot.js +191 -0
  59. package/dist/cjs/mixins/OxyServices.deviceTransfer.js +314 -0
  60. package/dist/cjs/mixins/OxyServices.devices.js +116 -0
  61. package/dist/cjs/mixins/OxyServices.features.js +335 -0
  62. package/dist/cjs/mixins/OxyServices.followGraph.js +221 -0
  63. package/dist/cjs/mixins/OxyServices.identity.js +461 -0
  64. package/dist/cjs/mixins/OxyServices.identityBackup.js +172 -0
  65. package/dist/cjs/mixins/OxyServices.inference.js +59 -0
  66. package/dist/cjs/mixins/OxyServices.language.js +125 -0
  67. package/dist/cjs/mixins/OxyServices.links.js +72 -0
  68. package/dist/cjs/mixins/OxyServices.location.js +43 -0
  69. package/dist/cjs/mixins/OxyServices.nodes.js +175 -0
  70. package/dist/cjs/mixins/OxyServices.notifications.js +63 -0
  71. package/dist/cjs/mixins/OxyServices.payment.js +158 -0
  72. package/dist/cjs/mixins/OxyServices.privacy.js +199 -0
  73. package/dist/cjs/mixins/OxyServices.reputation.js +289 -0
  74. package/dist/cjs/mixins/OxyServices.security.js +89 -0
  75. package/dist/cjs/mixins/OxyServices.store.js +266 -0
  76. package/dist/cjs/mixins/OxyServices.topics.js +137 -0
  77. package/dist/cjs/mixins/OxyServices.user.js +902 -0
  78. package/dist/cjs/mixins/OxyServices.utility.js +1098 -0
  79. package/dist/cjs/mixins/index.js +135 -0
  80. package/dist/cjs/mixins/mixinHelpers.js +53 -0
  81. package/dist/cjs/models/Topic.js +16 -0
  82. package/dist/cjs/models/interfaces.js +20 -0
  83. package/dist/cjs/models/session.js +2 -0
  84. package/dist/cjs/server/auth.js +167 -0
  85. package/dist/cjs/server/capabilityTicket.js +192 -0
  86. package/dist/cjs/server/cors.js +225 -0
  87. package/dist/cjs/server/index.js +97 -0
  88. package/dist/cjs/server/rateLimit.js +207 -0
  89. package/dist/cjs/server/safeFetch.js +458 -0
  90. package/dist/cjs/server/securityHeaders.js +377 -0
  91. package/dist/cjs/server/userInvalidation.js +156 -0
  92. package/dist/cjs/server/verifySecret.js +50 -0
  93. package/dist/cjs/session/SessionClient.js +865 -0
  94. package/dist/cjs/session/accountDelegationAccess.js +48 -0
  95. package/dist/cjs/session/accountDialogController.js +1131 -0
  96. package/dist/cjs/session/accountSwitchTargets.js +81 -0
  97. package/dist/cjs/session/authStateStore.js +404 -0
  98. package/dist/cjs/session/createSessionClient.js +32 -0
  99. package/dist/cjs/session/deviceDirectory.js +143 -0
  100. package/dist/cjs/session/deviceSwitcherRows.js +76 -0
  101. package/dist/cjs/session/identityPin.js +256 -0
  102. package/dist/cjs/session/identitySession.js +135 -0
  103. package/dist/cjs/session/projectSessionState.js +121 -0
  104. package/dist/cjs/session/refresh.js +398 -0
  105. package/dist/cjs/session/sessionClientHost.js +34 -0
  106. package/dist/cjs/session/sharedDeviceCredential.js +247 -0
  107. package/dist/cjs/session/socketLoader.js +55 -0
  108. package/dist/cjs/shared/utils/colorUtils.js +153 -0
  109. package/dist/cjs/shared/utils/errorUtils.js +183 -0
  110. package/dist/cjs/shared/utils/index.js +49 -0
  111. package/dist/cjs/shared/utils/networkUtils.js +183 -0
  112. package/dist/cjs/shared/utils/themeUtils.js +106 -0
  113. package/dist/cjs/utils/accountCacheSweep.js +80 -0
  114. package/dist/cjs/utils/accountUtils.js +140 -0
  115. package/dist/cjs/utils/activityId.js +6 -0
  116. package/dist/cjs/utils/apiUtils.js +91 -0
  117. package/dist/cjs/utils/asyncUtils.js +222 -0
  118. package/dist/cjs/utils/authHelpers.js +120 -0
  119. package/dist/cjs/utils/authWebUrl.js +19 -0
  120. package/dist/cjs/utils/avatarUtils.js +29 -0
  121. package/dist/cjs/utils/cache.js +235 -0
  122. package/dist/cjs/utils/cacheKey.js +85 -0
  123. package/dist/cjs/utils/coldBoot.js +122 -0
  124. package/dist/cjs/utils/commonsApproval.js +31 -0
  125. package/dist/cjs/utils/commonsDelivery.js +59 -0
  126. package/dist/cjs/utils/deviceManager.js +174 -0
  127. package/dist/cjs/utils/displayNamePolicyRanges.generated.js +60 -0
  128. package/dist/cjs/utils/edgeRegion.js +6 -0
  129. package/dist/cjs/utils/errorUtils.js +243 -0
  130. package/dist/cjs/utils/identityCacheSweep.js +97 -0
  131. package/dist/cjs/utils/languageUtils.js +224 -0
  132. package/dist/cjs/utils/oauthPkce.js +311 -0
  133. package/dist/cjs/utils/officialOrigins.js +64 -0
  134. package/dist/cjs/utils/oxyServiceEnvironment.js +19 -0
  135. package/dist/cjs/utils/platform.js +119 -0
  136. package/dist/cjs/utils/profileLinks.js +63 -0
  137. package/dist/cjs/utils/redactUrl.js +29 -0
  138. package/dist/cjs/utils/registrableApex.js +45 -0
  139. package/dist/cjs/utils/requestUtils.js +209 -0
  140. package/dist/cjs/utils/sessionUtils.js +181 -0
  141. package/dist/cjs/utils/textNormalization.js +158 -0
  142. package/dist/cjs/utils/userHandle.js +37 -0
  143. package/dist/cjs/utils/userIdentity.js +50 -0
  144. package/dist/cjs/utils/validationUtils.js +370 -0
  145. package/dist/cjs/utils/webauthnOrigin.js +51 -0
  146. package/dist/esm/.tsbuildinfo +1 -0
  147. package/dist/esm/HttpService.js +1266 -0
  148. package/dist/esm/OxyServices.base.js +386 -0
  149. package/dist/esm/OxyServices.errors.js +88 -0
  150. package/dist/esm/OxyServices.js +66 -0
  151. package/dist/esm/boot/sessionColdBoot.js +392 -0
  152. package/dist/esm/constants/version.js +13 -0
  153. package/dist/esm/crypto/aead.js +74 -0
  154. package/dist/esm/crypto/ecdh.js +32 -0
  155. package/dist/esm/crypto/identityMarker.js +248 -0
  156. package/dist/esm/crypto/kdf.js +36 -0
  157. package/dist/esm/crypto/keyManager.js +2227 -0
  158. package/dist/esm/crypto/polyfill.js +129 -0
  159. package/dist/esm/crypto/recoveryPhrase.js +238 -0
  160. package/dist/esm/crypto/signatureService.js +269 -0
  161. package/dist/esm/i18n/accountCategoryLabels.js +37 -0
  162. package/dist/esm/i18n/accountRoleLabels.js +20 -0
  163. package/dist/esm/i18n/index.js +83 -0
  164. package/dist/esm/i18n/locales/ar-SA.json +282 -0
  165. package/dist/esm/i18n/locales/ca-ES.json +282 -0
  166. package/dist/esm/i18n/locales/de-DE.json +282 -0
  167. package/dist/esm/i18n/locales/en-US.json +2161 -0
  168. package/dist/esm/i18n/locales/es-ES.json +2161 -0
  169. package/dist/esm/i18n/locales/fr-FR.json +282 -0
  170. package/dist/esm/i18n/locales/it-IT.json +282 -0
  171. package/dist/esm/i18n/locales/ja-JP.json +282 -0
  172. package/dist/esm/i18n/locales/ko-KR.json +282 -0
  173. package/dist/esm/i18n/locales/locales/ar-SA.json +282 -0
  174. package/dist/esm/i18n/locales/locales/ca-ES.json +282 -0
  175. package/dist/esm/i18n/locales/locales/de-DE.json +282 -0
  176. package/dist/esm/i18n/locales/locales/en-US.json +2161 -0
  177. package/dist/esm/i18n/locales/locales/es-ES.json +2161 -0
  178. package/dist/esm/i18n/locales/locales/fr-FR.json +282 -0
  179. package/dist/esm/i18n/locales/locales/it-IT.json +282 -0
  180. package/dist/esm/i18n/locales/locales/ja-JP.json +282 -0
  181. package/dist/esm/i18n/locales/locales/ko-KR.json +282 -0
  182. package/dist/esm/i18n/locales/locales/pt-PT.json +282 -0
  183. package/dist/esm/i18n/locales/locales/zh-CN.json +282 -0
  184. package/dist/esm/i18n/locales/pt-PT.json +282 -0
  185. package/dist/esm/i18n/locales/zh-CN.json +282 -0
  186. package/dist/esm/i18n/reputationCategoryLabels.js +13 -0
  187. package/dist/esm/i18n/trustTierLabels.js +12 -0
  188. package/dist/esm/index.js +254 -0
  189. package/dist/esm/inference/OxyInferenceClient.js +538 -0
  190. package/dist/esm/logger/index.js +140 -0
  191. package/dist/esm/mixins/OxyServices.accounts.js +543 -0
  192. package/dist/esm/mixins/OxyServices.analytics.js +46 -0
  193. package/dist/esm/mixins/OxyServices.appData.js +103 -0
  194. package/dist/esm/mixins/OxyServices.assets.js +761 -0
  195. package/dist/esm/mixins/OxyServices.auth.js +1247 -0
  196. package/dist/esm/mixins/OxyServices.chains.js +70 -0
  197. package/dist/esm/mixins/OxyServices.civic.js +605 -0
  198. package/dist/esm/mixins/OxyServices.connectedApps.js +75 -0
  199. package/dist/esm/mixins/OxyServices.contacts.js +47 -0
  200. package/dist/esm/mixins/OxyServices.deviceBoot.js +186 -0
  201. package/dist/esm/mixins/OxyServices.deviceTransfer.js +311 -0
  202. package/dist/esm/mixins/OxyServices.devices.js +113 -0
  203. package/dist/esm/mixins/OxyServices.features.js +332 -0
  204. package/dist/esm/mixins/OxyServices.followGraph.js +218 -0
  205. package/dist/esm/mixins/OxyServices.identity.js +457 -0
  206. package/dist/esm/mixins/OxyServices.identityBackup.js +169 -0
  207. package/dist/esm/mixins/OxyServices.inference.js +56 -0
  208. package/dist/esm/mixins/OxyServices.language.js +122 -0
  209. package/dist/esm/mixins/OxyServices.links.js +69 -0
  210. package/dist/esm/mixins/OxyServices.location.js +40 -0
  211. package/dist/esm/mixins/OxyServices.nodes.js +172 -0
  212. package/dist/esm/mixins/OxyServices.notifications.js +60 -0
  213. package/dist/esm/mixins/OxyServices.payment.js +155 -0
  214. package/dist/esm/mixins/OxyServices.privacy.js +196 -0
  215. package/dist/esm/mixins/OxyServices.reputation.js +286 -0
  216. package/dist/esm/mixins/OxyServices.security.js +86 -0
  217. package/dist/esm/mixins/OxyServices.store.js +263 -0
  218. package/dist/esm/mixins/OxyServices.topics.js +134 -0
  219. package/dist/esm/mixins/OxyServices.user.js +899 -0
  220. package/dist/esm/mixins/OxyServices.utility.js +1095 -0
  221. package/dist/esm/mixins/index.js +132 -0
  222. package/dist/esm/mixins/mixinHelpers.js +48 -0
  223. package/dist/esm/models/Topic.js +13 -0
  224. package/dist/esm/models/interfaces.js +17 -0
  225. package/dist/esm/models/session.js +1 -0
  226. package/dist/esm/server/auth.js +154 -0
  227. package/dist/esm/server/capabilityTicket.js +183 -0
  228. package/dist/esm/server/cors.js +220 -0
  229. package/dist/esm/server/index.js +47 -0
  230. package/dist/esm/server/rateLimit.js +201 -0
  231. package/dist/esm/server/safeFetch.js +447 -0
  232. package/dist/esm/server/securityHeaders.js +364 -0
  233. package/dist/esm/server/userInvalidation.js +152 -0
  234. package/dist/esm/server/verifySecret.js +47 -0
  235. package/dist/esm/session/SessionClient.js +861 -0
  236. package/dist/esm/session/accountDelegationAccess.js +45 -0
  237. package/dist/esm/session/accountDialogController.js +1126 -0
  238. package/dist/esm/session/accountSwitchTargets.js +77 -0
  239. package/dist/esm/session/authStateStore.js +398 -0
  240. package/dist/esm/session/createSessionClient.js +29 -0
  241. package/dist/esm/session/deviceDirectory.js +135 -0
  242. package/dist/esm/session/deviceSwitcherRows.js +72 -0
  243. package/dist/esm/session/identityPin.js +249 -0
  244. package/dist/esm/session/identitySession.js +131 -0
  245. package/dist/esm/session/projectSessionState.js +114 -0
  246. package/dist/esm/session/refresh.js +390 -0
  247. package/dist/esm/session/sessionClientHost.js +31 -0
  248. package/dist/esm/session/sharedDeviceCredential.js +239 -0
  249. package/dist/esm/session/socketLoader.js +19 -0
  250. package/dist/esm/shared/utils/colorUtils.js +143 -0
  251. package/dist/esm/shared/utils/errorUtils.js +170 -0
  252. package/dist/esm/shared/utils/index.js +15 -0
  253. package/dist/esm/shared/utils/networkUtils.js +173 -0
  254. package/dist/esm/shared/utils/themeUtils.js +98 -0
  255. package/dist/esm/utils/accountCacheSweep.js +75 -0
  256. package/dist/esm/utils/accountUtils.js +131 -0
  257. package/dist/esm/utils/activityId.js +2 -0
  258. package/dist/esm/utils/apiUtils.js +84 -0
  259. package/dist/esm/utils/asyncUtils.js +207 -0
  260. package/dist/esm/utils/authHelpers.js +111 -0
  261. package/dist/esm/utils/authWebUrl.js +16 -0
  262. package/dist/esm/utils/avatarUtils.js +26 -0
  263. package/dist/esm/utils/cache.js +227 -0
  264. package/dist/esm/utils/cacheKey.js +80 -0
  265. package/dist/esm/utils/coldBoot.js +119 -0
  266. package/dist/esm/utils/commonsApproval.js +27 -0
  267. package/dist/esm/utils/commonsDelivery.js +54 -0
  268. package/dist/esm/utils/deviceManager.js +170 -0
  269. package/dist/esm/utils/displayNamePolicyRanges.generated.js +57 -0
  270. package/dist/esm/utils/edgeRegion.js +2 -0
  271. package/dist/esm/utils/errorUtils.js +232 -0
  272. package/dist/esm/utils/identityCacheSweep.js +92 -0
  273. package/dist/esm/utils/languageUtils.js +212 -0
  274. package/dist/esm/utils/oauthPkce.js +297 -0
  275. package/dist/esm/utils/officialOrigins.js +59 -0
  276. package/dist/esm/utils/oxyServiceEnvironment.js +16 -0
  277. package/dist/esm/utils/platform.js +110 -0
  278. package/dist/esm/utils/profileLinks.js +60 -0
  279. package/dist/esm/utils/redactUrl.js +26 -0
  280. package/dist/esm/utils/registrableApex.js +42 -0
  281. package/dist/esm/utils/requestUtils.js +203 -0
  282. package/dist/esm/utils/sessionUtils.js +171 -0
  283. package/dist/esm/utils/textNormalization.js +154 -0
  284. package/dist/esm/utils/userHandle.js +33 -0
  285. package/dist/esm/utils/userIdentity.js +45 -0
  286. package/dist/esm/utils/validationUtils.js +350 -0
  287. package/dist/esm/utils/webauthnOrigin.js +48 -0
  288. package/dist/types/.tsbuildinfo +1 -0
  289. package/dist/types/HttpService.d.ts +413 -0
  290. package/dist/types/OxyServices.base.d.ts +187 -0
  291. package/dist/types/OxyServices.d.ts +139 -0
  292. package/dist/types/OxyServices.errors.d.ts +84 -0
  293. package/dist/types/boot/sessionColdBoot.d.ts +140 -0
  294. package/dist/types/constants/version.d.ts +13 -0
  295. package/dist/types/crypto/aead.d.ts +56 -0
  296. package/dist/types/crypto/ecdh.d.ts +29 -0
  297. package/dist/types/crypto/identityMarker.d.ts +94 -0
  298. package/dist/types/crypto/kdf.d.ts +25 -0
  299. package/dist/types/crypto/keyManager.d.ts +582 -0
  300. package/dist/types/crypto/polyfill.d.ts +25 -0
  301. package/dist/types/crypto/recoveryPhrase.d.ts +173 -0
  302. package/dist/types/crypto/signatureService.d.ts +136 -0
  303. package/dist/types/i18n/accountCategoryLabels.d.ts +34 -0
  304. package/dist/types/i18n/accountRoleLabels.d.ts +10 -0
  305. package/dist/types/i18n/index.d.ts +3 -0
  306. package/dist/types/i18n/reputationCategoryLabels.d.ts +10 -0
  307. package/dist/types/i18n/trustTierLabels.d.ts +9 -0
  308. package/dist/types/index.d.ts +145 -0
  309. package/dist/types/inference/OxyInferenceClient.d.ts +342 -0
  310. package/dist/types/logger/index.d.ts +104 -0
  311. package/dist/types/mixins/OxyServices.accounts.d.ts +868 -0
  312. package/dist/types/mixins/OxyServices.analytics.d.ts +72 -0
  313. package/dist/types/mixins/OxyServices.appData.d.ts +108 -0
  314. package/dist/types/mixins/OxyServices.assets.d.ts +329 -0
  315. package/dist/types/mixins/OxyServices.auth.d.ts +908 -0
  316. package/dist/types/mixins/OxyServices.chains.d.ts +156 -0
  317. package/dist/types/mixins/OxyServices.civic.d.ts +510 -0
  318. package/dist/types/mixins/OxyServices.connectedApps.d.ts +169 -0
  319. package/dist/types/mixins/OxyServices.contacts.d.ts +100 -0
  320. package/dist/types/mixins/OxyServices.deviceBoot.d.ts +185 -0
  321. package/dist/types/mixins/OxyServices.deviceTransfer.d.ts +149 -0
  322. package/dist/types/mixins/OxyServices.devices.d.ts +103 -0
  323. package/dist/types/mixins/OxyServices.features.d.ts +250 -0
  324. package/dist/types/mixins/OxyServices.followGraph.d.ts +223 -0
  325. package/dist/types/mixins/OxyServices.identity.d.ts +351 -0
  326. package/dist/types/mixins/OxyServices.identityBackup.d.ts +129 -0
  327. package/dist/types/mixins/OxyServices.inference.d.ts +95 -0
  328. package/dist/types/mixins/OxyServices.language.d.ts +87 -0
  329. package/dist/types/mixins/OxyServices.links.d.ts +104 -0
  330. package/dist/types/mixins/OxyServices.location.d.ts +70 -0
  331. package/dist/types/mixins/OxyServices.nodes.d.ts +198 -0
  332. package/dist/types/mixins/OxyServices.notifications.d.ts +143 -0
  333. package/dist/types/mixins/OxyServices.payment.d.ts +117 -0
  334. package/dist/types/mixins/OxyServices.privacy.d.ts +146 -0
  335. package/dist/types/mixins/OxyServices.reputation.d.ts +202 -0
  336. package/dist/types/mixins/OxyServices.security.d.ts +84 -0
  337. package/dist/types/mixins/OxyServices.store.d.ts +334 -0
  338. package/dist/types/mixins/OxyServices.topics.d.ts +110 -0
  339. package/dist/types/mixins/OxyServices.user.d.ts +558 -0
  340. package/dist/types/mixins/OxyServices.utility.d.ts +393 -0
  341. package/dist/types/mixins/index.d.ts +87 -0
  342. package/dist/types/mixins/mixinHelpers.d.ts +31 -0
  343. package/dist/types/models/Topic.d.ts +42 -0
  344. package/dist/types/models/interfaces.d.ts +675 -0
  345. package/dist/types/models/session.d.ts +51 -0
  346. package/dist/types/server/auth.d.ts +130 -0
  347. package/dist/types/server/capabilityTicket.d.ts +41 -0
  348. package/dist/types/server/cors.d.ts +98 -0
  349. package/dist/types/server/index.d.ts +35 -0
  350. package/dist/types/server/rateLimit.d.ts +40 -0
  351. package/dist/types/server/safeFetch.d.ts +137 -0
  352. package/dist/types/server/securityHeaders.d.ts +221 -0
  353. package/dist/types/server/userInvalidation.d.ts +114 -0
  354. package/dist/types/server/verifySecret.d.ts +29 -0
  355. package/dist/types/session/SessionClient.d.ts +418 -0
  356. package/dist/types/session/accountDelegationAccess.d.ts +40 -0
  357. package/dist/types/session/accountDialogController.d.ts +648 -0
  358. package/dist/types/session/accountSwitchTargets.d.ts +70 -0
  359. package/dist/types/session/authStateStore.d.ts +153 -0
  360. package/dist/types/session/createSessionClient.d.ts +30 -0
  361. package/dist/types/session/deviceDirectory.d.ts +182 -0
  362. package/dist/types/session/deviceSwitcherRows.d.ts +92 -0
  363. package/dist/types/session/identityPin.d.ts +87 -0
  364. package/dist/types/session/identitySession.d.ts +105 -0
  365. package/dist/types/session/projectSessionState.d.ts +74 -0
  366. package/dist/types/session/refresh.d.ts +179 -0
  367. package/dist/types/session/sessionClientHost.d.ts +18 -0
  368. package/dist/types/session/sharedDeviceCredential.d.ts +202 -0
  369. package/dist/types/session/socketLoader.d.ts +11 -0
  370. package/dist/types/shared/utils/colorUtils.d.ts +104 -0
  371. package/dist/types/shared/utils/errorUtils.d.ts +97 -0
  372. package/dist/types/shared/utils/index.d.ts +13 -0
  373. package/dist/types/shared/utils/networkUtils.d.ts +139 -0
  374. package/dist/types/shared/utils/themeUtils.d.ts +90 -0
  375. package/dist/types/utils/accountCacheSweep.d.ts +75 -0
  376. package/dist/types/utils/accountUtils.d.ts +100 -0
  377. package/dist/types/utils/activityId.d.ts +2 -0
  378. package/dist/types/utils/apiUtils.d.ts +95 -0
  379. package/dist/types/utils/asyncUtils.d.ts +62 -0
  380. package/dist/types/utils/authHelpers.d.ts +58 -0
  381. package/dist/types/utils/authWebUrl.d.ts +16 -0
  382. package/dist/types/utils/avatarUtils.d.ts +16 -0
  383. package/dist/types/utils/cache.d.ts +130 -0
  384. package/dist/types/utils/cacheKey.d.ts +66 -0
  385. package/dist/types/utils/coldBoot.d.ts +131 -0
  386. package/dist/types/utils/commonsApproval.d.ts +13 -0
  387. package/dist/types/utils/commonsDelivery.d.ts +105 -0
  388. package/dist/types/utils/deviceManager.d.ts +65 -0
  389. package/dist/types/utils/displayNamePolicyRanges.generated.d.ts +57 -0
  390. package/dist/types/utils/edgeRegion.d.ts +2 -0
  391. package/dist/types/utils/errorUtils.d.ts +125 -0
  392. package/dist/types/utils/identityCacheSweep.d.ts +80 -0
  393. package/dist/types/utils/languageUtils.d.ts +100 -0
  394. package/dist/types/utils/oauthPkce.d.ts +157 -0
  395. package/dist/types/utils/officialOrigins.d.ts +13 -0
  396. package/dist/types/utils/oxyServiceEnvironment.d.ts +17 -0
  397. package/dist/types/utils/platform.d.ts +49 -0
  398. package/dist/types/utils/profileLinks.d.ts +44 -0
  399. package/dist/types/utils/redactUrl.d.ts +17 -0
  400. package/dist/types/utils/registrableApex.d.ts +27 -0
  401. package/dist/types/utils/requestUtils.d.ts +123 -0
  402. package/dist/types/utils/sessionUtils.d.ts +54 -0
  403. package/dist/types/utils/textNormalization.d.ts +103 -0
  404. package/dist/types/utils/userHandle.d.ts +26 -0
  405. package/dist/types/utils/userIdentity.d.ts +21 -0
  406. package/dist/types/utils/validationUtils.d.ts +268 -0
  407. package/dist/types/utils/webauthnOrigin.d.ts +32 -0
  408. package/package.json +168 -0
  409. package/src/HttpService.ts +1553 -0
  410. package/src/OxyServices.base.ts +478 -0
  411. package/src/OxyServices.errors.ts +122 -0
  412. package/src/OxyServices.ts +172 -0
  413. package/src/__tests__/authSocket.test.ts +96 -0
  414. package/src/__tests__/httpServiceActivityId.test.ts +49 -0
  415. package/src/__tests__/httpServiceAuthSelfAwait.test.ts +108 -0
  416. package/src/__tests__/httpServiceCache.test.ts +318 -0
  417. package/src/__tests__/httpServiceCsrf.test.ts +235 -0
  418. package/src/__tests__/httpServiceFormEncoded.test.ts +142 -0
  419. package/src/__tests__/httpServiceQueueDeadlock.test.ts +128 -0
  420. package/src/__tests__/httpServiceUnwrapEnvelope.test.ts +115 -0
  421. package/src/__tests__/inSessionRefresh.test.ts +260 -0
  422. package/src/__tests__/linkedClient.test.ts +370 -0
  423. package/src/__tests__/parseHttpErrorBody.test.ts +116 -0
  424. package/src/__tests__/serverValueImportsDeclared.test.ts +120 -0
  425. package/src/__tests__/userIdentity.test.ts +143 -0
  426. package/src/boot/__tests__/sessionColdBoot.identity.test.ts +393 -0
  427. package/src/boot/__tests__/sessionColdBoot.sharedDevice.test.ts +325 -0
  428. package/src/boot/__tests__/sessionColdBoot.test.ts +570 -0
  429. package/src/boot/sessionColdBoot.ts +529 -0
  430. package/src/constants/version.ts +15 -0
  431. package/src/crypto/__tests__/backupMaterial.test.ts +86 -0
  432. package/src/crypto/__tests__/cryptoPrimitives.test.ts +226 -0
  433. package/src/crypto/__tests__/identityMocks.ts +125 -0
  434. package/src/crypto/__tests__/keyManager.atomicity.test.ts +272 -0
  435. package/src/crypto/__tests__/keyManager.cacheSafety.test.ts +175 -0
  436. package/src/crypto/__tests__/keyManager.identityStatus.test.ts +217 -0
  437. package/src/crypto/__tests__/keyManager.recoveryLadder.test.ts +179 -0
  438. package/src/crypto/__tests__/keyManager.recoveryMnemonic.test.ts +138 -0
  439. package/src/crypto/__tests__/keyManager.storageMigration.test.ts +227 -0
  440. package/src/crypto/__tests__/keyManager.test.ts +329 -0
  441. package/src/crypto/__tests__/polyfill.test.ts +109 -0
  442. package/src/crypto/__tests__/recoveryPhrase.test.ts +61 -0
  443. package/src/crypto/__tests__/scopedSeed.test.ts +126 -0
  444. package/src/crypto/__tests__/signChallengeShared.test.ts +63 -0
  445. package/src/crypto/__tests__/signedRecord.test.ts +230 -0
  446. package/src/crypto/aead.ts +97 -0
  447. package/src/crypto/ecdh.ts +37 -0
  448. package/src/crypto/identityMarker.ts +291 -0
  449. package/src/crypto/kdf.ts +43 -0
  450. package/src/crypto/keyManager.ts +2607 -0
  451. package/src/crypto/polyfill.ts +143 -0
  452. package/src/crypto/recoveryPhrase.ts +337 -0
  453. package/src/crypto/signatureService.ts +350 -0
  454. package/src/i18n/__tests__/accountCategoryLabels.test.ts +62 -0
  455. package/src/i18n/__tests__/accountRoleLabels.test.ts +54 -0
  456. package/src/i18n/__tests__/reputationCategoryLabels.test.ts +56 -0
  457. package/src/i18n/__tests__/trustTierLabels.test.ts +47 -0
  458. package/src/i18n/accountCategoryLabels.ts +44 -0
  459. package/src/i18n/accountRoleLabels.ts +26 -0
  460. package/src/i18n/index.ts +88 -0
  461. package/src/i18n/locales/ar-SA.json +282 -0
  462. package/src/i18n/locales/ca-ES.json +282 -0
  463. package/src/i18n/locales/de-DE.json +282 -0
  464. package/src/i18n/locales/en-US.json +2161 -0
  465. package/src/i18n/locales/es-ES.json +2161 -0
  466. package/src/i18n/locales/fr-FR.json +282 -0
  467. package/src/i18n/locales/it-IT.json +282 -0
  468. package/src/i18n/locales/ja-JP.json +282 -0
  469. package/src/i18n/locales/ko-KR.json +282 -0
  470. package/src/i18n/locales/pt-PT.json +282 -0
  471. package/src/i18n/locales/zh-CN.json +282 -0
  472. package/src/i18n/reputationCategoryLabels.ts +20 -0
  473. package/src/i18n/trustTierLabels.ts +18 -0
  474. package/src/index.ts +881 -0
  475. package/src/inference/OxyInferenceClient.ts +888 -0
  476. package/src/inference/__tests__/OxyInferenceClient.test.ts +834 -0
  477. package/src/logger/__tests__/logger.test.ts +207 -0
  478. package/src/logger/index.ts +217 -0
  479. package/src/mixins/OxyServices.accounts.ts +1330 -0
  480. package/src/mixins/OxyServices.analytics.ts +53 -0
  481. package/src/mixins/OxyServices.appData.ts +158 -0
  482. package/src/mixins/OxyServices.assets.ts +861 -0
  483. package/src/mixins/OxyServices.auth.ts +1858 -0
  484. package/src/mixins/OxyServices.chains.ts +134 -0
  485. package/src/mixins/OxyServices.civic.ts +945 -0
  486. package/src/mixins/OxyServices.connectedApps.ts +174 -0
  487. package/src/mixins/OxyServices.contacts.ts +73 -0
  488. package/src/mixins/OxyServices.deviceBoot.ts +210 -0
  489. package/src/mixins/OxyServices.deviceTransfer.ts +393 -0
  490. package/src/mixins/OxyServices.devices.ts +120 -0
  491. package/src/mixins/OxyServices.features.ts +454 -0
  492. package/src/mixins/OxyServices.followGraph.ts +293 -0
  493. package/src/mixins/OxyServices.identity.ts +705 -0
  494. package/src/mixins/OxyServices.identityBackup.ts +250 -0
  495. package/src/mixins/OxyServices.inference.ts +57 -0
  496. package/src/mixins/OxyServices.language.ts +144 -0
  497. package/src/mixins/OxyServices.links.ts +107 -0
  498. package/src/mixins/OxyServices.location.ts +46 -0
  499. package/src/mixins/OxyServices.nodes.ts +337 -0
  500. package/src/mixins/OxyServices.notifications.ts +142 -0
  501. package/src/mixins/OxyServices.payment.ts +163 -0
  502. package/src/mixins/OxyServices.privacy.ts +219 -0
  503. package/src/mixins/OxyServices.reputation.ts +436 -0
  504. package/src/mixins/OxyServices.security.ts +110 -0
  505. package/src/mixins/OxyServices.store.ts +585 -0
  506. package/src/mixins/OxyServices.topics.ts +169 -0
  507. package/src/mixins/OxyServices.user.ts +1137 -0
  508. package/src/mixins/OxyServices.utility.ts +1370 -0
  509. package/src/mixins/__tests__/OxyServices.civic.test.ts +1095 -0
  510. package/src/mixins/__tests__/OxyServices.deviceBoot.test.ts +124 -0
  511. package/src/mixins/__tests__/OxyServices.deviceTransfer.test.ts +271 -0
  512. package/src/mixins/__tests__/OxyServices.identity.test.ts +376 -0
  513. package/src/mixins/__tests__/OxyServices.links.test.ts +154 -0
  514. package/src/mixins/__tests__/OxyServices.nodes.test.ts +341 -0
  515. package/src/mixins/__tests__/OxyServices.rotateKey.test.ts +276 -0
  516. package/src/mixins/__tests__/OxyServices.serviceAssetMetadata.test.ts +192 -0
  517. package/src/mixins/__tests__/OxyServices.serviceAssetMetadataBySha.test.ts +135 -0
  518. package/src/mixins/__tests__/accounts.test.ts +727 -0
  519. package/src/mixins/__tests__/appData.test.ts +203 -0
  520. package/src/mixins/__tests__/assetCredentials.test.ts +47 -0
  521. package/src/mixins/__tests__/assetUpload.test.ts +191 -0
  522. package/src/mixins/__tests__/backgroundCredential.httpIntegration.test.ts +135 -0
  523. package/src/mixins/__tests__/chains.test.ts +113 -0
  524. package/src/mixins/__tests__/commonsSignIn.test.ts +1144 -0
  525. package/src/mixins/__tests__/connectedApps.test.ts +123 -0
  526. package/src/mixins/__tests__/deviceTokenMintPinned.test.ts +113 -0
  527. package/src/mixins/__tests__/discoveryErrorHandling.test.ts +266 -0
  528. package/src/mixins/__tests__/followCacheInvalidation.test.ts +173 -0
  529. package/src/mixins/__tests__/followGraph.test.ts +147 -0
  530. package/src/mixins/__tests__/followGraphPagination.test.ts +271 -0
  531. package/src/mixins/__tests__/getFileDownloadUrl.test.ts +346 -0
  532. package/src/mixins/__tests__/getFollowStatuses.test.ts +95 -0
  533. package/src/mixins/__tests__/getUsersByIds.test.ts +149 -0
  534. package/src/mixins/__tests__/identityBackup.test.ts +315 -0
  535. package/src/mixins/__tests__/identityWriteCacheInvalidation.test.ts +407 -0
  536. package/src/mixins/__tests__/inferenceFactory.test.ts +58 -0
  537. package/src/mixins/__tests__/onTokensChanged.test.ts +129 -0
  538. package/src/mixins/__tests__/preSessionSkipAuth.test.ts +284 -0
  539. package/src/mixins/__tests__/privacyCacheInvalidation.test.ts +155 -0
  540. package/src/mixins/__tests__/reputation.test.ts +522 -0
  541. package/src/mixins/__tests__/serviceAuth.test.ts +832 -0
  542. package/src/mixins/__tests__/serviceTokenJwks.test.ts +208 -0
  543. package/src/mixins/__tests__/store.test.ts +304 -0
  544. package/src/mixins/__tests__/topics.test.ts +156 -0
  545. package/src/mixins/__tests__/userReadCacheBypass.test.ts +129 -0
  546. package/src/mixins/__tests__/userTokenAuth.test.ts +746 -0
  547. package/src/mixins/__tests__/verifyChallenge.test.ts +130 -0
  548. package/src/mixins/__tests__/verifyServiceActingAs.test.ts +167 -0
  549. package/src/mixins/__tests__/webauthnAuth.test.ts +205 -0
  550. package/src/mixins/index.ts +201 -0
  551. package/src/mixins/mixinHelpers.ts +69 -0
  552. package/src/models/Topic.ts +46 -0
  553. package/src/models/interfaces.ts +786 -0
  554. package/src/models/session.ts +55 -0
  555. package/src/server/__tests__/auth.test.ts +78 -0
  556. package/src/server/__tests__/capabilityTicket.test.ts +111 -0
  557. package/src/server/__tests__/cors.socket.test.ts +225 -0
  558. package/src/server/__tests__/cors.test.ts +158 -0
  559. package/src/server/__tests__/rateLimit.test.ts +257 -0
  560. package/src/server/__tests__/safeFetch.test.ts +232 -0
  561. package/src/server/__tests__/securityHeaders.test.ts +348 -0
  562. package/src/server/__tests__/serviceTokenAttribution.test.ts +396 -0
  563. package/src/server/__tests__/userInvalidation.test.ts +203 -0
  564. package/src/server/__tests__/verifySecret.test.ts +40 -0
  565. package/src/server/auth.ts +271 -0
  566. package/src/server/capabilityTicket.ts +258 -0
  567. package/src/server/cors.ts +277 -0
  568. package/src/server/index.ts +136 -0
  569. package/src/server/rateLimit.ts +313 -0
  570. package/src/server/safeFetch.ts +586 -0
  571. package/src/server/securityHeaders.ts +467 -0
  572. package/src/server/userInvalidation.ts +199 -0
  573. package/src/server/verifySecret.ts +52 -0
  574. package/src/session/SessionClient.ts +1001 -0
  575. package/src/session/__tests__/SessionClient.additive.test.ts +153 -0
  576. package/src/session/__tests__/SessionClient.broadcastChannel.test.ts +114 -0
  577. package/src/session/__tests__/SessionClient.diagnostics.test.ts +91 -0
  578. package/src/session/__tests__/SessionClient.directory.test.ts +688 -0
  579. package/src/session/__tests__/SessionClient.httpIntegration.test.ts +65 -0
  580. package/src/session/__tests__/SessionClient.identityPin.test.ts +175 -0
  581. package/src/session/__tests__/SessionClient.rest.test.ts +119 -0
  582. package/src/session/__tests__/SessionClient.serverEvents.test.ts +72 -0
  583. package/src/session/__tests__/SessionClient.socket.test.ts +202 -0
  584. package/src/session/__tests__/SessionClient.socketFactory.test.ts +81 -0
  585. package/src/session/__tests__/SessionClient.state.test.ts +78 -0
  586. package/src/session/__tests__/SessionClient.switchTokenOrder.test.ts +170 -0
  587. package/src/session/__tests__/accountDelegationAccess.test.ts +104 -0
  588. package/src/session/__tests__/accountDialogController.test.ts +1969 -0
  589. package/src/session/__tests__/accountDialogShape.test.ts +118 -0
  590. package/src/session/__tests__/accountSwitchTargets.test.ts +137 -0
  591. package/src/session/__tests__/authStateStore.test.ts +451 -0
  592. package/src/session/__tests__/deviceDirectory.test.ts +422 -0
  593. package/src/session/__tests__/deviceSwitcherRows.test.ts +223 -0
  594. package/src/session/__tests__/identityPin.test.ts +131 -0
  595. package/src/session/__tests__/identitySession.test.ts +194 -0
  596. package/src/session/__tests__/projectSessionState.test.ts +105 -0
  597. package/src/session/__tests__/refresh.identityPin.test.ts +307 -0
  598. package/src/session/__tests__/refresh.test.ts +443 -0
  599. package/src/session/__tests__/sessionIntegration.test.ts +209 -0
  600. package/src/session/__tests__/sharedDeviceCredential.test.ts +300 -0
  601. package/src/session/accountDelegationAccess.ts +72 -0
  602. package/src/session/accountDialogController.ts +1494 -0
  603. package/src/session/accountSwitchTargets.ts +93 -0
  604. package/src/session/authStateStore.ts +502 -0
  605. package/src/session/createSessionClient.ts +39 -0
  606. package/src/session/deviceDirectory.ts +269 -0
  607. package/src/session/deviceSwitcherRows.ts +145 -0
  608. package/src/session/identityPin.ts +310 -0
  609. package/src/session/identitySession.ts +206 -0
  610. package/src/session/projectSessionState.ts +137 -0
  611. package/src/session/refresh.ts +517 -0
  612. package/src/session/sessionClientHost.ts +39 -0
  613. package/src/session/sharedDeviceCredential.ts +349 -0
  614. package/src/session/socketLoader.ts +30 -0
  615. package/src/shared/utils/colorUtils.ts +155 -0
  616. package/src/shared/utils/errorUtils.ts +181 -0
  617. package/src/shared/utils/index.ts +59 -0
  618. package/src/shared/utils/networkUtils.ts +248 -0
  619. package/src/shared/utils/themeUtils.ts +115 -0
  620. package/src/types/buffer.d.ts +97 -0
  621. package/src/types/expo-crypto.d.ts +16 -0
  622. package/src/types/expo-secure-store.d.ts +17 -0
  623. package/src/utils/__tests__/accountUtils.test.ts +151 -0
  624. package/src/utils/__tests__/activityId.test.ts +45 -0
  625. package/src/utils/__tests__/asyncUtils.test.ts +187 -0
  626. package/src/utils/__tests__/authWebUrl.test.ts +15 -0
  627. package/src/utils/__tests__/cacheKey.test.ts +0 -0
  628. package/src/utils/__tests__/coldBoot.test.ts +496 -0
  629. package/src/utils/__tests__/commonsApproval.test.ts +60 -0
  630. package/src/utils/__tests__/identityCacheSweep.test.ts +151 -0
  631. package/src/utils/__tests__/languageUtils.test.ts +219 -0
  632. package/src/utils/__tests__/oauthPkce.test.ts +272 -0
  633. package/src/utils/__tests__/officialOrigins.test.ts +31 -0
  634. package/src/utils/__tests__/oxyServiceEnvironment.test.ts +7 -0
  635. package/src/utils/__tests__/profileLinks.test.ts +180 -0
  636. package/src/utils/__tests__/redactUrl.test.ts +33 -0
  637. package/src/utils/__tests__/registrableApex.test.ts +62 -0
  638. package/src/utils/__tests__/textNormalization.test.ts +205 -0
  639. package/src/utils/__tests__/validationUtils.test.ts +703 -0
  640. package/src/utils/__tests__/webauthnOrigin.test.ts +83 -0
  641. package/src/utils/accountCacheSweep.ts +93 -0
  642. package/src/utils/accountUtils.ts +194 -0
  643. package/src/utils/activityId.ts +2 -0
  644. package/src/utils/apiUtils.ts +137 -0
  645. package/src/utils/asyncUtils.ts +280 -0
  646. package/src/utils/authHelpers.ts +148 -0
  647. package/src/utils/authWebUrl.ts +17 -0
  648. package/src/utils/avatarUtils.ts +39 -0
  649. package/src/utils/cache.ts +273 -0
  650. package/src/utils/cacheKey.ts +93 -0
  651. package/src/utils/coldBoot.ts +218 -0
  652. package/src/utils/commonsApproval.ts +39 -0
  653. package/src/utils/commonsDelivery.ts +132 -0
  654. package/src/utils/deviceManager.ts +203 -0
  655. package/src/utils/displayNamePolicyRanges.generated.ts +69 -0
  656. package/src/utils/edgeRegion.ts +2 -0
  657. package/src/utils/errorUtils.ts +352 -0
  658. package/src/utils/identityCacheSweep.ts +104 -0
  659. package/src/utils/languageUtils.ts +256 -0
  660. package/src/utils/oauthPkce.ts +392 -0
  661. package/src/utils/officialOrigins.ts +62 -0
  662. package/src/utils/oxyServiceEnvironment.ts +17 -0
  663. package/src/utils/platform.ts +139 -0
  664. package/src/utils/profileLinks.ts +91 -0
  665. package/src/utils/redactUrl.ts +28 -0
  666. package/src/utils/registrableApex.ts +41 -0
  667. package/src/utils/requestUtils.ts +237 -0
  668. package/src/utils/sessionUtils.ts +206 -0
  669. package/src/utils/textNormalization.ts +162 -0
  670. package/src/utils/userHandle.ts +49 -0
  671. package/src/utils/userIdentity.ts +60 -0
  672. package/src/utils/validationUtils.ts +390 -0
  673. package/src/utils/webauthnOrigin.ts +52 -0
@@ -0,0 +1,1001 @@
1
+ import {
2
+ deviceActivateResponseSchema,
3
+ deviceDirectorySchema,
4
+ deviceDirectorySyncSchema,
5
+ deviceSessionStateSchema,
6
+ deviceSessionSyncSchema,
7
+ safeParseContract,
8
+ SESSION_ACCOUNTS_CHANGED_EVENT,
9
+ sessionAccountsChangedEventSchema,
10
+ type DeviceDirectory,
11
+ type DeviceSessionState,
12
+ type DeviceSessionSync,
13
+ } from '@oxy.so/contracts';
14
+ import { logger } from '../logger';
15
+ import { computeIdentityTag } from '../utils/cacheKey';
16
+ import { resolveActiveContext, type DeviceContext } from './deviceDirectory';
17
+ import { getSocketIO } from './socketLoader';
18
+ import type { MinimalSocket, SocketIOFactory } from './socketLoader';
19
+
20
+ export interface TokenTransport {
21
+ /** Ensure this app holds a per-domain access token for state.activeAccountId (mint via the persisted refresh family / shared keychain). Best-effort. */
22
+ ensureActiveToken(state: DeviceSessionState): Promise<void>;
23
+ }
24
+
25
+ /**
26
+ * Where an applied device state came from, so consumers can decide how
27
+ * AUTHORITATIVE a zero-account ("signed out") verdict is:
28
+ * - `request` — the response to a direct REST call this client made
29
+ * (`bootstrap` / `switch` / `signOut` / `add`). A stable, server-authoritative
30
+ * verdict: an empty state here reflects a real sign-out or revocation, so the
31
+ * durable device credential MAY be erased.
32
+ * - `push` — an out-of-band Socket.IO `session_state` broadcast. Potentially
33
+ * transient (a reconnect race / another device's mutation), so an empty state
34
+ * here must NOT erase THIS origin's durable device credential — only clear the
35
+ * local UI session. A dead credential re-mints to `no_active_session` and
36
+ * resolves signed-out cleanly on the next boot; a wrongly-erased one cannot be
37
+ * recovered without a fresh sign-in.
38
+ */
39
+ export type SessionStateOrigin = 'request' | 'push';
40
+
41
+ export interface DeviceCredential {
42
+ deviceId: string;
43
+ deviceSecret: string;
44
+ }
45
+
46
+ export interface SessionClientHost {
47
+ makeRequest<T>(method: 'GET' | 'POST', url: string, data?: unknown, options?: { cache?: boolean }): Promise<T>;
48
+ getBaseURL(): string;
49
+ getAccessToken(): string | null;
50
+ /** Zero-cookie device credential for socket handshake when no bearer is planted yet. */
51
+ getDeviceCredential(): DeviceCredential | null;
52
+ onTokensChanged(listener: (token: string | null) => void): () => void;
53
+ setTokens(accessToken: string): void;
54
+ getCurrentAccountId(): string | null;
55
+ }
56
+
57
+ export interface SessionClientOptions {
58
+ transport?: TokenTransport;
59
+ /**
60
+ * Invoked when an APPLIED state has zero accounts — i.e. a device
61
+ * signout-all removed the last account from this device set. Providers use
62
+ * this to clear local session state and, for a `request`-origin verdict, the
63
+ * persisted {@link AuthStateStore} so a reload does not try to restore a
64
+ * session that no longer exists on the device.
65
+ *
66
+ * The {@link SessionStateOrigin} is passed so the consumer can gate the
67
+ * DESTRUCTIVE credential wipe: a `push`-origin empty state (a socket broadcast,
68
+ * possibly a transient reconnect artifact) must NOT erase the durable device
69
+ * credential — only a `request`-origin verdict (a direct REST sign-out /
70
+ * revocation) is authoritative enough for that.
71
+ *
72
+ * Only fires when a state is actually applied (revision advanced), never on
73
+ * a stale/rejected push. Exceptions thrown by the callback are isolated.
74
+ */
75
+ onUnauthenticated?: (origin: SessionStateOrigin) => void;
76
+ /**
77
+ * Statically-injected `socket.io-client` factory (its `io` export).
78
+ * `@oxy.so/services` lists `socket.io-client` as a real dependency and
79
+ * passes `io` in directly, so realtime session sync never
80
+ * depends on a runtime dynamic `import('socket.io-client')` of a bare
81
+ * specifier — which is bundler-fragile in Metro/Expo-web and Vite when
82
+ * `@oxy.so/core` is consumed as its published dist (the import resolves to
83
+ * nothing → `connectSocket` warns and falls back to REST-only). When this is
84
+ * provided, `connectSocket` uses it and never touches the lazy loader; when
85
+ * absent it falls back to `getSocketIO()`.
86
+ */
87
+ socketFactory?: SocketIOFactory;
88
+ /**
89
+ * The PINNED account id for an IDENTITY-BOUND client (the identity vault),
90
+ * or `null` for every ordinary account-mode client. Read as a function because
91
+ * the pin is resolved asynchronously at boot and can move when the identity
92
+ * session is re-established.
93
+ *
94
+ * While pinned this client tracks device state TRUTHFULLY — other apps'
95
+ * accounts and the device's real `activeAccountId` stay visible in `getState()`
96
+ * — but it NEVER re-binds its bearer to that active account: no `activeToken`
97
+ * is planted for a non-pinned account (from a sync response or a pushed
98
+ * `session_state`), the `TokenTransport` (whose entire job is converging on
99
+ * `activeAccountId`) is bypassed, and a push that switches the device does not
100
+ * trigger a token re-fetch. The pinned token's lifecycle belongs solely to the
101
+ * cold boot / re-mint lane, which mints it with an explicit `accountId`.
102
+ */
103
+ getPinnedAccountId?: () => string | null;
104
+ }
105
+
106
+ type StateListener = (state: DeviceSessionState | null) => void;
107
+ type DirectoryListener = (directory: DeviceDirectory | null) => void;
108
+
109
+ /**
110
+ * Same-origin `BroadcastChannel` name for instant, network-free session-state
111
+ * propagation across tabs of the SAME origin (e.g. two `accounts.oxy.so` tabs):
112
+ * when one authenticated tab commits an account switch / sign-out, siblings
113
+ * re-sync their device state without waiting on the socket. Cross-origin
114
+ * same-apex propagation rides the authenticated device socket; cross-APEX
115
+ * (mention.earth) relies on a reload — the documented limitation.
116
+ */
117
+ const SESSION_BROADCAST_CHANNEL = 'oxy.session';
118
+
119
+ /** A minimal, feature-detected `BroadcastChannel` surface (absent on native RN). */
120
+ interface SessionBroadcastChannel {
121
+ postMessage(message: unknown): void;
122
+ close(): void;
123
+ onmessage: ((event: { data: unknown }) => void) | null;
124
+ }
125
+
126
+ export class SessionClient {
127
+ private state: DeviceSessionState | null = null;
128
+ /**
129
+ * The device DIRECTORY (ADR 0002) — principals, the contexts each may act as,
130
+ * and which context is active. Held BESIDE `state` rather than replacing it:
131
+ * `state` is the flat compatibility projection every app renders until Phase 7
132
+ * moves it, and the two describe the same device at the same `revision`.
133
+ *
134
+ * `null` until something asks for it. A client that never calls
135
+ * {@link refreshDirectory} has no consumer for a directory and never pays for
136
+ * the round trip — which is also what keeps the whole account lane's request
137
+ * count unchanged.
138
+ */
139
+ private directory: DeviceDirectory | null = null;
140
+ private readonly listeners = new Set<StateListener>();
141
+ private readonly directoryListeners = new Set<DirectoryListener>();
142
+ protected socket: MinimalSocket | null = null;
143
+ private tokenUnsub: (() => void) | null = null;
144
+ private started = false;
145
+ /** Same-origin cross-tab state-propagation channel; null on platforms without BroadcastChannel. */
146
+ private channel: SessionBroadcastChannel | null = null;
147
+ /** App-facing subscriptions to named server-pushed socket events. */
148
+ private readonly serverEvents = new Map<string, Set<(payload: unknown) => void>>();
149
+ /** Event names already bound on the CURRENT socket instance. */
150
+ private readonly boundServerEvents = new Set<string>();
151
+
152
+ constructor(
153
+ protected readonly host: SessionClientHost,
154
+ protected readonly options: SessionClientOptions = {},
155
+ ) {}
156
+
157
+ getState(): DeviceSessionState | null {
158
+ return this.state;
159
+ }
160
+
161
+ /**
162
+ * The device directory, or `null` when this client has never read one.
163
+ * Populated by {@link refreshDirectory} / {@link activateContext} and kept
164
+ * fresh from there on.
165
+ */
166
+ getDirectory(): DeviceDirectory | null {
167
+ return this.directory;
168
+ }
169
+
170
+ /**
171
+ * The active `principal acting as account` pair, with the actor and the
172
+ * subject kept apart. `null` when no directory has been read, or when the
173
+ * device genuinely has no active context.
174
+ *
175
+ * This is the answer `getState()` cannot give: `activeAccountId` names the
176
+ * subject and says nothing about whose authentication is behind it.
177
+ */
178
+ getActiveContext(): DeviceContext | null {
179
+ return resolveActiveContext(this.directory);
180
+ }
181
+
182
+ /**
183
+ * The account this client's bearer is pinned to, or `null` when it follows the
184
+ * device's active account (the default). Resolvers are expected to be a plain
185
+ * synchronous read of already-resolved state (see
186
+ * {@link SessionClientOptions.getPinnedAccountId}).
187
+ */
188
+ private pinnedAccountId(): string | null {
189
+ return this.options.getPinnedAccountId?.() ?? null;
190
+ }
191
+
192
+ subscribe(listener: StateListener): () => void {
193
+ this.listeners.add(listener);
194
+ return () => {
195
+ this.listeners.delete(listener);
196
+ };
197
+ }
198
+
199
+ /**
200
+ * Subscribe to the directory half. Fires from the SAME {@link notify} as
201
+ * {@link subscribe}, so the flat state and the directory are never published
202
+ * at two different points of the ordering sequence — a directory subscriber
203
+ * and a state subscriber woken by one transition always see the same device
204
+ * revision under the same bearer.
205
+ */
206
+ subscribeDirectory(listener: DirectoryListener): () => void {
207
+ this.directoryListeners.add(listener);
208
+ return () => {
209
+ this.directoryListeners.delete(listener);
210
+ };
211
+ }
212
+
213
+ /**
214
+ * Subscribe to a named server-pushed Socket.IO event (e.g. `civic:attested`).
215
+ * Listeners survive reconnects and socket re-creation; the returned function
216
+ * unsubscribes. Payloads are delivered as-is — callers validate shape.
217
+ */
218
+ onServerEvent(event: string, listener: (payload: unknown) => void): () => void {
219
+ let listeners = this.serverEvents.get(event);
220
+ if (!listeners) {
221
+ listeners = new Set();
222
+ this.serverEvents.set(event, listeners);
223
+ }
224
+ listeners.add(listener);
225
+ this.bindServerEvent(event);
226
+ return () => {
227
+ listeners.delete(listener);
228
+ };
229
+ }
230
+
231
+ private bindServerEvent(event: string): void {
232
+ if (!this.socket || this.boundServerEvents.has(event)) return;
233
+ this.boundServerEvents.add(event);
234
+ this.socket.on(event, (payload: unknown) => {
235
+ const listeners = this.serverEvents.get(event);
236
+ if (!listeners) return;
237
+ for (const listener of [...listeners]) {
238
+ try {
239
+ listener(payload);
240
+ } catch (error) {
241
+ logger.warn('[SessionClient] server-event listener threw', { component: 'SessionClient' }, error);
242
+ }
243
+ }
244
+ });
245
+ }
246
+
247
+ protected notify(): void {
248
+ for (const listener of this.listeners) {
249
+ try {
250
+ listener(this.state);
251
+ } catch (error) {
252
+ logger.error('[SessionClient] subscriber threw', error);
253
+ }
254
+ }
255
+ for (const listener of this.directoryListeners) {
256
+ try {
257
+ listener(this.directory);
258
+ } catch (error) {
259
+ logger.error('[SessionClient] directory subscriber threw', error);
260
+ }
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Validate + last-writer-wins by revision. Returns true if applied.
266
+ *
267
+ * `activeToken` (sync path only) is the server-issued access token for
268
+ * `raw.activeAccountId`. When present and the state is applied, it is planted
269
+ * BEFORE any subscriber is notified so the bearer already belongs to the new
270
+ * active account — the local switch/bootstrap path then needs no redundant
271
+ * device-secret mint. Push-origin applies carry no token and rely on the
272
+ * mint-before-notify gate below.
273
+ *
274
+ * ORDERING INVARIANT: a subscriber must NEVER observe a newly-active account
275
+ * while the planted bearer still identifies the PREVIOUS one — otherwise a
276
+ * `useCurrentUser`-style refetch fires under the wrong account's token (the
277
+ * account-switch 404 race). So when a transport is available and the planted
278
+ * bearer does not already belong to `next.activeAccountId`, minting is awaited
279
+ * BEFORE `notify()`. This covers EVERY notify source (a switch push, a
280
+ * cross-device push, a cold mint), not just the initial "no bearer yet" case.
281
+ */
282
+ protected applyState(raw: unknown, origin: SessionStateOrigin = 'push', activeToken?: string): boolean {
283
+ const next = safeParseContract(deviceSessionStateSchema, raw);
284
+ if (!next) {
285
+ logger.warn('[SessionClient] discarded invalid session state');
286
+ return false;
287
+ }
288
+ // The revision is monotone ONLY within a single deviceId. When the incoming
289
+ // state belongs to a DIFFERENT device than the currently-applied one, reset
290
+ // the baseline and accept it regardless of revision: a freshly-converged
291
+ // device (low revision) must not lose last-writer-wins to a stale, higher-
292
+ // revision state from a retired device. Only when the deviceIds MATCH is the
293
+ // `revision <= current` guard a valid staleness check.
294
+ if (
295
+ this.state &&
296
+ next.deviceId === this.state.deviceId &&
297
+ next.revision <= this.state.revision
298
+ ) {
299
+ return false;
300
+ }
301
+ const previousState = this.state;
302
+ this.state = next;
303
+ if (next.accounts.length === 0) {
304
+ // A device with nobody signed in has no principals either. The directory
305
+ // is only ever refreshed by a bearer-carrying read and a sign-out leaves
306
+ // no bearer, so `settleDirectory` below cannot correct it — without this,
307
+ // the switcher would go on rendering the people who used to be here.
308
+ this.directory = null;
309
+ }
310
+ const pinnedAccountId = this.pinnedAccountId();
311
+ // Plant the sync-supplied active token (it is for `next.activeAccountId`)
312
+ // now — before the notify below — so the bearer matches the new active
313
+ // account when subscribers observe it. Guarded on difference to avoid a
314
+ // redundant token-change notification on an unchanged token (bootstrap
315
+ // restate). An identity-bound client only accepts it when the active account
316
+ // IS its pinned account — otherwise the token belongs to somebody else's
317
+ // switch and must never displace the pinned bearer.
318
+ if (
319
+ activeToken &&
320
+ next.activeAccountId !== null &&
321
+ (pinnedAccountId === null || next.activeAccountId === pinnedAccountId) &&
322
+ activeToken !== this.host.getAccessToken()
323
+ ) {
324
+ this.host.setTokens(activeToken);
325
+ }
326
+ // The transport's entire job is converging the bearer on `activeAccountId`,
327
+ // which is precisely what an identity-bound client must not do: its token is
328
+ // minted for the pinned account by the cold boot / re-mint lane. Bypass it
329
+ // while pinned (also removing the mint-before-notify gate, which exists only
330
+ // to keep the bearer and the ACTIVE account in step).
331
+ const transport = pinnedAccountId === null ? this.options.transport : null;
332
+ const activeAccountId = next.activeAccountId;
333
+ // Mint before notifying when the bearer does not already belong to the new
334
+ // active account: no bearer at all, an opaque bearer, OR a bearer for a
335
+ // DIFFERENT account. `computeIdentityTag` yields the token's `userId`/`id`
336
+ // for a real JWT (comparable to the account id) and a non-account sentinel
337
+ // otherwise, so a mismatch always resolves to "mint".
338
+ const needsMintBeforeNotify =
339
+ transport != null &&
340
+ next.accounts.length > 0 &&
341
+ (activeAccountId === null || computeIdentityTag(this.host.getAccessToken()) !== activeAccountId);
342
+
343
+ const publish = (): void => {
344
+ this.notify();
345
+ if (next.accounts.length === 0 && this.options.onUnauthenticated) {
346
+ try {
347
+ this.options.onUnauthenticated(origin);
348
+ } catch (error) {
349
+ logger.error('[SessionClient] onUnauthenticated threw', error);
350
+ }
351
+ }
352
+ };
353
+
354
+ // The directory half of the same ordering invariant: when this client holds
355
+ // a directory and the flat state has just moved past it, re-read the
356
+ // directory BEFORE anyone is notified — otherwise a directory-rendering
357
+ // consumer observes the PREVIOUS subject under the new subject's bearer,
358
+ // which is the account-switch race mirrored. `settleDirectory` returns null
359
+ // (and this stays synchronous) for every client that never read a
360
+ // directory, i.e. the whole account lane.
361
+ const finishApply = (): void => {
362
+ const settling = this.settleDirectory(next);
363
+ if (settling === null) {
364
+ publish();
365
+ return;
366
+ }
367
+ void settling.then(publish);
368
+ };
369
+
370
+ if (needsMintBeforeNotify) {
371
+ void transport.ensureActiveToken(next).then(finishApply).catch((error) => {
372
+ logger.warn('[SessionClient] ensureActiveToken failed — reverting session state', { component: 'SessionClient' }, error);
373
+ // Do NOT notify under a mismatched bearer. Revert to the last applied
374
+ // state so subscribers keep observing the account whose token is planted.
375
+ this.state = previousState ?? null;
376
+ });
377
+ } else {
378
+ if (transport) {
379
+ void transport.ensureActiveToken(next).catch((error) => {
380
+ logger.warn('[SessionClient] ensureActiveToken failed', { component: 'SessionClient' }, error);
381
+ });
382
+ }
383
+ finishApply();
384
+ }
385
+ return true;
386
+ }
387
+
388
+ /**
389
+ * Validate `{ state, activeToken }`, apply the state, and plant the active token host-side.
390
+ * Token-planting is decoupled from whether `applyState` advanced the revision: a socket push
391
+ * followed by this same `GET /state` fetch returns the SAME revision (applyState no-ops), but
392
+ * the token still needs to be planted. The account-match guard rejects a stale response for an
393
+ * account that is no longer active.
394
+ */
395
+ private applySync(raw: unknown): void {
396
+ const sync = safeParseContract(deviceSessionSyncSchema, raw);
397
+ if (!sync) {
398
+ const parsed = deviceSessionSyncSchema.safeParse(raw);
399
+ // Log field-level type diagnostics ONLY — never values. The payload carries tokens and
400
+ // session ids; issue.path/code and the invalid_type expected/received TYPE names are safe,
401
+ // but zod messages can embed offending values for other codes, so they are omitted.
402
+ const issues = parsed.success
403
+ ? []
404
+ : parsed.error.issues.map((issue) =>
405
+ issue.code === 'invalid_type'
406
+ ? { path: issue.path.join('.'), code: issue.code, expected: issue.expected, received: issue.received }
407
+ : { path: issue.path.join('.'), code: issue.code },
408
+ );
409
+ const keys = raw && typeof raw === 'object' ? Object.keys(raw) : [];
410
+ logger.warn('[SessionClient] discarded invalid session sync', { component: 'SessionClient', issues, keys });
411
+ return;
412
+ }
413
+ this.commitSync(sync);
414
+ }
415
+
416
+ /**
417
+ * The apply + token-plant half of {@link applySync}, on an ALREADY-VALIDATED
418
+ * sync. Split out so the context-aware removal lane — whose response is a
419
+ * different wire shape and therefore a different parse — reuses this ordering
420
+ * verbatim instead of re-deriving it. Two implementations of
421
+ * "plant before notify" is two chances to get it wrong once.
422
+ *
423
+ * Returns whether `applyState` applied, so a caller holding a second half
424
+ * (the directory) can decide whether anything still needs publishing.
425
+ */
426
+ private commitSync(sync: DeviceSessionSync): boolean {
427
+ // A `sync` is always the response to a direct REST call this client made
428
+ // (bootstrap / switch / signOut / add) → a `request`-origin, authoritative
429
+ // verdict. Hand the active token to `applyState`: in the applied path it is
430
+ // planted BEFORE notify (bearer matches the new active account when
431
+ // subscribers observe it, and no redundant device-secret mint is triggered).
432
+ const applied = this.applyState(sync.state, 'request', sync.activeToken?.accessToken);
433
+ // Equal-revision restate (this revision was already applied by a preceding
434
+ // socket push): `applyState` no-ops without planting, but the token still
435
+ // needs planting. Guard on the sync's active account STILL being the current
436
+ // active account so a stale response cannot adopt a token for an account a
437
+ // newer state already switched away from — and, when pinned, on that account
438
+ // being the PINNED one (same rule as the applied path in `applyState`).
439
+ const pinnedAccountId = this.pinnedAccountId();
440
+ if (
441
+ !applied &&
442
+ sync.activeToken &&
443
+ this.state &&
444
+ sync.state.activeAccountId !== null &&
445
+ sync.state.activeAccountId === this.state.activeAccountId &&
446
+ (pinnedAccountId === null || sync.state.activeAccountId === pinnedAccountId) &&
447
+ sync.activeToken.accessToken !== this.host.getAccessToken()
448
+ ) {
449
+ this.host.setTokens(sync.activeToken.accessToken);
450
+ }
451
+ return applied;
452
+ }
453
+
454
+ /**
455
+ * Validate + last-writer-wins, `deviceId`-SCOPED exactly as {@link applyState}
456
+ * is for the flat half: a directory belonging to a DIFFERENT device resets the
457
+ * baseline and is accepted at any revision, so a freshly-converged device
458
+ * cannot lose to a retired device's higher number.
459
+ *
460
+ * The comparison itself is deliberately WEAKER than the flat state's. There it
461
+ * is `revision <= current` — correct, because a `DeviceSessionState` arrives
462
+ * out of band over a socket, so a straggler can genuinely land after a newer
463
+ * one. A directory only ever arrives as the response to a request THIS client
464
+ * just made, so the newest response is the freshest answer and only a strictly
465
+ * LOWER revision can be a straggler (two GETs racing).
466
+ *
467
+ * Equal-revision reads are not redundant, and rejecting them was a bug: the
468
+ * directory includes rows projected from the account GRAPH, and the server
469
+ * materializes a context for every account a principal may act as WITHOUT
470
+ * bumping `revision` — deliberately, since `revision` tracks what the device
471
+ * holds and must never advance on a read. So a newly-granted `account:act_as`,
472
+ * and a removed-then-rematerialized context under its NEW id, both appear at
473
+ * an unchanged revision. Under `<=` neither would ever be seen until some
474
+ * unrelated device mutation happened to move the number.
475
+ *
476
+ * Notifies nothing. Every caller decides where in its own ordering sequence
477
+ * the publish belongs.
478
+ */
479
+ private applyDirectory(raw: unknown): boolean {
480
+ const next = safeParseContract(deviceDirectorySchema, raw);
481
+ if (!next) {
482
+ logger.warn('[SessionClient] discarded invalid device directory');
483
+ return false;
484
+ }
485
+ if (
486
+ this.directory &&
487
+ next.deviceId === this.directory.deviceId &&
488
+ next.revision < this.directory.revision
489
+ ) {
490
+ return false;
491
+ }
492
+ this.directory = next;
493
+ return true;
494
+ }
495
+
496
+ /** `GET /session/device/directory` → {@link applyDirectory}. No notify. */
497
+ private async fetchDirectory(): Promise<boolean> {
498
+ const res = await this.host.makeRequest<unknown>('GET', '/session/device/directory', undefined, { cache: false });
499
+ return this.applyDirectory(res);
500
+ }
501
+
502
+ /**
503
+ * Re-read the directory when the flat state has moved past it, returning the
504
+ * in-flight work so the caller can hold its notify until both halves describe
505
+ * the same revision.
506
+ *
507
+ * `null` — meaning "nothing to settle, stay synchronous" — when this client
508
+ * holds no directory (nobody reads one), when the directory is already at or
509
+ * ahead of the state, or when there is no bearer to make the call with.
510
+ * Never rejects: a failed refresh leaves the previous directory in place and
511
+ * the next transition tries again; it must not swallow the flat state's
512
+ * notify.
513
+ */
514
+ private settleDirectory(state: DeviceSessionState): Promise<void> | null {
515
+ const held = this.directory;
516
+ if (held === null) {
517
+ return null;
518
+ }
519
+ if (held.deviceId === state.deviceId && held.revision >= state.revision) {
520
+ return null;
521
+ }
522
+ if (!this.host.getAccessToken()) {
523
+ return null;
524
+ }
525
+ return this.fetchDirectory().then(
526
+ () => undefined,
527
+ (error: unknown) => {
528
+ logger.warn('[SessionClient] directory refresh failed', { component: 'SessionClient' }, error);
529
+ },
530
+ );
531
+ }
532
+
533
+ /**
534
+ * The highest device revision this client currently knows for `deviceId`,
535
+ * across BOTH halves, or `null` when it knows nothing about that device.
536
+ *
537
+ * Used to read the server's `changed` flag, which
538
+ * `POST /session/device/activate` deliberately does not carry: the revision
539
+ * already says whether the device moved, and a second field saying the same
540
+ * thing is a second field that can disagree with the first.
541
+ */
542
+ private knownRevisionFor(deviceId: string): number | null {
543
+ const fromDirectory = this.directory?.deviceId === deviceId ? this.directory.revision : null;
544
+ const fromState = this.state?.deviceId === deviceId ? this.state.revision : null;
545
+ if (fromDirectory === null) return fromState;
546
+ if (fromState === null) return fromDirectory;
547
+ return Math.max(fromDirectory, fromState);
548
+ }
549
+
550
+ /**
551
+ * Plant the bearer `POST /session/device/activate` returned for the newly
552
+ * active context, under the same guards {@link applyState} applies to a
553
+ * sync-supplied `activeToken`: never for a null active context, never a
554
+ * foreign account's token while pinned, and never a redundant re-plant of the
555
+ * token already held.
556
+ *
557
+ * `activeToken: null` is not an error — it is an identity-pinned client, or a
558
+ * caller whose application is not entitled to a bearer for the new context.
559
+ */
560
+ private plantActiveContextToken(directory: DeviceDirectory, accessToken: string | undefined): void {
561
+ if (!accessToken) {
562
+ return;
563
+ }
564
+ const subjectAccountId = resolveActiveContext(directory)?.subject.accountId ?? null;
565
+ if (subjectAccountId === null) {
566
+ return;
567
+ }
568
+ const pinnedAccountId = this.pinnedAccountId();
569
+ if (pinnedAccountId !== null && subjectAccountId !== pinnedAccountId) {
570
+ return;
571
+ }
572
+ if (accessToken === this.host.getAccessToken()) {
573
+ return;
574
+ }
575
+ this.host.setTokens(accessToken);
576
+ }
577
+
578
+ /**
579
+ * Bring the FLAT projection back in step after a context activation.
580
+ *
581
+ * The activation response answers with the directory and a bearer and
582
+ * deliberately not with `DeviceSessionState` (ADR 0002) — but every app that
583
+ * has not moved to the directory still renders from `getState()`, and leaving
584
+ * it a revision behind would show the PREVIOUS subject under the new
585
+ * subject's bearer. So it is settled BEFORE the notify, not after.
586
+ *
587
+ * This is also what converges the bearer when the activation returned no
588
+ * token: `GET /session/device/state` mints one for the active account, and
589
+ * `applyState`'s own mint-before-notify gate holds its notify until it lands.
590
+ *
591
+ * Non-fatal on failure — the directory is applied and (usually) the bearer is
592
+ * planted; the socket push or the next bootstrap catches the flat half up. A
593
+ * network blip must not turn a completed activation into a thrown error.
594
+ */
595
+ private async reconcileFlatState(directory: DeviceDirectory): Promise<void> {
596
+ if (
597
+ this.state &&
598
+ this.state.deviceId === directory.deviceId &&
599
+ this.state.revision >= directory.revision
600
+ ) {
601
+ return;
602
+ }
603
+ if (!this.host.getAccessToken()) {
604
+ return;
605
+ }
606
+ try {
607
+ await this.bootstrap();
608
+ } catch (error) {
609
+ logger.warn('[SessionClient] flat-state reconcile after activation failed', { component: 'SessionClient' }, error);
610
+ }
611
+ }
612
+
613
+ async bootstrap(): Promise<void> {
614
+ const res = await this.host.makeRequest<unknown>('GET', '/session/device/state', undefined, { cache: false });
615
+ this.applySync(res);
616
+ }
617
+
618
+ /**
619
+ * Adopt server-authoritative state already returned by another session
620
+ * endpoint. This avoids immediately re-reading `/session/device/state` after
621
+ * a successful device-token mint while preserving the same validation,
622
+ * revision ordering and notification path as `bootstrap()`.
623
+ */
624
+ adoptState(state: DeviceSessionState): boolean {
625
+ return this.applyState(state, 'request');
626
+ }
627
+
628
+ /**
629
+ * Read `GET /session/device/directory` and publish it.
630
+ *
631
+ * Calling this is what opts a client into the directory: from here on every
632
+ * applied device state re-reads it (see {@link settleDirectory}), so the two
633
+ * halves stay at one revision without the caller polling.
634
+ */
635
+ async refreshDirectory(): Promise<void> {
636
+ if (await this.fetchDirectory()) {
637
+ this.notify();
638
+ }
639
+ }
640
+
641
+ /**
642
+ * `POST /session/device/activate` — make one `principal acting as account`
643
+ * context active (ADR 0002).
644
+ *
645
+ * The body is `{ contextId }` and nothing else: an `accountId` cannot name
646
+ * what to activate on a device where two people can both reach the same
647
+ * organization, and the server refuses a body carrying one rather than
648
+ * guessing inside an authorization path.
649
+ *
650
+ * The sequence is the ADR's ordering invariant, in order — commit the bearer
651
+ * for the new context, publish the new snapshot, notify — with the flat
652
+ * projection reconciled in the middle so no consumer can observe the two
653
+ * halves disagreeing.
654
+ *
655
+ * An IDEMPOTENT activation (the target was already active) moves no revision,
656
+ * so it reconciles nothing and wakes no sibling tab, mirroring the server's
657
+ * "bumps nothing and broadcasts nothing". `switchAccount` remains the
658
+ * compatibility path for callers still keyed on account ids.
659
+ */
660
+ async activateContext(contextId: string): Promise<void> {
661
+ const res = await this.host.makeRequest<unknown>('POST', '/session/device/activate', { contextId }, { cache: false });
662
+ const activation = safeParseContract(deviceActivateResponseSchema, res);
663
+ if (!activation) {
664
+ logger.warn('[SessionClient] discarded invalid activation response');
665
+ return;
666
+ }
667
+ const known = this.knownRevisionFor(activation.directory.deviceId);
668
+ const moved = known === null || activation.directory.revision > known;
669
+ this.plantActiveContextToken(activation.directory, activation.activeToken?.accessToken);
670
+ // Applied BEFORE the reconcile, and only then: the reconcile's own
671
+ // `GET /session/device/state` runs the full apply path, whose
672
+ // `settleDirectory` would otherwise see a stale directory and issue a
673
+ // second, redundant `GET /session/device/directory` for the revision we are
674
+ // already holding in hand.
675
+ const applied = this.applyDirectory(activation.directory);
676
+ if (moved) {
677
+ await this.reconcileFlatState(activation.directory);
678
+ }
679
+ if (applied) {
680
+ this.notify();
681
+ }
682
+ if (moved) {
683
+ this.postCommitPing();
684
+ }
685
+ }
686
+
687
+ async switchAccount(accountId: string): Promise<void> {
688
+ const res = await this.host.makeRequest<unknown>('POST', '/session/device/switch', { accountId }, { cache: false });
689
+ this.applySync(res);
690
+ this.postCommitPing();
691
+ }
692
+
693
+ /**
694
+ * The FLAT removal meanings, unchanged: `{ accountId }` removes that account
695
+ * however it is reached — plus the operator cascade — and `{ all: true }`
696
+ * removes the whole device including its credentials.
697
+ *
698
+ * `{ accountId }` is deliberately still account-grained. On a device holding
699
+ * two people it removes BOTH of their routes to that account, which is the
700
+ * right meaning for "sign this account out of this device" and the wrong one
701
+ * for "this person is done here" — see {@link signOutContext} and
702
+ * {@link signOutPrincipal} for the two that can tell those apart.
703
+ */
704
+ async signOut(target: { accountId: string } | { all: true }): Promise<void> {
705
+ const res = await this.host.makeRequest<unknown>('POST', '/session/device/signout', target, { cache: false });
706
+ this.applySync(res);
707
+ this.postCommitPing();
708
+ }
709
+
710
+ /**
711
+ * Remove ONE `principal → account` pair, and only that pair.
712
+ *
713
+ * Never the account across the device: the same organization reached through
714
+ * a second person is a different session, a different audit actor and a
715
+ * different revocation path, and it stays. That distinction is unreachable
716
+ * through {@link signOut}, whose `accountId` cannot name which route to drop.
717
+ *
718
+ * Removal is not permanent while the membership lives — the server offers the
719
+ * pair again on the next directory read, as `onDevice: false` under a NEW id.
720
+ */
721
+ async signOutContext(contextId: string): Promise<void> {
722
+ await this.removeFromDevice({ contextId });
723
+ }
724
+
725
+ /**
726
+ * Remove ONE PERSON and every context they reach — and nobody else's,
727
+ * including when another principal independently operates the same account.
728
+ */
729
+ async signOutPrincipal(principalId: string): Promise<void> {
730
+ await this.removeFromDevice({ principalId });
731
+ }
732
+
733
+ /**
734
+ * The shared apply path for both context-aware removals.
735
+ *
736
+ * The response is `{directory, state, activeToken}` — its own contract, never
737
+ * `deviceSessionSyncSchema`, which would strip the directory silently. Both
738
+ * halves move in one server transition (a removal elects a replacement active
739
+ * context), so both are applied before anything is published: the directory
740
+ * first, so the flat apply's own `settleDirectory` sees a current directory
741
+ * and does not issue a redundant `GET /session/device/directory` for the
742
+ * revision already in hand.
743
+ *
744
+ * Token-before-notify is `commitSync`'s, reused verbatim rather than
745
+ * re-derived — including the equal-revision plant when a socket push already
746
+ * applied this revision.
747
+ */
748
+ private async removeFromDevice(target: { contextId: string } | { principalId: string }): Promise<void> {
749
+ const res = await this.host.makeRequest<unknown>('POST', '/session/device/signout', target, { cache: false });
750
+ const removal = safeParseContract(deviceDirectorySyncSchema, res);
751
+ if (!removal) {
752
+ logger.warn('[SessionClient] discarded invalid device removal response');
753
+ return;
754
+ }
755
+ const directoryApplied = this.applyDirectory(removal.directory);
756
+ const stateApplied = this.commitSync({ state: removal.state, activeToken: removal.activeToken });
757
+ // `commitSync` publishes whenever the flat state moved. When only the
758
+ // directory did — a socket push already applied this revision — the
759
+ // directory half would otherwise never reach a subscriber.
760
+ if (directoryApplied && !stateApplied) {
761
+ this.notify();
762
+ }
763
+ this.postCommitPing();
764
+ }
765
+
766
+ async addCurrentAccount(): Promise<void> {
767
+ const res = await this.host.makeRequest<unknown>('POST', '/session/device/add', undefined, { cache: false });
768
+ this.applySync(res);
769
+ this.postCommitPing();
770
+ }
771
+
772
+ /**
773
+ * Register the just-signed-in account into the device set AND make it the
774
+ * ACTIVE account — the explicit user-intent activation a sign-in UI performs.
775
+ *
776
+ * `addCurrentAccount` alone honors the server's `activate:'if-empty'` policy
777
+ * (a new account does NOT steal focus from an already-active one), which is
778
+ * correct for a background/silent add but wrong right after a deliberate
779
+ * sign-in. This adds, then switches to the target so the UI lands on the
780
+ * account the user just authenticated.
781
+ *
782
+ * An identity-bound (pinned) client only ADDS: its own session is minted for
783
+ * the pinned account explicitly, so switching the device would gratuitously
784
+ * re-elect the active account under every OTHER app on this device — a
785
+ * mutation a pinned client must never make.
786
+ *
787
+ * @param accountId - The signed-in account id (e.g. `session.user.id`). When
788
+ * omitted, falls back to the host's current-account ref. If neither
789
+ * resolves, the add still applies and no switch is performed.
790
+ */
791
+ async registerAndActivate(accountId?: string): Promise<void> {
792
+ await this.addCurrentAccount();
793
+ if (this.pinnedAccountId() !== null) return;
794
+ const target = accountId ?? this.host.getCurrentAccountId();
795
+ if (target && this.state?.activeAccountId !== target) {
796
+ await this.switchAccount(target);
797
+ }
798
+ }
799
+
800
+ async start(): Promise<void> {
801
+ if (this.started) return;
802
+ this.started = true;
803
+ this.tokenUnsub = this.host.onTokensChanged((token) => {
804
+ // A rotated/fresh bearer landed — reconnect a dropped socket so its
805
+ // handshake re-runs with the current token. Sign-out (null token) keeps
806
+ // the device-scoped socket when a device credential is available.
807
+ if (!this.socket) return;
808
+ if (token) {
809
+ if (!this.socket.connected) {
810
+ this.socket.connect();
811
+ }
812
+ return;
813
+ }
814
+ const cred = this.host.getDeviceCredential();
815
+ if (cred && !this.socket.connected) {
816
+ this.socket.connect();
817
+ }
818
+ });
819
+ this.openBroadcastChannel();
820
+ // Device-scoped socket: bearer when authenticated, else deviceId+deviceSecret so
821
+ // signed-out tabs still receive `session_state` and can mint on change.
822
+ if (this.host.getAccessToken()) {
823
+ try {
824
+ await this.bootstrap();
825
+ } catch (error) {
826
+ logger.warn('[SessionClient] bootstrap during start failed (non-fatal)', { component: 'SessionClient' }, error);
827
+ }
828
+ }
829
+ await this.connectSocket();
830
+ }
831
+
832
+ stop(): void {
833
+ this.started = false;
834
+ if (this.tokenUnsub) {
835
+ this.tokenUnsub();
836
+ this.tokenUnsub = null;
837
+ }
838
+ if (this.channel) {
839
+ this.channel.onmessage = null;
840
+ try {
841
+ this.channel.close();
842
+ } catch (error) {
843
+ logger.debug('[SessionClient] BroadcastChannel close failed', { component: 'SessionClient' }, error);
844
+ }
845
+ this.channel = null;
846
+ }
847
+ if (this.socket) {
848
+ this.socket.disconnect();
849
+ this.socket = null;
850
+ this.boundServerEvents.clear();
851
+ }
852
+ }
853
+
854
+ private async connectSocket(): Promise<void> {
855
+ // Prefer a statically-injected factory (services/auth-sdk bundle
856
+ // socket.io-client as a real dep); fall back to the lazy loader — and warn
857
+ // if THAT yields nothing — only when no factory was injected.
858
+ const io = this.options.socketFactory ?? (await getSocketIO());
859
+ if (!io) {
860
+ logger.warn('[SessionClient] no socket.io-client; running REST-only (no realtime sync)', { component: 'SessionClient' });
861
+ return;
862
+ }
863
+ if (!this.started) return; // stopped while the dynamic import was in flight
864
+
865
+ const token = this.host.getAccessToken();
866
+ const deviceCredential = this.host.getDeviceCredential();
867
+ if (!token && !deviceCredential) return;
868
+
869
+ const socket = io(this.host.getBaseURL(), {
870
+ transports: ['websocket'],
871
+ autoConnect: true,
872
+ reconnection: true,
873
+ reconnectionAttempts: Number.POSITIVE_INFINITY,
874
+ reconnectionDelay: 1000,
875
+ reconnectionDelayMax: 10000,
876
+ auth: (cb: (data: Record<string, string>) => void) => {
877
+ const bearer = this.host.getAccessToken();
878
+ if (bearer) {
879
+ cb({ token: bearer });
880
+ return;
881
+ }
882
+ const cred = this.host.getDeviceCredential();
883
+ if (cred) {
884
+ cb({ deviceId: cred.deviceId, deviceSecret: cred.deviceSecret });
885
+ return;
886
+ }
887
+ cb({ token: '' });
888
+ },
889
+ });
890
+ socket.on('session_state', (payload: unknown) => {
891
+ // A socket broadcast is a `push`-origin — potentially transient, so an
892
+ // empty state here must not erase the durable device credential.
893
+ const applied = this.applyState(payload, 'push');
894
+ if (!applied) return;
895
+ // An identity-bound client tracks the pushed state but NEVER chases the
896
+ // device's active account: re-fetching here would only pull an
897
+ // `activeToken` for somebody else's switch, which the plant guards would
898
+ // then discard. Skip the round-trip entirely.
899
+ if (this.pinnedAccountId() !== null) return;
900
+ // A push changed the active account on another device/tab — re-fetch state
901
+ // to plant the access token for the newly-active account. When this tab is
902
+ // still signed out, applyState mints via ensureActiveToken first; bootstrap
903
+ // requires a bearer and must not run until then.
904
+ const active = this.state?.activeAccountId ?? null;
905
+ if (active && active !== this.host.getCurrentAccountId() && this.host.getAccessToken()) {
906
+ void this.bootstrap().catch((error) => {
907
+ logger.warn('[SessionClient] post-push token fetch failed', { component: 'SessionClient' }, error);
908
+ });
909
+ }
910
+ });
911
+ socket.on(SESSION_ACCOUNTS_CHANGED_EVENT, (payload: unknown) => {
912
+ this.onSessionAccountsChanged(payload);
913
+ });
914
+ this.socket = socket;
915
+ // (Re)bind app-facing server-event subscriptions on the fresh socket.
916
+ this.boundServerEvents.clear();
917
+ for (const event of this.serverEvents.keys()) {
918
+ this.bindServerEvent(event);
919
+ }
920
+ }
921
+
922
+ /**
923
+ * Handle the token-free `session_accounts_changed` signal (room `user:<userId>`).
924
+ *
925
+ * Unlike `session_state` (device-scoped, carries the new state to APPLY), this
926
+ * reaches ALL of a user's connected sockets across their devices/origins and is
927
+ * a pure SIGNAL: it carries no token, no secret, and no account bodies. The only
928
+ * trustworthy bit is "something changed for this user", so — matching the
929
+ * `session_state` contract's guidance — we re-fetch our OWN authoritative device
930
+ * state (`bootstrap` → `GET /session/device/state`) and let the existing
931
+ * `applyState` revision guard reconcile it. We never trust any field on the event
932
+ * beyond routing it to the current user.
933
+ *
934
+ * The refetch is a private (bearer) call: the socket only joins `user:<userId>`
935
+ * when authenticated, so a signed-out client never receives this — but we guard
936
+ * the bearer anyway so a race at sign-out can't 401.
937
+ */
938
+ private onSessionAccountsChanged(payload: unknown): void {
939
+ const event = safeParseContract(sessionAccountsChangedEventSchema, payload);
940
+ if (!event) {
941
+ logger.warn('[SessionClient] discarded invalid session_accounts_changed', { component: 'SessionClient' });
942
+ return;
943
+ }
944
+ // The socket is in `user:<activeUserId>` for the planted bearer, so this should
945
+ // always be the current user; ignore a foreign id defensively (out-of-band relay).
946
+ if (event.userId !== this.host.getCurrentAccountId()) return;
947
+ if (!this.host.getAccessToken()) return;
948
+ void this.bootstrap().catch((error) => {
949
+ logger.warn('[SessionClient] session_accounts_changed refetch failed', { component: 'SessionClient' }, error);
950
+ });
951
+ }
952
+
953
+ /**
954
+ * Open the same-origin `BroadcastChannel` (web only). A sibling tab that
955
+ * commits an account switch / sign-out posts a wake ping; on receipt an
956
+ * authenticated tab re-syncs its device state — instant + network-free for the
957
+ * common "two tabs of the same origin" case, with no state (and no tokens)
958
+ * ever crossing the channel. No-op on native (no BroadcastChannel).
959
+ */
960
+ private openBroadcastChannel(): void {
961
+ if (this.channel) return;
962
+ const Ctor = (globalThis as { BroadcastChannel?: new (name: string) => SessionBroadcastChannel }).BroadcastChannel;
963
+ if (typeof Ctor !== 'function') return; // native RN / SSR — feature absent
964
+ let channel: SessionBroadcastChannel;
965
+ try {
966
+ channel = new Ctor(SESSION_BROADCAST_CHANNEL);
967
+ } catch (error) {
968
+ logger.debug('[SessionClient] BroadcastChannel unavailable', { component: 'SessionClient' }, error);
969
+ return;
970
+ }
971
+ channel.onmessage = (event) => {
972
+ if (!event || typeof event.data !== 'object' || event.data === null) return;
973
+ if ((event.data as { type?: unknown }).type !== 'commit') return;
974
+ // BroadcastChannel never echoes to the posting context, so this is a
975
+ // sibling's commit. An authenticated tab re-syncs its device state; the
976
+ // re-sync does not re-post, so there is no cross-tab ping loop.
977
+ if (this.host.getAccessToken()) {
978
+ void this.bootstrap().catch((error) => {
979
+ logger.warn('[SessionClient] broadcast re-sync failed', { component: 'SessionClient' }, error);
980
+ });
981
+ }
982
+ };
983
+ this.channel = channel;
984
+ }
985
+
986
+ /**
987
+ * Wake same-origin sibling tabs after a locally-initiated session mutation.
988
+ * Opens the channel lazily: a sign-in registers the account (`addCurrentAccount`
989
+ * / `switchAccount`) BEFORE `start()` runs, so the ping must not depend on
990
+ * `start()` having opened the channel first.
991
+ */
992
+ private postCommitPing(): void {
993
+ this.openBroadcastChannel();
994
+ if (!this.channel) return;
995
+ try {
996
+ this.channel.postMessage({ type: 'commit', at: Date.now() });
997
+ } catch (error) {
998
+ logger.debug('[SessionClient] BroadcastChannel post failed', { component: 'SessionClient' }, error);
999
+ }
1000
+ }
1001
+ }