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