@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,1553 @@
1
+ /**
2
+ * Unified HTTP Service
3
+ *
4
+ * Consolidates HttpClient + RequestManager into a single efficient class.
5
+ * Uses native fetch instead of axios for smaller bundle size.
6
+ *
7
+ * Handles:
8
+ * - Authentication (token management, auto-refresh)
9
+ * - Caching (TTL-based)
10
+ * - Deduplication (concurrent requests)
11
+ * - Retry logic
12
+ * - Error handling
13
+ * - Request queuing
14
+ */
15
+
16
+ import { TTLCache, registerCacheForCleanup } from './utils/cache';
17
+ import { RequestDeduplicator, RequestQueue, SimpleLogger } from './utils/requestUtils';
18
+ import { retryAsync } from './utils/asyncUtils';
19
+ import { handleHttpError, parseHttpErrorBody } from './utils/errorUtils';
20
+ import { jwtDecode } from 'jwt-decode';
21
+ import { isNative, getPlatformOS } from './utils/platform';
22
+ import { isReactNative } from '@oxy.so/protocol';
23
+ import { computeIdentityTag, fnv1a32 } from './utils/cacheKey';
24
+ import { redactUrlQuery } from './utils/redactUrl';
25
+ import type { OxyConfig } from './models/interfaces';
26
+ import type { DeviceSecretMintOutcome } from './session/refresh';
27
+ import { OxyAuthenticationError } from './OxyServices.errors';
28
+ import { getBrowserEdgeRegionHeader } from './utils/edgeRegion';
29
+ import { getBrowserActivityIdHeader } from './utils/activityId';
30
+
31
+ /**
32
+ * Check if we're running in a native app environment (React Native, not web)
33
+ * This is used to determine CSRF handling mode
34
+ */
35
+ const isNativeApp = isNative();
36
+
37
+ interface JwtPayload {
38
+ exp?: number;
39
+ userId?: string;
40
+ id?: string;
41
+ sessionId?: string;
42
+ [key: string]: unknown;
43
+ }
44
+
45
+ export type AuthRefreshReason = 'preflight' | 'response-401';
46
+ export type AuthRefreshHandler = (reason: AuthRefreshReason) => Promise<string | null>;
47
+ export type AccessTokenProvider = () => string | null;
48
+
49
+ /**
50
+ * A low-level authenticated request whose response body remains unread.
51
+ *
52
+ * This is intended for streaming protocols such as SSE. The body must already
53
+ * be serialised so it can be replayed once after an access-token refresh.
54
+ */
55
+ export interface AuthenticatedResponseRequest {
56
+ method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
57
+ url: string;
58
+ body?: string;
59
+ headers?: Record<string, string>;
60
+ signal?: AbortSignal;
61
+ }
62
+
63
+ /**
64
+ * Structural type that captures the multipart-write surface every supported
65
+ * FormData implementation exposes (browser, React Native, Node `form-data`
66
+ * polyfill, jsdom, undici, etc). We type-narrow against this in
67
+ * `isFormData()` so callers don't have to know which runtime produced the
68
+ * value.
69
+ *
70
+ * Deliberately mirrored from the lib.dom `FormData` interface — kept as a
71
+ * local type because @types/node and @types/react-native model FormData
72
+ * differently and a single import wouldn't be safe in both bundles.
73
+ */
74
+ interface FormDataLike {
75
+ append(name: string, value: unknown, fileName?: string): void;
76
+ delete(name: string): void;
77
+ get(name: string): unknown;
78
+ getAll(name: string): unknown[];
79
+ has(name: string): boolean;
80
+ }
81
+
82
+ export interface RequestOptions {
83
+ cache?: boolean;
84
+ cacheTTL?: number;
85
+ deduplicate?: boolean;
86
+ retry?: boolean;
87
+ maxRetries?: number;
88
+ timeout?: number;
89
+ signal?: AbortSignal;
90
+ headers?: Record<string, string>;
91
+ responseType?: 'blob';
92
+ /**
93
+ * Skip BOTH the bearer auth header (and its near-expiry preflight refresh)
94
+ * AND the 401-driven auto-refresh/retry for this request.
95
+ *
96
+ * Required for the body-authenticated device-secret mint (`POST
97
+ * /session/device/token`): it does not need a bearer, and — critically — it is
98
+ * itself invoked from inside the registered `AuthRefreshHandler`. If it went
99
+ * through the normal preflight, `getAuthHeader` would call
100
+ * `refreshAccessToken` while the handler-owning `tokenRefreshPromise` is
101
+ * still in flight and await ITSELF (deadlock). Skipping auth makes the
102
+ * mint call fully independent of the current (near-expired) bearer.
103
+ */
104
+ skipAuth?: boolean;
105
+ /**
106
+ * Execute this request WITHOUT taking a `RequestQueue` slot — run it directly.
107
+ *
108
+ * A queue slot represents network occupancy for ordinary DATA requests. The
109
+ * CONTROL-PLANE calls the auth lane depends on (the device-secret mint, `POST
110
+ * /session/device/token`, reached from `getAuthHeader` → `refreshAccessToken`)
111
+ * must never compete for a slot: when `maxConcurrentRequests` requests are all
112
+ * parked awaiting that very mint, a queued mint could never acquire a slot to
113
+ * run — a systemic deadlock. `bypassQueue` lets the mint run even when the
114
+ * queue is saturated. (The auth preflight is also resolved OUTSIDE the slot in
115
+ * `request()`, so ordinary requests never hold a slot while awaiting the mint;
116
+ * this flag is the explicit guarantee for the mint itself.)
117
+ */
118
+ bypassQueue?: boolean;
119
+ }
120
+
121
+ interface RequestConfig extends RequestOptions {
122
+ method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
123
+ url: string;
124
+ data?: unknown;
125
+ params?: Record<string, unknown>;
126
+ /** @internal Used to prevent infinite auth retry loops */
127
+ _isAuthRetry?: boolean;
128
+ /** @internal Used to prevent infinite CSRF retry loops */
129
+ _isCsrfRetry?: boolean;
130
+ }
131
+
132
+ /**
133
+ * Default per-request timeout (ms) when neither the call site nor
134
+ * {@link OxyConfig.requestTimeout} overrides it. Kept tight so a stalled
135
+ * endpoint surfaces as an `AbortError` quickly rather than blocking the
136
+ * request queue.
137
+ */
138
+ const DEFAULT_REQUEST_TIMEOUT_MS = 5000;
139
+
140
+ /**
141
+ * Timeout (ms) for the dedicated `GET /csrf-token` fetch. Independent of the
142
+ * regular request timeout: this is a small, fast, unauthenticated call and
143
+ * should never inherit a longer per-request budget.
144
+ */
145
+ const CSRF_FETCH_TIMEOUT_MS = 5000;
146
+
147
+ /**
148
+ * Number of attempts for fetching a CSRF token before giving up. The first
149
+ * failure is usually a cold edge/cookie race; a single retry recovers it
150
+ * without masking a genuinely broken `/csrf-token` route.
151
+ */
152
+ const CSRF_FETCH_MAX_ATTEMPTS = 2;
153
+
154
+ /**
155
+ * Backoff (ms) between CSRF-token fetch attempts. Short by design — a CSRF
156
+ * fetch sits in the critical path of a state-changing request, so the retry
157
+ * must add minimal latency.
158
+ */
159
+ const CSRF_FETCH_RETRY_DELAY_MS = 500;
160
+
161
+ /**
162
+ * Cooldown (ms) applied after a failed access-token refresh before another
163
+ * refresh is attempted while the CURRENT token is still valid (a proactive,
164
+ * near-expiry refresh). Prevents a refresh storm (and server hammering) when
165
+ * the auth refresh handler is failing — every in-flight request that
166
+ * hits a 401 would otherwise trigger its own refresh. A still-valid token can
167
+ * afford to wait this out; the request keeps carrying it in the meantime.
168
+ */
169
+ const TOKEN_REFRESH_COOLDOWN_MS = 15000;
170
+
171
+ /**
172
+ * Cooldown (ms) applied after a failed refresh when the CURRENT access token is
173
+ * already past its `exp`. Much shorter than {@link TOKEN_REFRESH_COOLDOWN_MS}:
174
+ * an expired token is UNUSABLE, so the client must re-mint as soon as the mint
175
+ * endpoint is reachable again (e.g. a few seconds after an ECS rolling-deploy
176
+ * blip drains/restarts a task) instead of waiting out the full proactive
177
+ * cooldown while every request forwards or omits a stale bearer → server 401.
178
+ *
179
+ * Still NON-ZERO on purpose: it bounds the request-driven retry rate to at most
180
+ * one attempt per this interval so a PROLONGED outage cannot become a tight
181
+ * network storm. Combined with the process-wide single-flight below (concurrent
182
+ * requests coalesce to one in-flight mint) and the refresh handler's own
183
+ * terminal-state handling (a genuinely revoked session clears its device
184
+ * credential and stops issuing network mints), this recovers a transient blip
185
+ * ~15× faster without weakening the storm guard.
186
+ */
187
+ const EXPIRED_TOKEN_REFRESH_COOLDOWN_MS = 1000;
188
+
189
+ /**
190
+ * Lead time (seconds) before access-token expiry at which a preflight refresh
191
+ * is triggered. A token within this window of `exp` is treated as effectively
192
+ * expired so the request carries a fresh bearer rather than racing the clock.
193
+ */
194
+ const TOKEN_REFRESH_LEAD_SECONDS = 60;
195
+
196
+ /**
197
+ * Soft ceiling on the number of live entries in the identity-scoped GET
198
+ * response cache. Crossing it does NOT evict anything (the {@link TTLCache}
199
+ * still expires by TTL and is swept on its cleanup interval) — it emits a
200
+ * single throttled telemetry warning via the logger so an unbounded-growth
201
+ * regression (e.g. an endpoint that mints a fresh identity tag per request, or
202
+ * a cache-key that accidentally folds in volatile data) is observable in the
203
+ * field instead of silently consuming memory. Tuned well above the working set
204
+ * a single authenticated user generates in normal use.
205
+ */
206
+ const CACHE_SOFT_MAX_ENTRIES = 500;
207
+
208
+ /**
209
+ * Minimum interval (ms) between successive cache-size telemetry warnings, so a
210
+ * cache that sits above the soft limit logs at most once per window rather than
211
+ * on every cached write.
212
+ */
213
+ const CACHE_SIZE_WARNING_THROTTLE_MS = 60000;
214
+
215
+ /**
216
+ * Token store for authentication (instance-based)
217
+ * Each HttpService gets its own TokenStore to prevent conflicts
218
+ * when multiple OxyServices instances coexist server-side.
219
+ */
220
+ class TokenStore {
221
+ private accessToken: string | null = null;
222
+ private csrfToken: string | null = null;
223
+ private csrfTokenFetchPromise: Promise<string | null> | null = null;
224
+
225
+ setTokens(accessToken: string): void {
226
+ this.accessToken = accessToken;
227
+ }
228
+
229
+ getAccessToken(): string | null {
230
+ return this.accessToken;
231
+ }
232
+
233
+ clearTokens(): void {
234
+ this.accessToken = null;
235
+ }
236
+
237
+ hasAccessToken(): boolean {
238
+ return !!this.accessToken;
239
+ }
240
+
241
+ setCsrfToken(token: string | null): void {
242
+ this.csrfToken = token;
243
+ }
244
+
245
+ getCsrfToken(): string | null {
246
+ return this.csrfToken;
247
+ }
248
+
249
+ setCsrfTokenFetchPromise(promise: Promise<string | null> | null): void {
250
+ this.csrfTokenFetchPromise = promise;
251
+ }
252
+
253
+ getCsrfTokenFetchPromise(): Promise<string | null> | null {
254
+ return this.csrfTokenFetchPromise;
255
+ }
256
+
257
+ clearCsrfToken(): void {
258
+ this.csrfToken = null;
259
+ this.csrfTokenFetchPromise = null;
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Unified HTTP Service
265
+ *
266
+ * Consolidates HttpClient + RequestManager into a single efficient class.
267
+ * Uses native fetch instead of axios for smaller bundle size.
268
+ */
269
+ export class HttpService {
270
+ private baseURL: string;
271
+ private tokenStore: TokenStore;
272
+ private cache: TTLCache<any>;
273
+ /**
274
+ * When true, the per-instance GET response cache is OFF: GET responses are
275
+ * never read from nor written to {@link cache}, so every request hits the
276
+ * network. Set from `config.enableCache === false` OR `config.cacheTTL <= 0`.
277
+ * A disabled instance does not register its cache for the global cleanup
278
+ * interval (nothing ever lands in it). Request deduplication is unaffected —
279
+ * concurrent identical in-flight requests still collapse into one.
280
+ */
281
+ private readonly cacheDisabled: boolean;
282
+ private deduplicator: RequestDeduplicator;
283
+ private requestQueue: RequestQueue;
284
+ private logger: SimpleLogger;
285
+ private config: OxyConfig;
286
+ private tokenRefreshPromise: Promise<string | null> | null = null;
287
+ /**
288
+ * Epoch ms of the last FAILED refresh (0 = none since the last success). The
289
+ * post-failure cooldown is measured from here; its length depends on whether
290
+ * the current token is still valid ({@link TOKEN_REFRESH_COOLDOWN_MS}) or
291
+ * already expired ({@link EXPIRED_TOKEN_REFRESH_COOLDOWN_MS}), so an expired
292
+ * token recovers promptly the instant it crosses `exp` — without storing a
293
+ * fixed deadline that could not shrink once the token expired mid-cooldown.
294
+ */
295
+ private lastRefreshFailureAt = 0;
296
+ private authRefreshHandler: AuthRefreshHandler | null = null;
297
+ private accessTokenProvider: AccessTokenProvider | null = null;
298
+ private deviceSecretMintInFlight: Promise<DeviceSecretMintOutcome> | null = null;
299
+
300
+ /**
301
+ * Epoch (ms) before which a cache-size telemetry warning must not be
302
+ * re-emitted. Throttles the {@link CACHE_SOFT_MAX_ENTRIES} warning to at most
303
+ * one per {@link CACHE_SIZE_WARNING_THROTTLE_MS} window.
304
+ */
305
+ private cacheSizeWarningSilentUntil = 0;
306
+
307
+ /**
308
+ * Fan-out listeners notified on EVERY access-token change on this instance:
309
+ * explicit `setTokens`, `clearTokens`, a refresh-handler rotation, and the
310
+ * internal 401-driven clear. This is a Set so multiple independent observers
311
+ * can mirror token state without clobbering each other.
312
+ *
313
+ * Each listener receives the resulting access token, or `null` when cleared.
314
+ */
315
+ private _tokenChangeListeners = new Set<(accessToken: string | null) => void>();
316
+
317
+ // Performance monitoring
318
+ private requestMetrics = {
319
+ totalRequests: 0,
320
+ successfulRequests: 0,
321
+ failedRequests: 0,
322
+ cacheHits: 0,
323
+ cacheMisses: 0,
324
+ averageResponseTime: 0,
325
+ };
326
+
327
+ constructor(config: OxyConfig) {
328
+ this.config = config;
329
+ this.baseURL = config.baseURL;
330
+ this.tokenStore = new TokenStore();
331
+
332
+ this.logger = new SimpleLogger(
333
+ config.enableLogging || false,
334
+ config.logLevel || 'error',
335
+ 'HttpService'
336
+ );
337
+
338
+ // Initialize performance infrastructure. The per-instance GET response
339
+ // cache is disabled when the consumer explicitly opts out
340
+ // (`enableCache: false`) or asks for a non-positive TTL (`cacheTTL <= 0`).
341
+ // When disabled, nothing is ever stored, so there is no reason to register
342
+ // the cache for the global cleanup interval. Default (config unset) keeps
343
+ // caching ON with the 5-minute TTL — unchanged for existing consumers.
344
+ this.cacheDisabled =
345
+ config.enableCache === false ||
346
+ (typeof config.cacheTTL === 'number' && config.cacheTTL <= 0);
347
+ this.cache = new TTLCache<any>(config.cacheTTL && config.cacheTTL > 0 ? config.cacheTTL : 5 * 60 * 1000);
348
+ if (!this.cacheDisabled) {
349
+ registerCacheForCleanup(this.cache);
350
+ }
351
+ this.deduplicator = new RequestDeduplicator();
352
+ this.requestQueue = new RequestQueue(
353
+ config.maxConcurrentRequests || 10,
354
+ config.requestQueueSize || 100
355
+ );
356
+ }
357
+
358
+ private syncAccessTokenFromProvider(): string | null {
359
+ if (!this.accessTokenProvider) {
360
+ return this.tokenStore.getAccessToken();
361
+ }
362
+
363
+ const providedToken = this.accessTokenProvider();
364
+ const currentToken = this.tokenStore.getAccessToken();
365
+
366
+ if (providedToken) {
367
+ if (providedToken !== currentToken) {
368
+ this.tokenStore.setTokens(providedToken);
369
+ this.notifyTokenChange();
370
+ }
371
+ return providedToken;
372
+ }
373
+
374
+ if (currentToken) {
375
+ this.clearTokens();
376
+ }
377
+
378
+ return null;
379
+ }
380
+
381
+ /**
382
+ * Robust FormData detection that works in browser, React Native, and
383
+ * Node.js polyfill environments.
384
+ *
385
+ * Why we don't use `instanceof FormData` alone:
386
+ * - React Native's FormData is a separate class, not the browser one —
387
+ * `instanceof FormData` is true only inside the JS runtime that
388
+ * instantiated the value (browser-side polyfills also have their own).
389
+ * - The Node.js `form-data` polyfill ships its own constructor.
390
+ *
391
+ * Why we explicitly reject `URLSearchParams`:
392
+ * - `URLSearchParams` ALSO exposes `append` / `get` / `has`, so the
393
+ * duck-type fallback below would have misidentified it as FormData and
394
+ * sent an empty multipart body.
395
+ * - It has its own encoding path instead — see {@link isUrlSearchParams}.
396
+ */
397
+ private isFormData(data: unknown): data is FormDataLike {
398
+ if (!data || typeof data !== 'object') {
399
+ return false;
400
+ }
401
+
402
+ // Reject URLSearchParams up front: it shares the duck-typed surface
403
+ // (append / get / has) but is a fundamentally different content type.
404
+ // The caller routes URLSearchParams through the regular body path.
405
+ if (typeof URLSearchParams !== 'undefined' && data instanceof URLSearchParams) {
406
+ return false;
407
+ }
408
+
409
+ // Primary check: instanceof FormData. Works whenever the value was
410
+ // constructed by the same runtime/realm that exposes `FormData`.
411
+ if (typeof FormData !== 'undefined' && data instanceof FormData) {
412
+ return true;
413
+ }
414
+
415
+ // Fallback: detect Node / RN polyfills by constructor name. Limited to
416
+ // the small handful of known names so we don't accept arbitrary
417
+ // user-supplied objects with a coincidental `name`.
418
+ const constructorName = data.constructor?.name;
419
+ if (constructorName === 'FormData' || constructorName === 'FormDataImpl') {
420
+ return true;
421
+ }
422
+
423
+ // Last-resort duck typing — require the full FormData write surface
424
+ // (`append`, `get`, `has`, `getAll`, `delete`) so plain objects with
425
+ // an `append` method don't accidentally match.
426
+ const candidate = data as Partial<Record<keyof FormDataLike, unknown>>;
427
+ return (
428
+ typeof candidate.append === 'function' &&
429
+ typeof candidate.get === 'function' &&
430
+ typeof candidate.has === 'function' &&
431
+ typeof candidate.getAll === 'function' &&
432
+ typeof candidate.delete === 'function'
433
+ );
434
+ }
435
+
436
+ /**
437
+ * True for an `application/x-www-form-urlencoded` payload.
438
+ *
439
+ * Needed because a handful of endpoints are defined by a standard that fixes
440
+ * their request encoding rather than by our own JSON conventions — today
441
+ * `POST /auth/oauth/token`, whose encoding RFC 6749 §4.1.3 mandates. Passing
442
+ * a `URLSearchParams` as `data` selects that encoding; everything else is
443
+ * still JSON.
444
+ */
445
+ private isUrlSearchParams(data: unknown): data is URLSearchParams {
446
+ return typeof URLSearchParams !== 'undefined' && data instanceof URLSearchParams;
447
+ }
448
+
449
+ /**
450
+ * Main request method - handles everything in one place
451
+ */
452
+ async request<T = unknown>(config: RequestConfig): Promise<T> {
453
+ const {
454
+ method,
455
+ url,
456
+ data,
457
+ params,
458
+ timeout = this.config.requestTimeout || DEFAULT_REQUEST_TIMEOUT_MS,
459
+ signal,
460
+ cache: cacheRequested = method === 'GET',
461
+ cacheTTL,
462
+ deduplicate = true,
463
+ retry = this.config.enableRetry !== false,
464
+ maxRetries = this.config.maxRetries || 3,
465
+ } = config;
466
+
467
+ // A per-instance disabled cache (`enableCache:false` / `cacheTTL<=0`)
468
+ // overrides any per-request `cache:true`: nothing is read from nor written
469
+ // to the response cache. Request deduplication below is unaffected.
470
+ const cache = cacheRequested && !this.cacheDisabled;
471
+
472
+ // Generate cache key (optimized for large objects)
473
+ const cacheKey = cache ? this.generateCacheKey(method, url, data || params) : null;
474
+
475
+ // Check cache first
476
+ if (cache && cacheKey) {
477
+ const cached = this.cache.get(cacheKey) as T | null;
478
+ if (cached !== null) {
479
+ this.requestMetrics.cacheHits++;
480
+ // Redact the query string: an asset stream URL passed here carries a
481
+ // scoped `mt=` media token that must never reach a log sink.
482
+ this.logger.debug('Cache hit:', redactUrlQuery(url));
483
+ return cached;
484
+ }
485
+ this.requestMetrics.cacheMisses++;
486
+ }
487
+
488
+ // Resolve the auth preflight OUTSIDE the queue slot. A slot represents
489
+ // NETWORK OCCUPANCY only. `getAuthHeader` may await the single-flight token
490
+ // refresh/mint — itself a request — so if a slot-holding request awaited it
491
+ // here and every slot were held by requests all waiting on that same mint,
492
+ // the mint could never acquire a slot to run (systemic deadlock). Resolving
493
+ // it before enqueue means auth-blocked requests hold NO slot while the shared
494
+ // mint runs. `skipAuth` requests (the body-authenticated mint) send NO bearer
495
+ // and skip the near-expiry preflight — see RequestOptions.skipAuth. The
496
+ // 401/CSRF retry re-enters request() with a fresh config, so it re-resolves
497
+ // these here with the refreshed token / cleared CSRF.
498
+ const isStateChangingMethod = ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method);
499
+ const authHeader = config.skipAuth ? null : await this.getAuthHeader();
500
+ // CSRF protects cookie-authenticated browser writes. Bearer-authenticated SDK
501
+ // clients are not vulnerable to ambient-cookie CSRF, and linked app APIs
502
+ // should not need to implement a duplicate `/csrf-token` route.
503
+ const csrfToken = isStateChangingMethod && !authHeader ? await this.fetchCsrfToken() : null;
504
+ const edgeRegionHeader = await getBrowserEdgeRegionHeader();
505
+ const activityIdHeader = getBrowserActivityIdHeader();
506
+
507
+ // Request function
508
+ const requestFn = async (): Promise<T> => {
509
+ const startTime = Date.now();
510
+ try {
511
+ // Build URL with params
512
+ const fullUrl = this.buildURL(url, params);
513
+
514
+ // Determine if data is FormData using robust detection
515
+ const isFormData = this.isFormData(data);
516
+ const isUrlEncoded = this.isUrlSearchParams(data);
517
+
518
+ // Make fetch request
519
+ const controller = new AbortController();
520
+ const timeoutId = timeout ? setTimeout(() => controller.abort(), timeout) : null;
521
+
522
+ if (signal) {
523
+ signal.addEventListener('abort', () => controller.abort());
524
+ }
525
+
526
+ // Build headers - start with defaults
527
+ const headers: Record<string, string> = {
528
+ 'Accept': 'application/json',
529
+ };
530
+
531
+ // Only set Content-Type for non-FormData requests (FormData sets it automatically with boundary)
532
+ if (isUrlEncoded) {
533
+ headers['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
534
+ } else if (!isFormData) {
535
+ headers['Content-Type'] = 'application/json';
536
+ }
537
+
538
+ // Add authorization header if available
539
+ if (authHeader) {
540
+ headers['Authorization'] = authHeader;
541
+ }
542
+
543
+ // Add CSRF token header for state-changing requests
544
+ if (csrfToken) {
545
+ headers['X-CSRF-Token'] = csrfToken;
546
+ }
547
+
548
+ // Add native app header for React Native (required for CSRF validation)
549
+ // Native apps can't persist cookies like browsers, so the server uses
550
+ // header-only CSRF validation when this header is present
551
+ if (isNativeApp && isStateChangingMethod) {
552
+ headers['X-Native-App'] = 'true';
553
+ }
554
+
555
+ // Debug logging for CSRF issues, routed through SimpleLogger so it only
556
+ // fires when consumers opt in via `enableLogging`.
557
+ if (isStateChangingMethod) {
558
+ this.logger.debug('CSRF Debug:', {
559
+ url,
560
+ method,
561
+ isNativeApp,
562
+ platformOS: getPlatformOS(),
563
+ hasCsrfToken: !!csrfToken,
564
+ csrfTokenLength: csrfToken?.length,
565
+ hasNativeAppHeader: headers['X-Native-App'] === 'true',
566
+ });
567
+ }
568
+
569
+ // Merge custom headers if provided
570
+ if (config.headers) {
571
+ Object.entries(config.headers).forEach(([key, value]) => {
572
+ // For FormData, explicitly remove Content-Type if user tries to set it
573
+ // The browser/fetch API will set it automatically with the boundary
574
+ if (isFormData && key.toLowerCase() === 'content-type') {
575
+ this.logger.debug('Ignoring Content-Type header for FormData - will be set automatically');
576
+ return;
577
+ }
578
+ headers[key] = value;
579
+ });
580
+ }
581
+
582
+ // This is Cloudflare's coarse serving PoP (for example `mad`), never an
583
+ // IP or coordinate. Set it after caller headers so request code cannot
584
+ // accidentally or deliberately substitute a different origin.
585
+ Object.assign(headers, edgeRegionHeader);
586
+ Object.assign(headers, activityIdHeader);
587
+
588
+ // `URLSearchParams` is serialised explicitly rather than handed to
589
+ // `fetch` as-is: RN's fetch does not consistently encode it, and doing
590
+ // it here keeps the body identical across every platform.
591
+ const bodyValue = method !== 'GET' && data
592
+ ? (isFormData ? data : isUrlEncoded ? data.toString() : JSON.stringify(data))
593
+ : undefined;
594
+
595
+ // React Native FormData workaround:
596
+ // Expo SDK 56's "winter fetch" rejects RN file descriptors `{uri, type, name}`
597
+ // in FormDataPart conversion (`Unsupported FormDataPart implementation`).
598
+ // RN's native XMLHttpRequest handles those descriptors correctly, so we
599
+ // route multipart uploads through XHR on RN only. JSON, text, etc. still
600
+ // use fetch on every platform.
601
+ const useXhrForUpload = isFormData && isReactNative() && typeof XMLHttpRequest !== 'undefined';
602
+
603
+ const response = useXhrForUpload
604
+ ? await this.uploadViaXHR(
605
+ fullUrl,
606
+ method,
607
+ headers,
608
+ bodyValue as FormData,
609
+ controller.signal,
610
+ timeout,
611
+ this.shouldSendCredentials(fullUrl),
612
+ )
613
+ : await fetch(fullUrl, {
614
+ method,
615
+ headers,
616
+ body: bodyValue as BodyInit | null | undefined,
617
+ signal: controller.signal,
618
+ credentials: this.getCredentialsMode(fullUrl),
619
+ });
620
+
621
+ if (timeoutId) clearTimeout(timeoutId);
622
+
623
+ // Handle response
624
+ if (!response.ok) {
625
+ // On 401, delegate to the installed auth refresh handler and retry
626
+ // once before giving up. HttpService deliberately does not know any
627
+ // session routes; the refresh handler owns session rotation.
628
+ if (response.status === 401 && !config._isAuthRetry && !config.skipAuth) {
629
+ const refreshed = await this.refreshAccessToken('response-401');
630
+ if (refreshed) {
631
+ // `deduplicate: false` is REQUIRED on the retry (mirrors the 403
632
+ // CSRF retry below). This re-issue runs while the ORIGINAL request
633
+ // is still in-flight under its dedupe key; the refreshed token is
634
+ // for the SAME user, so the identity-scoped key is UNCHANGED — a
635
+ // deduplicated retry would resolve to the still-pending original
636
+ // and await itself (deadlock). Opting the retry out of dedupe makes
637
+ // it a fresh request.
638
+ return this.request<T>({ ...config, _isAuthRetry: true, retry: false, deduplicate: false });
639
+ }
640
+ // Refresh failed or no token — clear tokens and stale CSRF
641
+ this.tokenStore.clearTokens();
642
+ this.tokenStore.clearCsrfToken();
643
+ this.notifyTokenChange();
644
+ }
645
+
646
+ // On 403 with CSRF error, clear cached token and retry once
647
+ if (response.status === 403 && !config._isCsrfRetry) {
648
+ try {
649
+ const clonedResponse = response.clone();
650
+ const errBody = await clonedResponse.json() as { code?: string } | null;
651
+ if (errBody?.code === 'CSRF_TOKEN_INVALID' || errBody?.code === 'CSRF_TOKEN_MISSING') {
652
+ this.tokenStore.clearCsrfToken();
653
+ return this.request<T>({ ...config, _isCsrfRetry: true, retry: false, deduplicate: false });
654
+ }
655
+ } catch {
656
+ // Failed to parse error body — not a CSRF error
657
+ }
658
+ }
659
+
660
+ // Read the error body (may be absent, non-JSON, empty or malformed).
661
+ // Anything unreadable leaves `errorBody` undefined and degrades to the
662
+ // status-based message — an error path that throws its own error is
663
+ // worse than the error it was reporting.
664
+ let errorBody: unknown;
665
+ const errorContentType = response.headers.get('content-type');
666
+ if (errorContentType?.includes('application/json')) {
667
+ try {
668
+ errorBody = await response.json();
669
+ } catch (parseError) {
670
+ // Malformed JSON or empty response - use status text
671
+ this.logger.warn('Failed to parse error response JSON:', parseError);
672
+ }
673
+ }
674
+
675
+ // `parseHttpErrorBody` handles every envelope in use, including the
676
+ // nested `{ error: { code, message } }` shape — assigning that nested
677
+ // OBJECT as the message is what produced `"[object Object]"`.
678
+ const parsed = parseHttpErrorBody(errorBody);
679
+ const error = new Error(
680
+ parsed.message ?? `HTTP ${response.status}: ${response.statusText}`,
681
+ ) as Error & {
682
+ status?: number;
683
+ code?: string;
684
+ details?: Record<string, unknown>;
685
+ response?: { status: number; statusText: string; data?: unknown };
686
+ };
687
+ error.status = response.status;
688
+ error.response = { status: response.status, statusText: response.statusText, data: errorBody };
689
+ // Only set `code`/`details` when the server actually sent them.
690
+ // Assigning `undefined` would still create the property, which changes
691
+ // how `handleHttpError` classifies the error downstream.
692
+ if (parsed.code !== undefined) {
693
+ error.code = parsed.code;
694
+ }
695
+ if (parsed.details !== undefined) {
696
+ error.details = parsed.details;
697
+ }
698
+ throw error;
699
+ }
700
+
701
+ // Handle different response types (optimized - read response once)
702
+ const contentType = response.headers.get('content-type');
703
+ let responseData: unknown;
704
+
705
+ if (config.responseType === 'blob') {
706
+ responseData = await response.blob();
707
+ } else if (contentType && contentType.includes('application/json')) {
708
+ // Use response.json() directly for better performance
709
+ try {
710
+ responseData = await response.json();
711
+ // Handle null/undefined responses
712
+ if (responseData === null || responseData === undefined) {
713
+ responseData = null;
714
+ } else {
715
+ // Unwrap standardized API response format for JSON
716
+ responseData = this.unwrapResponse(responseData);
717
+ }
718
+ } catch (parseError) {
719
+ // Handle malformed JSON or empty responses gracefully
720
+ // Note: Once response.json() is called, the body is consumed and cannot be read again
721
+ // So we check the error type to determine if it's empty or malformed
722
+ if (parseError instanceof SyntaxError) {
723
+ this.logger.warn('Failed to parse JSON response (malformed or empty):', parseError);
724
+ // SyntaxError typically means empty or malformed JSON
725
+ // For empty responses, return null; for malformed JSON, throw descriptive error
726
+ responseData = null; // Treat as empty response for safety
727
+ } else {
728
+ this.logger.warn('Failed to read response:', parseError);
729
+ throw new Error('Failed to read response from server');
730
+ }
731
+ }
732
+ } else if (contentType && (contentType.includes('application/octet-stream') || contentType.includes('image/') || contentType.includes('video/') || contentType.includes('audio/'))) {
733
+ // For binary responses (blobs), return the blob directly without unwrapping
734
+ responseData = await response.blob();
735
+ } else {
736
+ // For other responses, return as text
737
+ const text = await response.text();
738
+ responseData = text || null;
739
+ }
740
+
741
+ const duration = Date.now() - startTime;
742
+ this.updateMetrics(true, duration);
743
+ this.config.onRequestEnd?.(url, method, duration, true);
744
+
745
+ return responseData as T;
746
+ } catch (error: unknown) {
747
+ const duration = Date.now() - startTime;
748
+ this.updateMetrics(false, duration);
749
+ this.config.onRequestEnd?.(url, method, duration, false);
750
+ this.config.onRequestError?.(url, method, error instanceof Error ? error : new Error(String(error)));
751
+
752
+ // Handle AbortError specifically for better error messages
753
+ if (error instanceof Error && error.name === 'AbortError') {
754
+ throw handleHttpError(error);
755
+ }
756
+
757
+ throw handleHttpError(error);
758
+ }
759
+ };
760
+
761
+ // Wrap with retry if enabled
762
+ const requestWithRetry = retry
763
+ ? () => retryAsync(requestFn, maxRetries, this.config.retryDelay || 1000)
764
+ : requestFn;
765
+
766
+ // Wrap with deduplication if enabled (use optimized key generation)
767
+ const dedupeKey = deduplicate ? this.generateCacheKey(method, url, data || params) : null;
768
+ const finalRequest = dedupeKey
769
+ ? () => this.deduplicator.deduplicate(dedupeKey, requestWithRetry)
770
+ : requestWithRetry;
771
+
772
+ // Execute the request. Control-plane calls the auth lane depends on
773
+ // (`bypassQueue`, e.g. the device-secret mint) run DIRECTLY — a queued mint
774
+ // could never acquire a slot when every slot is parked awaiting it.
775
+ const result = config.bypassQueue
776
+ ? await finalRequest()
777
+ : await this.requestQueue.enqueue(finalRequest);
778
+
779
+ // Cache the result if caching is enabled
780
+ if (cache && cacheKey && result) {
781
+ this.cache.set(cacheKey, result, cacheTTL);
782
+ this.warnIfCacheOversized();
783
+ }
784
+
785
+ return result;
786
+ }
787
+
788
+ /**
789
+ * Soft cache-size guard. Emits a single throttled telemetry warning when the
790
+ * identity-scoped response cache grows past {@link CACHE_SOFT_MAX_ENTRIES}.
791
+ *
792
+ * This intentionally does NOT evict: the {@link TTLCache} already bounds
793
+ * memory by TTL (and the global cleanup interval sweeps expired entries), so
794
+ * an LRU here would only risk thrashing a legitimately warm cache. The point
795
+ * is observability — if entry count climbs and stays high, an identity tag or
796
+ * cache key is folding in volatile data (a per-request nonce, an undecodable
797
+ * rotating token) and the cache is no longer doing its job. Surfacing that via
798
+ * the logger lets a consumer with `enableLogging` catch the regression in the
799
+ * field instead of debugging silent memory growth.
800
+ */
801
+ private warnIfCacheOversized(): void {
802
+ const size = this.cache.size();
803
+ if (size <= CACHE_SOFT_MAX_ENTRIES) {
804
+ return;
805
+ }
806
+ const now = Date.now();
807
+ if (now < this.cacheSizeWarningSilentUntil) {
808
+ return;
809
+ }
810
+ this.cacheSizeWarningSilentUntil = now + CACHE_SIZE_WARNING_THROTTLE_MS;
811
+ this.logger.warn(
812
+ 'Response cache exceeded soft entry limit — possible identity-tag or cache-key bloat',
813
+ { size, softLimit: CACHE_SOFT_MAX_ENTRIES },
814
+ );
815
+ }
816
+
817
+ /**
818
+ * Upload via XMLHttpRequest (React Native FormData workaround).
819
+ *
820
+ * Expo SDK 56's "winter fetch" cannot serialize RN file descriptors
821
+ * (`{uri, type, name}`) — `convertFormDataAsync` rejects them as
822
+ * `Unsupported FormDataPart implementation`. RN's native XHR streams
823
+ * the file from disk correctly, so multipart uploads go through XHR
824
+ * on RN only.
825
+ *
826
+ * Returns a standard `Response` so downstream parsing in `request()`
827
+ * (status checks, 401/403 retries, JSON/blob/text parsing) is identical
828
+ * to the fetch path.
829
+ */
830
+ private uploadViaXHR(
831
+ url: string,
832
+ method: string,
833
+ headers: Record<string, string>,
834
+ body: FormData,
835
+ abortSignal: AbortSignal,
836
+ timeout: number,
837
+ withCredentials: boolean,
838
+ ): Promise<Response> {
839
+ return new Promise<Response>((resolve, reject) => {
840
+ const xhr = new XMLHttpRequest();
841
+ xhr.open(method, url, true);
842
+ // Only send ambient cookies to the configured API origin. Absolute
843
+ // caller-supplied URLs can target arbitrary origins, so they must not
844
+ // receive credential-bearing requests by default.
845
+ xhr.withCredentials = withCredentials;
846
+
847
+ // Forward headers but skip Content-Type — XHR sets the multipart
848
+ // boundary automatically and overriding it breaks the upload.
849
+ for (const [key, value] of Object.entries(headers)) {
850
+ if (key.toLowerCase() === 'content-type') continue;
851
+ try {
852
+ xhr.setRequestHeader(key, value);
853
+ } catch (headerError) {
854
+ // Some headers (e.g. forbidden header names) cannot be set —
855
+ // log and continue rather than failing the whole upload.
856
+ this.logger.warn('XHR setRequestHeader failed:', key, headerError);
857
+ }
858
+ }
859
+
860
+ xhr.responseType = 'text';
861
+ if (timeout > 0) {
862
+ xhr.timeout = timeout;
863
+ }
864
+
865
+ const onAbort = (): void => {
866
+ try { xhr.abort(); } catch { /* xhr already finished */ }
867
+ };
868
+ if (abortSignal.aborted) {
869
+ reject(new DOMException('The user aborted a request.', 'AbortError'));
870
+ return;
871
+ }
872
+ abortSignal.addEventListener('abort', onAbort);
873
+
874
+ const cleanup = (): void => {
875
+ abortSignal.removeEventListener('abort', onAbort);
876
+ };
877
+
878
+ xhr.onload = (): void => {
879
+ cleanup();
880
+ const responseHeaders = HttpService.parseXHRHeaders(xhr.getAllResponseHeaders());
881
+ resolve(new Response(xhr.responseText, {
882
+ status: xhr.status,
883
+ statusText: xhr.statusText,
884
+ headers: responseHeaders,
885
+ }));
886
+ };
887
+ xhr.onerror = (): void => {
888
+ cleanup();
889
+ reject(new TypeError('Network request failed'));
890
+ };
891
+ xhr.ontimeout = (): void => {
892
+ cleanup();
893
+ reject(new DOMException('The request timed out.', 'TimeoutError'));
894
+ };
895
+ xhr.onabort = (): void => {
896
+ cleanup();
897
+ reject(new DOMException('The user aborted a request.', 'AbortError'));
898
+ };
899
+
900
+ xhr.send(body);
901
+ });
902
+ }
903
+
904
+ /**
905
+ * Parse raw header string from `XMLHttpRequest.getAllResponseHeaders()`
906
+ * into a `Headers`-compatible object.
907
+ */
908
+ private static parseXHRHeaders(rawHeaders: string): Headers {
909
+ const headers = new Headers();
910
+ if (!rawHeaders) return headers;
911
+ // RFC 7230 line terminator is CRLF; some XHR implementations use LF only.
912
+ const lines = rawHeaders.trim().split(/\r?\n/);
913
+ for (const line of lines) {
914
+ const colonIndex = line.indexOf(':');
915
+ if (colonIndex <= 0) continue;
916
+ const key = line.slice(0, colonIndex).trim();
917
+ const value = line.slice(colonIndex + 1).trim();
918
+ if (key) {
919
+ try {
920
+ headers.append(key, value);
921
+ } catch {
922
+ // Invalid header name/value — skip.
923
+ }
924
+ }
925
+ }
926
+ return headers;
927
+ }
928
+
929
+ /**
930
+ * Delimiter that separates the logical `method:url[:data]` portion of a
931
+ * cache key from its identity suffix. Always APPENDED, never used to parse
932
+ * a key apart, so the `method:url` prefix stays intact for
933
+ * `clearCacheByPrefix` sweeps and `clearCacheEntry` base-key matching.
934
+ * The `clearCacheEntry` callsites all pass fixed, dataless logical keys
935
+ * (`GET:/users/<id>`, `GET:/session/user/<sessionId>`,
936
+ * `GET:/auth/grants`), so this readable suffix can never be
937
+ * ambiguous with a serialized request body.
938
+ */
939
+ private static readonly CACHE_IDENTITY_DELIM = ' id=';
940
+
941
+ /**
942
+ * The keys whose presence beside `data` makes a body a PAGE rather than a
943
+ * payload — see {@link unwrapResponse} for why this list is narrow.
944
+ *
945
+ * - `pagination` — the offset-paginated house envelope (`sendPaginated`).
946
+ * - `nextCursor` — the keyset-paginated one (the account audit trails).
947
+ *
948
+ * Membership is decided by key PRESENCE, never by value: the last page sends
949
+ * `nextCursor: null`, and an envelope that collapsed into a bare payload
950
+ * exactly when the stream ended would be a worse bug than the one this fixes.
951
+ */
952
+ private static readonly PAGE_ENVELOPE_KEYS: readonly string[] = ['pagination', 'nextCursor'];
953
+
954
+ /**
955
+ * Derive a stable, non-sensitive identity discriminator for cache scoping.
956
+ *
957
+ * Thin instance wrapper over the pure {@link computeIdentityTag} helper —
958
+ * binds it to this instance's live access token. See that function's docs for
959
+ * the full resolution contract (anon fallback, decoded `userId || id`,
960
+ * token-hash fallback for undecodable tokens).
961
+ */
962
+ private computeIdentityTag(): string {
963
+ return computeIdentityTag(this.tokenStore.getAccessToken());
964
+ }
965
+
966
+ /**
967
+ * Generate cache key efficiently
968
+ * Uses a content-addressed hash for large payloads so two requests with
969
+ * the same shape but different values never collide on the same key
970
+ * (which would silently serve stale data — e.g. paginated search results,
971
+ * large object updates).
972
+ *
973
+ * The key is identity-scoped: the logical `method:url[:data]` portion is
974
+ * suffixed with ` id=<identityTag>` so two callers with different
975
+ * identities (anon vs authed, or two different users) never share an entry.
976
+ * The identity tag is placed at the END so the key still STARTS with
977
+ * `method:url`, preserving the prefix-based invalidation in
978
+ * `clearCacheByPrefix` (e.g. `GET:/session/user/`) and the base-key matching
979
+ * in `clearCacheEntry`.
980
+ */
981
+ private generateCacheKey(method: string, url: string, data?: unknown): string {
982
+ return `${this.generateBaseCacheKey(method, url, data)}${HttpService.CACHE_IDENTITY_DELIM}${this.computeIdentityTag()}`;
983
+ }
984
+
985
+ /**
986
+ * Build the identity-agnostic portion of a cache key (`method:url[:data]`).
987
+ * Kept separate so identity scoping is applied in exactly one place
988
+ * (`generateCacheKey`) and cannot drift between the cache and dedupe paths.
989
+ */
990
+ private generateBaseCacheKey(method: string, url: string, data?: unknown): string {
991
+ if (!data || (typeof data === 'object' && Object.keys(data).length === 0)) {
992
+ return `${method}:${url}`;
993
+ }
994
+
995
+ // For small objects, the full serialization IS the key — fastest and
996
+ // guaranteed to be content-addressed.
997
+ const dataStr = JSON.stringify(data);
998
+ if (dataStr.length < 1000) {
999
+ return `${method}:${url}:${dataStr}`;
1000
+ }
1001
+
1002
+ // For large payloads, hash the full serialized string so the key remains
1003
+ // content-addressed (any byte change yields a different hash). Previous
1004
+ // implementation hashed `keys + length` which collided for any two
1005
+ // payloads with the same top-level keys and serialized length.
1006
+ return `${method}:${url}:${fnv1a32(dataStr)}`;
1007
+ }
1008
+
1009
+ /**
1010
+ * Build full URL with query params
1011
+ */
1012
+ private buildURL(url: string, params?: Record<string, unknown>): string {
1013
+ const base = /^https?:\/\//i.test(url)
1014
+ ? url
1015
+ : `${this.baseURL.replace(/\/+$/, '')}/${url.replace(/^\/+/, '')}`;
1016
+
1017
+ if (!params || Object.keys(params).length === 0) {
1018
+ return base;
1019
+ }
1020
+
1021
+ const searchParams = new URLSearchParams();
1022
+ Object.entries(params).forEach(([key, value]) => {
1023
+ if (value !== undefined && value !== null) {
1024
+ searchParams.append(key, String(value));
1025
+ }
1026
+ });
1027
+
1028
+ const queryString = searchParams.toString();
1029
+ return queryString ? `${base}${base.includes('?') ? '&' : '?'}${queryString}` : base;
1030
+ }
1031
+
1032
+ private getCredentialsMode(url: string): RequestCredentials {
1033
+ return this.shouldSendCredentials(url) ? 'include' : 'omit';
1034
+ }
1035
+
1036
+ private shouldSendCredentials(url: string): boolean {
1037
+ try {
1038
+ return new URL(url).origin === new URL(this.baseURL).origin;
1039
+ } catch {
1040
+ return false;
1041
+ }
1042
+ }
1043
+
1044
+ /**
1045
+ * Fetch CSRF token from server (with deduplication)
1046
+ * Required for state-changing requests (POST, PUT, PATCH, DELETE)
1047
+ */
1048
+ private async fetchCsrfToken(): Promise<string | null> {
1049
+ // Return cached token if available
1050
+ const cachedToken = this.tokenStore.getCsrfToken();
1051
+ if (cachedToken) {
1052
+ this.logger.debug('Using cached CSRF token');
1053
+ return cachedToken;
1054
+ }
1055
+
1056
+ // Deduplicate concurrent CSRF token fetches
1057
+ const existingPromise = this.tokenStore.getCsrfTokenFetchPromise();
1058
+ if (existingPromise) {
1059
+ this.logger.debug('Waiting for existing CSRF fetch');
1060
+ return existingPromise;
1061
+ }
1062
+
1063
+ const fetchPromise = (async () => {
1064
+ for (let attempt = 1; attempt <= CSRF_FETCH_MAX_ATTEMPTS; attempt++) {
1065
+ try {
1066
+ this.logger.debug('Fetching CSRF token from:', `${this.baseURL}/csrf-token`, `(attempt ${attempt})`);
1067
+
1068
+ // Use AbortController for timeout (more compatible than AbortSignal.timeout)
1069
+ const controller = new AbortController();
1070
+ const timeoutId = setTimeout(() => controller.abort(), CSRF_FETCH_TIMEOUT_MS);
1071
+
1072
+ const response = await fetch(`${this.baseURL}/csrf-token`, {
1073
+ method: 'GET',
1074
+ headers: { 'Accept': 'application/json' },
1075
+ credentials: 'include', // Required to receive and send cookies
1076
+ signal: controller.signal,
1077
+ });
1078
+
1079
+ clearTimeout(timeoutId);
1080
+
1081
+ this.logger.debug('CSRF fetch response:', response.status, response.ok);
1082
+
1083
+ if (response.ok) {
1084
+ const data = await response.json() as { csrfToken?: string };
1085
+ const token = data.csrfToken || null;
1086
+ this.logger.debug('CSRF response data:', {
1087
+ hasCsrfToken: typeof token === 'string' && token.length > 0,
1088
+ csrfTokenLength: token?.length,
1089
+ });
1090
+ this.tokenStore.setCsrfToken(token);
1091
+ this.logger.debug('CSRF token fetched');
1092
+ return token;
1093
+ }
1094
+
1095
+ // Also check response header for CSRF token
1096
+ const headerToken = response.headers.get('X-CSRF-Token');
1097
+ if (headerToken) {
1098
+ this.tokenStore.setCsrfToken(headerToken);
1099
+ this.logger.debug('CSRF token from header');
1100
+ return headerToken;
1101
+ }
1102
+
1103
+ this.logger.debug('CSRF fetch failed with status:', response.status);
1104
+ this.logger.warn('Failed to fetch CSRF token:', response.status);
1105
+ } catch (error) {
1106
+ this.logger.debug('CSRF fetch error:', error);
1107
+ this.logger.warn('CSRF token fetch error:', error);
1108
+ }
1109
+ // Brief backoff before the next attempt.
1110
+ if (attempt < CSRF_FETCH_MAX_ATTEMPTS) {
1111
+ await new Promise(resolve => setTimeout(resolve, CSRF_FETCH_RETRY_DELAY_MS));
1112
+ }
1113
+ }
1114
+ return null;
1115
+ })().finally(() => {
1116
+ this.tokenStore.setCsrfTokenFetchPromise(null);
1117
+ });
1118
+
1119
+ this.tokenStore.setCsrfTokenFetchPromise(fetchPromise);
1120
+ return fetchPromise;
1121
+ }
1122
+
1123
+ /**
1124
+ * Get auth header with automatic token refresh
1125
+ */
1126
+ private async getAuthHeader(): Promise<string | null> {
1127
+ const accessToken = this.syncAccessTokenFromProvider();
1128
+ if (!accessToken) {
1129
+ return null;
1130
+ }
1131
+
1132
+ try {
1133
+ const decoded = jwtDecode<JwtPayload>(accessToken);
1134
+ const currentTime = Math.floor(Date.now() / 1000);
1135
+
1136
+ // If the token expires within the refresh lead window, refresh it.
1137
+ if (decoded.exp && decoded.exp - currentTime < TOKEN_REFRESH_LEAD_SECONDS) {
1138
+ const refreshed = await this.refreshAccessToken('preflight');
1139
+ if (refreshed) return `Bearer ${refreshed}`;
1140
+ if (decoded.exp > currentTime) {
1141
+ return `Bearer ${accessToken}`;
1142
+ }
1143
+ // Refresh failed — don't use an expired token (would cause 401 loop)
1144
+ return null;
1145
+ }
1146
+
1147
+ return `Bearer ${accessToken}`;
1148
+ } catch (error) {
1149
+ this.logger.error('Error processing token:', error);
1150
+ return null;
1151
+ }
1152
+ }
1153
+
1154
+ async refreshAccessToken(reason: AuthRefreshReason): Promise<string | null> {
1155
+ if (!this.authRefreshHandler) {
1156
+ return null;
1157
+ }
1158
+
1159
+ // Post-failure cooldown. A genuinely EXPIRED current token uses a much
1160
+ // shorter cooldown than a still-valid (proactive, near-expiry) one: an
1161
+ // expired token is unusable, so re-mint as soon as the endpoint is reachable
1162
+ // again rather than waiting out the full window while requests carry a stale
1163
+ // bearer. Both cooldowns are measured from the last failure, so the moment a
1164
+ // still-valid token crosses `exp` mid-cooldown the shorter window applies.
1165
+ const cooldownMs = this.isAccessTokenExpired()
1166
+ ? EXPIRED_TOKEN_REFRESH_COOLDOWN_MS
1167
+ : TOKEN_REFRESH_COOLDOWN_MS;
1168
+ if (Date.now() - this.lastRefreshFailureAt < cooldownMs) {
1169
+ return null;
1170
+ }
1171
+
1172
+ if (!this.tokenRefreshPromise) {
1173
+ this.tokenRefreshPromise = this.authRefreshHandler(reason)
1174
+ .then((newToken) => {
1175
+ if (!newToken) {
1176
+ this.lastRefreshFailureAt = Date.now();
1177
+ return null;
1178
+ }
1179
+ if (this.tokenStore.getAccessToken() !== newToken) {
1180
+ this.tokenStore.setTokens(newToken);
1181
+ this.notifyTokenChange();
1182
+ }
1183
+ // A success clears the failure timestamp so the next refresh is never
1184
+ // throttled by a stale cooldown.
1185
+ this.lastRefreshFailureAt = 0;
1186
+ this.logger.debug('Token refreshed via the auth refresh handler');
1187
+ return newToken;
1188
+ })
1189
+ .catch((error) => {
1190
+ this.logger.warn('Token refresh failed:', error);
1191
+ this.lastRefreshFailureAt = Date.now();
1192
+ return null;
1193
+ })
1194
+ .finally(() => {
1195
+ this.tokenRefreshPromise = null;
1196
+ });
1197
+ }
1198
+
1199
+ return this.tokenRefreshPromise;
1200
+ }
1201
+
1202
+ /**
1203
+ * Whether the CURRENT stored access token is already past its `exp`. Drives
1204
+ * the shorter post-failure refresh cooldown ({@link EXPIRED_TOKEN_REFRESH_COOLDOWN_MS}):
1205
+ * a still-valid (near-expiry) token can wait out the full cooldown, but an
1206
+ * expired one must re-mint promptly. Returns `false` for an absent or
1207
+ * opaque/no-`exp` token — no proof it is expired, so keep the conservative
1208
+ * (longer) cooldown and avoid an unnecessary retry loop.
1209
+ */
1210
+ private isAccessTokenExpired(): boolean {
1211
+ const token = this.tokenStore.getAccessToken();
1212
+ if (!token) {
1213
+ return false;
1214
+ }
1215
+ try {
1216
+ const decoded = jwtDecode<JwtPayload>(token);
1217
+ return typeof decoded.exp === 'number' && decoded.exp <= Math.floor(Date.now() / 1000);
1218
+ } catch {
1219
+ return false;
1220
+ }
1221
+ }
1222
+
1223
+ /**
1224
+ * PROCESS-WIDE single-flight for the rotating device-secret mint
1225
+ * (`POST /session/device/token`).
1226
+ *
1227
+ * The server rotates the presented `deviceSecret` on every successful mint, so
1228
+ * two concurrent mints would double-rotate and the durable store could end up
1229
+ * holding a superseded secret → a later cold-boot mint 401s → the user is
1230
+ * signed out. Every mint lane (the re-mint handler behind `refreshAccessToken`,
1231
+ * the device-first cold boot, the socket token transport, the tab-focus
1232
+ * reconcile) funnels its `refreshDeviceSecretArm` call through here, so
1233
+ * concurrent callers await the SAME in-flight mint and all receive its result —
1234
+ * exactly one server rotation.
1235
+ *
1236
+ * Distinct from {@link tokenRefreshPromise} (which dedups the FULL re-mint
1237
+ * handler incl. the native shared-key arm + the failure cooldown): this inner
1238
+ * guard serializes the rotation itself across BOTH the handler and the
1239
+ * handler-independent cold boot, which never runs through `refreshAccessToken`.
1240
+ */
1241
+ runSingleFlightDeviceSecretMint(
1242
+ mint: () => Promise<DeviceSecretMintOutcome>,
1243
+ ): Promise<DeviceSecretMintOutcome> {
1244
+ if (!this.deviceSecretMintInFlight) {
1245
+ this.deviceSecretMintInFlight = mint().finally(() => {
1246
+ this.deviceSecretMintInFlight = null;
1247
+ });
1248
+ }
1249
+ return this.deviceSecretMintInFlight;
1250
+ }
1251
+
1252
+ /**
1253
+ * Unwrap the standardized API response envelope — EXCEPT when the envelope is
1254
+ * a page, in which case it travels whole.
1255
+ *
1256
+ * `{ data: <payload> }` is the house success envelope (`sendSuccess`), and
1257
+ * reducing it to `<payload>` is what every call site in the SDK expects. But
1258
+ * the reduction DISCARDS every sibling key, silently, and a page's siblings
1259
+ * are the only thing that says where the next page starts. That is how
1260
+ * `GET /accounts/:id/audit` lost its `nextCursor`: the caller received a bare
1261
+ * array, `getNextPageParam` read `undefined`, and pagination was dead past the
1262
+ * first page with nothing to show that it was.
1263
+ *
1264
+ * ## Why the rule is narrow, and not "any sibling key survives"
1265
+ *
1266
+ * "An object carrying `data` plus anything else is not an envelope" is the
1267
+ * tempting general rule, and it is wrong here: this API already answers
1268
+ * `{ data, count }` on ~15 routes, plus `{ data, source }`, `{ data, reason }`
1269
+ * and `{ data, credentialRevoked }`, and a dozen measured Console call sites
1270
+ * type those as the bare payload (`Array<ProviderConnection>`,
1271
+ * `AccountBillingState | null`, …). Preserving those envelopes would hand every
1272
+ * one of them an object where it expects its payload — at runtime only, since
1273
+ * the response type is a call-site assertion. So the rule names PAGINATION
1274
+ * specifically: `data` beside {@link PAGE_ENVELOPE_KEYS} is a page.
1275
+ *
1276
+ * A route whose sibling key genuinely matters to its caller belongs in that
1277
+ * list, or should not be a sibling of `data` at all — the cursor-paginated
1278
+ * surfaces already in the SDK (`{ follows, nextCursor }`,
1279
+ * `{ records, nextCursor }`) sidestep this by never using `data`.
1280
+ */
1281
+ private unwrapResponse(responseData: unknown): unknown {
1282
+ if (!responseData || typeof responseData !== 'object' || !('data' in responseData)) {
1283
+ // Not the success envelope (or not an object at all) — as-is.
1284
+ return responseData;
1285
+ }
1286
+
1287
+ // A page travels whole: its cursor/pagination sibling is unrecoverable
1288
+ // information, not decoration.
1289
+ if (HttpService.PAGE_ENVELOPE_KEYS.some((key) => key in responseData)) {
1290
+ return responseData;
1291
+ }
1292
+
1293
+ // Regular success envelope: `{ data: ... }` -> the payload.
1294
+ return Array.isArray(responseData) ? responseData : responseData.data;
1295
+ }
1296
+
1297
+ /**
1298
+ * Update request metrics
1299
+ */
1300
+ private updateMetrics(success: boolean, duration: number): void {
1301
+ this.requestMetrics.totalRequests++;
1302
+ if (success) {
1303
+ this.requestMetrics.successfulRequests++;
1304
+ } else {
1305
+ this.requestMetrics.failedRequests++;
1306
+ }
1307
+
1308
+ const alpha = 0.1;
1309
+ this.requestMetrics.averageResponseTime =
1310
+ this.requestMetrics.averageResponseTime * (1 - alpha) + duration * alpha;
1311
+ }
1312
+
1313
+ /**
1314
+ * Send an authenticated request without consuming its response body.
1315
+ *
1316
+ * Regular {@link request} calls parse the body before returning. Streaming
1317
+ * consumers instead need the original `Response`, while retaining the same
1318
+ * token ownership and one-time 401 refresh behaviour as every SDK request.
1319
+ * No SDK timeout is installed after the connection opens: the caller-owned
1320
+ * abort signal defines the lifetime of a healthy stream.
1321
+ */
1322
+ async requestAuthenticatedResponse(config: AuthenticatedResponseRequest): Promise<Response> {
1323
+ const authHeader = await this.getAuthHeader();
1324
+ if (!authHeader) {
1325
+ throw new OxyAuthenticationError(
1326
+ 'An active Oxy session is required for this request',
1327
+ 'AUTH_REQUIRED',
1328
+ );
1329
+ }
1330
+
1331
+ return this.requestAuthenticatedResponseAttempt(config, authHeader, false);
1332
+ }
1333
+
1334
+ private async requestAuthenticatedResponseAttempt(
1335
+ config: AuthenticatedResponseRequest,
1336
+ authHeader: string,
1337
+ isAuthRetry: boolean,
1338
+ ): Promise<Response> {
1339
+ const startTime = Date.now();
1340
+ const headers = new Headers(config.headers);
1341
+ if (!headers.has('Accept')) {
1342
+ headers.set('Accept', 'application/json');
1343
+ }
1344
+ // Authentication is owned by this SDK instance. A caller cannot replace
1345
+ // the bearer with a different session or leak one across linked apps.
1346
+ headers.set('Authorization', authHeader);
1347
+ const edgeRegionHeader = await getBrowserEdgeRegionHeader();
1348
+ for (const [name, value] of Object.entries(edgeRegionHeader)) headers.set(name, value);
1349
+ const activityIdHeader = getBrowserActivityIdHeader();
1350
+ for (const [name, value] of Object.entries(activityIdHeader)) headers.set(name, value);
1351
+
1352
+ try {
1353
+ const fullUrl = this.buildURL(config.url);
1354
+ const response = await fetch(fullUrl, {
1355
+ method: config.method,
1356
+ headers,
1357
+ body: config.method === 'GET' ? undefined : config.body,
1358
+ signal: config.signal,
1359
+ credentials: this.getCredentialsMode(fullUrl),
1360
+ });
1361
+
1362
+ if (response.status === 401 && !isAuthRetry) {
1363
+ const refreshed = await this.refreshAccessToken('response-401');
1364
+ if (refreshed) {
1365
+ await response.body?.cancel();
1366
+ return this.requestAuthenticatedResponseAttempt(config, `Bearer ${refreshed}`, true);
1367
+ }
1368
+
1369
+ this.tokenStore.clearTokens();
1370
+ this.tokenStore.clearCsrfToken();
1371
+ this.notifyTokenChange();
1372
+ }
1373
+
1374
+ const duration = Date.now() - startTime;
1375
+ this.updateMetrics(response.ok, duration);
1376
+ this.config.onRequestEnd?.(config.url, config.method, duration, response.ok);
1377
+ return response;
1378
+ } catch (error: unknown) {
1379
+ const duration = Date.now() - startTime;
1380
+ this.updateMetrics(false, duration);
1381
+ this.config.onRequestEnd?.(config.url, config.method, duration, false);
1382
+ const normalizedError = error instanceof Error ? error : new Error(String(error));
1383
+ this.config.onRequestError?.(config.url, config.method, normalizedError);
1384
+ throw handleHttpError(normalizedError);
1385
+ }
1386
+ }
1387
+
1388
+ // Convenience methods
1389
+ async get<T = unknown>(url: string, config?: Omit<RequestConfig, 'method' | 'url'>): Promise<T> {
1390
+ return this.request<T>({ method: 'GET', url, ...config });
1391
+ }
1392
+
1393
+ async post<T = unknown>(url: string, data?: unknown, config?: Omit<RequestConfig, 'method' | 'url' | 'data'>): Promise<T> {
1394
+ return this.request<T>({ method: 'POST', url, data, ...config });
1395
+ }
1396
+
1397
+ async put<T = unknown>(url: string, data?: unknown, config?: Omit<RequestConfig, 'method' | 'url' | 'data'>): Promise<T> {
1398
+ return this.request<T>({ method: 'PUT', url, data, ...config });
1399
+ }
1400
+
1401
+ async patch<T = unknown>(url: string, data?: unknown, config?: Omit<RequestConfig, 'method' | 'url' | 'data'>): Promise<T> {
1402
+ return this.request<T>({ method: 'PATCH', url, data, ...config });
1403
+ }
1404
+
1405
+ async delete<T = unknown>(url: string, config?: Omit<RequestConfig, 'method' | 'url'>): Promise<T> {
1406
+ return this.request<T>({ method: 'DELETE', url, ...config });
1407
+ }
1408
+
1409
+ // Token management
1410
+ setTokens(accessToken: string): void {
1411
+ this.tokenStore.setTokens(accessToken);
1412
+ this.notifyTokenChange();
1413
+ }
1414
+
1415
+ setAuthRefreshHandler(handler: AuthRefreshHandler | null): void {
1416
+ this.authRefreshHandler = handler;
1417
+ }
1418
+
1419
+ setAccessTokenProvider(provider: AccessTokenProvider | null): void {
1420
+ this.accessTokenProvider = provider;
1421
+ }
1422
+
1423
+ clearTokens(): void {
1424
+ this.tokenStore.clearTokens();
1425
+ this.tokenStore.clearCsrfToken();
1426
+ // Drop the response cache on logout. The cache is identity-scoped, so a
1427
+ // different user could never read these entries, but a logged-out client
1428
+ // must not keep the previous session's personalized data resident in
1429
+ // memory (privacy + correct logout semantics). We do NOT clear on
1430
+ // `setTokens` because a silent token refresh re-issues a token for the
1431
+ // SAME user — the identity tag is unchanged and the warm cache is still
1432
+ // valid; clearing there would defeat caching as refreshes fire near
1433
+ // every token expiry.
1434
+ this.cache.clear();
1435
+ this.notifyTokenChange();
1436
+ }
1437
+
1438
+ /**
1439
+ * Subscribe to access-token changes on this instance.
1440
+ *
1441
+ * Fires on every mutation of the access token — `setTokens`, `clearTokens`,
1442
+ * a successful silent refresh, and the internal 401-driven clear — passing
1443
+ * the resulting token (or `null` when cleared). Returns an unsubscribe
1444
+ * function; call it on teardown to avoid leaks.
1445
+ *
1446
+ * This is the single hook downstream code (e.g. @oxy.so/services' OxyProvider)
1447
+ * uses to keep an external token sink — such as the shared `oxyClient`
1448
+ * singleton — in lockstep with the active session, regardless of which code
1449
+ * path mutated the token.
1450
+ */
1451
+ addTokenChangeListener(listener: (accessToken: string | null) => void): () => void {
1452
+ this._tokenChangeListeners.add(listener);
1453
+ return () => {
1454
+ this._tokenChangeListeners.delete(listener);
1455
+ };
1456
+ }
1457
+
1458
+ /**
1459
+ * Notify all token-change listeners with the current access token.
1460
+ * Listener exceptions are isolated so one bad subscriber cannot break token
1461
+ * propagation to the others or to the calling auth flow.
1462
+ * @internal
1463
+ */
1464
+ private notifyTokenChange(): void {
1465
+ if (this._tokenChangeListeners.size === 0) return;
1466
+ const accessToken = this.tokenStore.getAccessToken();
1467
+ for (const listener of this._tokenChangeListeners) {
1468
+ try {
1469
+ listener(accessToken);
1470
+ } catch (error) {
1471
+ this.logger.error('Token change listener threw:', error);
1472
+ }
1473
+ }
1474
+ }
1475
+
1476
+ getAccessToken(): string | null {
1477
+ return this.tokenStore.getAccessToken();
1478
+ }
1479
+
1480
+ hasAccessToken(): boolean {
1481
+ return this.tokenStore.hasAccessToken();
1482
+ }
1483
+
1484
+ getBaseURL(): string {
1485
+ return this.baseURL;
1486
+ }
1487
+
1488
+ // Cache management
1489
+ clearCache(): void {
1490
+ this.cache.clear();
1491
+ }
1492
+
1493
+ /**
1494
+ * Delete a cache entry by its LOGICAL key (`method:url[:data]`).
1495
+ *
1496
+ * Because the response cache is identity-scoped — stored keys carry an
1497
+ * ` id=<identityTag>` suffix — a caller passing the logical key
1498
+ * `GET:/users/<id>` must invalidate that resource for EVERY identity that
1499
+ * cached it (e.g. `updateProfile` busting a user representation that may be
1500
+ * cached under both the owner's id and a viewer's id). We therefore delete
1501
+ * the exact key (for any pre-existing un-suffixed entries) AND every
1502
+ * identity-scoped variant `<key> id=*`.
1503
+ */
1504
+ clearCacheEntry(key: string): void {
1505
+ this.cache.delete(key);
1506
+ const identityVariantPrefix = `${key}${HttpService.CACHE_IDENTITY_DELIM}`;
1507
+ for (const existing of this.cache.keys()) {
1508
+ if (existing.startsWith(identityVariantPrefix)) {
1509
+ this.cache.delete(existing);
1510
+ }
1511
+ }
1512
+ }
1513
+
1514
+ /**
1515
+ * Delete every cache entry whose key starts with `prefix`.
1516
+ *
1517
+ * Used by mutations that don't know the exact downstream cache keys —
1518
+ * e.g. `updateProfile` invalidating all `GET:/session/user/*` entries
1519
+ * without having to track every active session ID. Returns the number of
1520
+ * deleted entries (for observability in tests).
1521
+ */
1522
+ clearCacheByPrefix(prefix: string): number {
1523
+ let removed = 0;
1524
+ for (const key of this.cache.keys()) {
1525
+ if (key.startsWith(prefix)) {
1526
+ this.cache.delete(key);
1527
+ removed++;
1528
+ }
1529
+ }
1530
+ return removed;
1531
+ }
1532
+
1533
+ getCacheStats() {
1534
+ const cacheStats = this.cache.getStats();
1535
+ const total = this.requestMetrics.cacheHits + this.requestMetrics.cacheMisses;
1536
+ return {
1537
+ size: cacheStats.size,
1538
+ hits: this.requestMetrics.cacheHits,
1539
+ misses: this.requestMetrics.cacheMisses,
1540
+ hitRate: total > 0 ? this.requestMetrics.cacheHits / total : 0,
1541
+ };
1542
+ }
1543
+
1544
+ getMetrics() {
1545
+ return { ...this.requestMetrics };
1546
+ }
1547
+
1548
+ // Test-only utility — clears tokens on this instance
1549
+ __resetTokensForTests(): void {
1550
+ this.tokenStore.clearTokens();
1551
+ this.tokenStore.clearCsrfToken();
1552
+ }
1553
+ }