@oxyhq/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 (277) hide show
  1. package/README.md +50 -0
  2. package/dist/cjs/AuthManager.js +361 -0
  3. package/dist/cjs/CrossDomainAuth.js +258 -0
  4. package/dist/cjs/HttpService.js +618 -0
  5. package/dist/cjs/OxyServices.base.js +263 -0
  6. package/dist/cjs/OxyServices.errors.js +22 -0
  7. package/dist/cjs/OxyServices.js +63 -0
  8. package/dist/cjs/constants/version.js +16 -0
  9. package/dist/cjs/crypto/index.js +20 -0
  10. package/dist/cjs/crypto/keyManager.js +887 -0
  11. package/dist/cjs/crypto/polyfill.js +64 -0
  12. package/dist/cjs/crypto/recoveryPhrase.js +169 -0
  13. package/dist/cjs/crypto/signatureService.js +296 -0
  14. package/dist/cjs/i18n/index.js +73 -0
  15. package/dist/cjs/i18n/locales/ar-SA.json +120 -0
  16. package/dist/cjs/i18n/locales/ca-ES.json +120 -0
  17. package/dist/cjs/i18n/locales/de-DE.json +120 -0
  18. package/dist/cjs/i18n/locales/en-US.json +956 -0
  19. package/dist/cjs/i18n/locales/es-ES.json +944 -0
  20. package/dist/cjs/i18n/locales/fr-FR.json +120 -0
  21. package/dist/cjs/i18n/locales/it-IT.json +120 -0
  22. package/dist/cjs/i18n/locales/ja-JP.json +119 -0
  23. package/dist/cjs/i18n/locales/ko-KR.json +120 -0
  24. package/dist/cjs/i18n/locales/locales/ar-SA.json +120 -0
  25. package/dist/cjs/i18n/locales/locales/ca-ES.json +120 -0
  26. package/dist/cjs/i18n/locales/locales/de-DE.json +120 -0
  27. package/dist/cjs/i18n/locales/locales/en-US.json +956 -0
  28. package/dist/cjs/i18n/locales/locales/es-ES.json +944 -0
  29. package/dist/cjs/i18n/locales/locales/fr-FR.json +120 -0
  30. package/dist/cjs/i18n/locales/locales/it-IT.json +120 -0
  31. package/dist/cjs/i18n/locales/locales/ja-JP.json +119 -0
  32. package/dist/cjs/i18n/locales/locales/ko-KR.json +120 -0
  33. package/dist/cjs/i18n/locales/locales/pt-PT.json +120 -0
  34. package/dist/cjs/i18n/locales/locales/zh-CN.json +120 -0
  35. package/dist/cjs/i18n/locales/pt-PT.json +120 -0
  36. package/dist/cjs/i18n/locales/zh-CN.json +120 -0
  37. package/dist/cjs/index.js +153 -0
  38. package/dist/cjs/mixins/OxyServices.analytics.js +49 -0
  39. package/dist/cjs/mixins/OxyServices.assets.js +380 -0
  40. package/dist/cjs/mixins/OxyServices.auth.js +259 -0
  41. package/dist/cjs/mixins/OxyServices.developer.js +97 -0
  42. package/dist/cjs/mixins/OxyServices.devices.js +116 -0
  43. package/dist/cjs/mixins/OxyServices.features.js +309 -0
  44. package/dist/cjs/mixins/OxyServices.fedcm.js +435 -0
  45. package/dist/cjs/mixins/OxyServices.karma.js +108 -0
  46. package/dist/cjs/mixins/OxyServices.language.js +154 -0
  47. package/dist/cjs/mixins/OxyServices.location.js +43 -0
  48. package/dist/cjs/mixins/OxyServices.payment.js +158 -0
  49. package/dist/cjs/mixins/OxyServices.popup.js +371 -0
  50. package/dist/cjs/mixins/OxyServices.privacy.js +162 -0
  51. package/dist/cjs/mixins/OxyServices.redirect.js +345 -0
  52. package/dist/cjs/mixins/OxyServices.security.js +81 -0
  53. package/dist/cjs/mixins/OxyServices.user.js +355 -0
  54. package/dist/cjs/mixins/OxyServices.utility.js +156 -0
  55. package/dist/cjs/mixins/index.js +79 -0
  56. package/dist/cjs/mixins/mixinHelpers.js +53 -0
  57. package/dist/cjs/models/interfaces.js +20 -0
  58. package/dist/cjs/models/session.js +2 -0
  59. package/dist/cjs/shared/index.js +70 -0
  60. package/dist/cjs/shared/utils/colorUtils.js +153 -0
  61. package/dist/cjs/shared/utils/debugUtils.js +73 -0
  62. package/dist/cjs/shared/utils/errorUtils.js +183 -0
  63. package/dist/cjs/shared/utils/index.js +49 -0
  64. package/dist/cjs/shared/utils/networkUtils.js +183 -0
  65. package/dist/cjs/shared/utils/themeUtils.js +106 -0
  66. package/dist/cjs/utils/apiUtils.js +61 -0
  67. package/dist/cjs/utils/asyncUtils.js +194 -0
  68. package/dist/cjs/utils/cache.js +226 -0
  69. package/dist/cjs/utils/deviceManager.js +205 -0
  70. package/dist/cjs/utils/errorUtils.js +154 -0
  71. package/dist/cjs/utils/index.js +26 -0
  72. package/dist/cjs/utils/languageUtils.js +165 -0
  73. package/dist/cjs/utils/loggerUtils.js +126 -0
  74. package/dist/cjs/utils/platform.js +144 -0
  75. package/dist/cjs/utils/requestUtils.js +209 -0
  76. package/dist/cjs/utils/sessionUtils.js +181 -0
  77. package/dist/cjs/utils/validationUtils.js +173 -0
  78. package/dist/esm/AuthManager.js +356 -0
  79. package/dist/esm/CrossDomainAuth.js +253 -0
  80. package/dist/esm/HttpService.js +614 -0
  81. package/dist/esm/OxyServices.base.js +259 -0
  82. package/dist/esm/OxyServices.errors.js +17 -0
  83. package/dist/esm/OxyServices.js +59 -0
  84. package/dist/esm/constants/version.js +13 -0
  85. package/dist/esm/crypto/index.js +13 -0
  86. package/dist/esm/crypto/keyManager.js +850 -0
  87. package/dist/esm/crypto/polyfill.js +61 -0
  88. package/dist/esm/crypto/recoveryPhrase.js +132 -0
  89. package/dist/esm/crypto/signatureService.js +259 -0
  90. package/dist/esm/i18n/index.js +69 -0
  91. package/dist/esm/i18n/locales/ar-SA.json +120 -0
  92. package/dist/esm/i18n/locales/ca-ES.json +120 -0
  93. package/dist/esm/i18n/locales/de-DE.json +120 -0
  94. package/dist/esm/i18n/locales/en-US.json +956 -0
  95. package/dist/esm/i18n/locales/es-ES.json +944 -0
  96. package/dist/esm/i18n/locales/fr-FR.json +120 -0
  97. package/dist/esm/i18n/locales/it-IT.json +120 -0
  98. package/dist/esm/i18n/locales/ja-JP.json +119 -0
  99. package/dist/esm/i18n/locales/ko-KR.json +120 -0
  100. package/dist/esm/i18n/locales/locales/ar-SA.json +120 -0
  101. package/dist/esm/i18n/locales/locales/ca-ES.json +120 -0
  102. package/dist/esm/i18n/locales/locales/de-DE.json +120 -0
  103. package/dist/esm/i18n/locales/locales/en-US.json +956 -0
  104. package/dist/esm/i18n/locales/locales/es-ES.json +944 -0
  105. package/dist/esm/i18n/locales/locales/fr-FR.json +120 -0
  106. package/dist/esm/i18n/locales/locales/it-IT.json +120 -0
  107. package/dist/esm/i18n/locales/locales/ja-JP.json +119 -0
  108. package/dist/esm/i18n/locales/locales/ko-KR.json +120 -0
  109. package/dist/esm/i18n/locales/locales/pt-PT.json +120 -0
  110. package/dist/esm/i18n/locales/locales/zh-CN.json +120 -0
  111. package/dist/esm/i18n/locales/pt-PT.json +120 -0
  112. package/dist/esm/i18n/locales/zh-CN.json +120 -0
  113. package/dist/esm/index.js +55 -0
  114. package/dist/esm/mixins/OxyServices.analytics.js +46 -0
  115. package/dist/esm/mixins/OxyServices.assets.js +377 -0
  116. package/dist/esm/mixins/OxyServices.auth.js +256 -0
  117. package/dist/esm/mixins/OxyServices.developer.js +94 -0
  118. package/dist/esm/mixins/OxyServices.devices.js +113 -0
  119. package/dist/esm/mixins/OxyServices.features.js +306 -0
  120. package/dist/esm/mixins/OxyServices.fedcm.js +433 -0
  121. package/dist/esm/mixins/OxyServices.karma.js +105 -0
  122. package/dist/esm/mixins/OxyServices.language.js +118 -0
  123. package/dist/esm/mixins/OxyServices.location.js +40 -0
  124. package/dist/esm/mixins/OxyServices.payment.js +155 -0
  125. package/dist/esm/mixins/OxyServices.popup.js +369 -0
  126. package/dist/esm/mixins/OxyServices.privacy.js +159 -0
  127. package/dist/esm/mixins/OxyServices.redirect.js +343 -0
  128. package/dist/esm/mixins/OxyServices.security.js +78 -0
  129. package/dist/esm/mixins/OxyServices.user.js +352 -0
  130. package/dist/esm/mixins/OxyServices.utility.js +153 -0
  131. package/dist/esm/mixins/index.js +76 -0
  132. package/dist/esm/mixins/mixinHelpers.js +48 -0
  133. package/dist/esm/models/interfaces.js +17 -0
  134. package/dist/esm/models/session.js +1 -0
  135. package/dist/esm/shared/index.js +31 -0
  136. package/dist/esm/shared/utils/colorUtils.js +143 -0
  137. package/dist/esm/shared/utils/debugUtils.js +65 -0
  138. package/dist/esm/shared/utils/errorUtils.js +170 -0
  139. package/dist/esm/shared/utils/index.js +15 -0
  140. package/dist/esm/shared/utils/networkUtils.js +173 -0
  141. package/dist/esm/shared/utils/themeUtils.js +98 -0
  142. package/dist/esm/utils/apiUtils.js +55 -0
  143. package/dist/esm/utils/asyncUtils.js +179 -0
  144. package/dist/esm/utils/cache.js +218 -0
  145. package/dist/esm/utils/deviceManager.js +168 -0
  146. package/dist/esm/utils/errorUtils.js +146 -0
  147. package/dist/esm/utils/index.js +7 -0
  148. package/dist/esm/utils/languageUtils.js +158 -0
  149. package/dist/esm/utils/loggerUtils.js +115 -0
  150. package/dist/esm/utils/platform.js +102 -0
  151. package/dist/esm/utils/requestUtils.js +203 -0
  152. package/dist/esm/utils/sessionUtils.js +171 -0
  153. package/dist/esm/utils/validationUtils.js +153 -0
  154. package/dist/types/AuthManager.d.ts +143 -0
  155. package/dist/types/CrossDomainAuth.d.ts +160 -0
  156. package/dist/types/HttpService.d.ts +163 -0
  157. package/dist/types/OxyServices.base.d.ts +126 -0
  158. package/dist/types/OxyServices.d.ts +81 -0
  159. package/dist/types/OxyServices.errors.d.ts +11 -0
  160. package/dist/types/constants/version.d.ts +13 -0
  161. package/dist/types/crypto/index.d.ts +11 -0
  162. package/dist/types/crypto/keyManager.d.ts +189 -0
  163. package/dist/types/crypto/polyfill.d.ts +11 -0
  164. package/dist/types/crypto/recoveryPhrase.d.ts +58 -0
  165. package/dist/types/crypto/signatureService.d.ts +86 -0
  166. package/dist/types/i18n/index.d.ts +3 -0
  167. package/dist/types/index.d.ts +50 -0
  168. package/dist/types/mixins/OxyServices.analytics.d.ts +66 -0
  169. package/dist/types/mixins/OxyServices.assets.d.ts +135 -0
  170. package/dist/types/mixins/OxyServices.auth.d.ts +186 -0
  171. package/dist/types/mixins/OxyServices.developer.d.ts +99 -0
  172. package/dist/types/mixins/OxyServices.devices.d.ts +96 -0
  173. package/dist/types/mixins/OxyServices.features.d.ts +228 -0
  174. package/dist/types/mixins/OxyServices.fedcm.d.ts +200 -0
  175. package/dist/types/mixins/OxyServices.karma.d.ts +85 -0
  176. package/dist/types/mixins/OxyServices.language.d.ts +81 -0
  177. package/dist/types/mixins/OxyServices.location.d.ts +64 -0
  178. package/dist/types/mixins/OxyServices.payment.d.ts +111 -0
  179. package/dist/types/mixins/OxyServices.popup.d.ts +205 -0
  180. package/dist/types/mixins/OxyServices.privacy.d.ts +122 -0
  181. package/dist/types/mixins/OxyServices.redirect.d.ts +245 -0
  182. package/dist/types/mixins/OxyServices.security.d.ts +78 -0
  183. package/dist/types/mixins/OxyServices.user.d.ts +182 -0
  184. package/dist/types/mixins/OxyServices.utility.d.ts +93 -0
  185. package/dist/types/mixins/index.d.ts +30 -0
  186. package/dist/types/mixins/mixinHelpers.d.ts +31 -0
  187. package/dist/types/models/interfaces.d.ts +415 -0
  188. package/dist/types/models/session.d.ts +27 -0
  189. package/dist/types/shared/index.d.ts +28 -0
  190. package/dist/types/shared/utils/colorUtils.d.ts +104 -0
  191. package/dist/types/shared/utils/debugUtils.d.ts +48 -0
  192. package/dist/types/shared/utils/errorUtils.d.ts +97 -0
  193. package/dist/types/shared/utils/index.d.ts +13 -0
  194. package/dist/types/shared/utils/networkUtils.d.ts +139 -0
  195. package/dist/types/shared/utils/themeUtils.d.ts +90 -0
  196. package/dist/types/utils/apiUtils.d.ts +53 -0
  197. package/dist/types/utils/asyncUtils.d.ts +58 -0
  198. package/dist/types/utils/cache.d.ts +127 -0
  199. package/dist/types/utils/deviceManager.d.ts +65 -0
  200. package/dist/types/utils/errorUtils.d.ts +46 -0
  201. package/dist/types/utils/index.d.ts +6 -0
  202. package/dist/types/utils/languageUtils.d.ts +37 -0
  203. package/dist/types/utils/loggerUtils.d.ts +48 -0
  204. package/dist/types/utils/platform.d.ts +40 -0
  205. package/dist/types/utils/requestUtils.d.ts +123 -0
  206. package/dist/types/utils/sessionUtils.d.ts +54 -0
  207. package/dist/types/utils/validationUtils.d.ts +85 -0
  208. package/package.json +84 -0
  209. package/src/AuthManager.ts +436 -0
  210. package/src/CrossDomainAuth.ts +307 -0
  211. package/src/HttpService.ts +752 -0
  212. package/src/OxyServices.base.ts +334 -0
  213. package/src/OxyServices.errors.ts +26 -0
  214. package/src/OxyServices.ts +129 -0
  215. package/src/constants/version.ts +15 -0
  216. package/src/crypto/index.ts +25 -0
  217. package/src/crypto/keyManager.ts +962 -0
  218. package/src/crypto/polyfill.ts +70 -0
  219. package/src/crypto/recoveryPhrase.ts +166 -0
  220. package/src/crypto/signatureService.ts +323 -0
  221. package/src/i18n/index.ts +75 -0
  222. package/src/i18n/locales/ar-SA.json +120 -0
  223. package/src/i18n/locales/ca-ES.json +120 -0
  224. package/src/i18n/locales/de-DE.json +120 -0
  225. package/src/i18n/locales/en-US.json +956 -0
  226. package/src/i18n/locales/es-ES.json +944 -0
  227. package/src/i18n/locales/fr-FR.json +120 -0
  228. package/src/i18n/locales/it-IT.json +120 -0
  229. package/src/i18n/locales/ja-JP.json +119 -0
  230. package/src/i18n/locales/ko-KR.json +120 -0
  231. package/src/i18n/locales/pt-PT.json +120 -0
  232. package/src/i18n/locales/zh-CN.json +120 -0
  233. package/src/index.ts +153 -0
  234. package/src/mixins/OxyServices.analytics.ts +53 -0
  235. package/src/mixins/OxyServices.assets.ts +412 -0
  236. package/src/mixins/OxyServices.auth.ts +358 -0
  237. package/src/mixins/OxyServices.developer.ts +114 -0
  238. package/src/mixins/OxyServices.devices.ts +119 -0
  239. package/src/mixins/OxyServices.features.ts +428 -0
  240. package/src/mixins/OxyServices.fedcm.ts +494 -0
  241. package/src/mixins/OxyServices.karma.ts +111 -0
  242. package/src/mixins/OxyServices.language.ts +127 -0
  243. package/src/mixins/OxyServices.location.ts +46 -0
  244. package/src/mixins/OxyServices.payment.ts +163 -0
  245. package/src/mixins/OxyServices.popup.ts +443 -0
  246. package/src/mixins/OxyServices.privacy.ts +182 -0
  247. package/src/mixins/OxyServices.redirect.ts +397 -0
  248. package/src/mixins/OxyServices.security.ts +103 -0
  249. package/src/mixins/OxyServices.user.ts +392 -0
  250. package/src/mixins/OxyServices.utility.ts +191 -0
  251. package/src/mixins/index.ts +91 -0
  252. package/src/mixins/mixinHelpers.ts +69 -0
  253. package/src/models/interfaces.ts +511 -0
  254. package/src/models/session.ts +30 -0
  255. package/src/shared/index.ts +82 -0
  256. package/src/shared/utils/colorUtils.ts +155 -0
  257. package/src/shared/utils/debugUtils.ts +73 -0
  258. package/src/shared/utils/errorUtils.ts +181 -0
  259. package/src/shared/utils/index.ts +59 -0
  260. package/src/shared/utils/networkUtils.ts +248 -0
  261. package/src/shared/utils/themeUtils.ts +115 -0
  262. package/src/types/bip39.d.ts +32 -0
  263. package/src/types/buffer.d.ts +97 -0
  264. package/src/types/color.d.ts +20 -0
  265. package/src/types/elliptic.d.ts +62 -0
  266. package/src/utils/apiUtils.ts +88 -0
  267. package/src/utils/asyncUtils.ts +252 -0
  268. package/src/utils/cache.ts +264 -0
  269. package/src/utils/deviceManager.ts +198 -0
  270. package/src/utils/errorUtils.ts +216 -0
  271. package/src/utils/index.ts +21 -0
  272. package/src/utils/languageUtils.ts +174 -0
  273. package/src/utils/loggerUtils.ts +153 -0
  274. package/src/utils/platform.ts +117 -0
  275. package/src/utils/requestUtils.ts +237 -0
  276. package/src/utils/sessionUtils.ts +206 -0
  277. package/src/utils/validationUtils.ts +174 -0
@@ -0,0 +1,614 @@
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';
16
+ import { RequestDeduplicator, RequestQueue, SimpleLogger } from './utils/requestUtils';
17
+ import { retryAsync } from './utils/asyncUtils';
18
+ import { handleHttpError } from './utils/errorUtils';
19
+ import { jwtDecode } from 'jwt-decode';
20
+ import { isNative, getPlatformOS } from './utils/platform';
21
+ /**
22
+ * Check if we're running in a native app environment (React Native, not web)
23
+ * This is used to determine CSRF handling mode
24
+ */
25
+ const isNativeApp = isNative();
26
+ /**
27
+ * Token store for authentication (singleton)
28
+ */
29
+ class TokenStore {
30
+ constructor() {
31
+ this.accessToken = null;
32
+ this.refreshToken = null;
33
+ this.csrfToken = null;
34
+ this.csrfTokenFetchPromise = null;
35
+ }
36
+ static getInstance() {
37
+ if (!TokenStore.instance) {
38
+ TokenStore.instance = new TokenStore();
39
+ }
40
+ return TokenStore.instance;
41
+ }
42
+ setTokens(accessToken, refreshToken = '') {
43
+ this.accessToken = accessToken;
44
+ this.refreshToken = refreshToken;
45
+ }
46
+ getAccessToken() {
47
+ return this.accessToken;
48
+ }
49
+ getRefreshToken() {
50
+ return this.refreshToken;
51
+ }
52
+ clearTokens() {
53
+ this.accessToken = null;
54
+ this.refreshToken = null;
55
+ }
56
+ hasAccessToken() {
57
+ return !!this.accessToken;
58
+ }
59
+ setCsrfToken(token) {
60
+ this.csrfToken = token;
61
+ }
62
+ getCsrfToken() {
63
+ return this.csrfToken;
64
+ }
65
+ setCsrfTokenFetchPromise(promise) {
66
+ this.csrfTokenFetchPromise = promise;
67
+ }
68
+ getCsrfTokenFetchPromise() {
69
+ return this.csrfTokenFetchPromise;
70
+ }
71
+ clearCsrfToken() {
72
+ this.csrfToken = null;
73
+ this.csrfTokenFetchPromise = null;
74
+ }
75
+ }
76
+ /**
77
+ * Unified HTTP Service
78
+ *
79
+ * Consolidates HttpClient + RequestManager into a single efficient class.
80
+ * Uses native fetch instead of axios for smaller bundle size.
81
+ */
82
+ export class HttpService {
83
+ constructor(config) {
84
+ // Performance monitoring
85
+ this.requestMetrics = {
86
+ totalRequests: 0,
87
+ successfulRequests: 0,
88
+ failedRequests: 0,
89
+ cacheHits: 0,
90
+ cacheMisses: 0,
91
+ averageResponseTime: 0,
92
+ };
93
+ this.config = config;
94
+ this.baseURL = config.baseURL;
95
+ this.tokenStore = TokenStore.getInstance();
96
+ this.logger = new SimpleLogger(config.enableLogging || false, config.logLevel || 'error', 'HttpService');
97
+ // Initialize performance infrastructure
98
+ this.cache = new TTLCache(config.cacheTTL || 5 * 60 * 1000);
99
+ registerCacheForCleanup(this.cache);
100
+ this.deduplicator = new RequestDeduplicator();
101
+ this.requestQueue = new RequestQueue(config.maxConcurrentRequests || 10, config.requestQueueSize || 100);
102
+ }
103
+ /**
104
+ * Robust FormData detection that works in browser and Node.js environments
105
+ * Checks multiple conditions to handle different FormData implementations
106
+ */
107
+ isFormData(data) {
108
+ if (!data) {
109
+ return false;
110
+ }
111
+ // Primary check: instanceof FormData (works in browser and Node.js with proper polyfills)
112
+ if (data instanceof FormData) {
113
+ return true;
114
+ }
115
+ // Fallback: Check constructor name (handles Node.js polyfills like form-data)
116
+ if (typeof data === 'object' && data !== null) {
117
+ const constructorName = data.constructor?.name;
118
+ if (constructorName === 'FormData' || constructorName === 'FormDataImpl') {
119
+ return true;
120
+ }
121
+ // Additional check: Look for FormData-like methods
122
+ if (typeof data.append === 'function' &&
123
+ typeof data.get === 'function' &&
124
+ typeof data.has === 'function') {
125
+ return true;
126
+ }
127
+ }
128
+ return false;
129
+ }
130
+ /**
131
+ * Main request method - handles everything in one place
132
+ */
133
+ async request(config) {
134
+ const { method, url, data, params, timeout = this.config.requestTimeout || 5000, signal, cache = method === 'GET', cacheTTL, deduplicate = true, retry = this.config.enableRetry !== false, maxRetries = this.config.maxRetries || 3, } = config;
135
+ // Generate cache key (optimized for large objects)
136
+ const cacheKey = cache ? this.generateCacheKey(method, url, data || params) : null;
137
+ // Check cache first
138
+ if (cache && cacheKey) {
139
+ const cached = this.cache.get(cacheKey);
140
+ if (cached !== null) {
141
+ this.requestMetrics.cacheHits++;
142
+ this.logger.debug('Cache hit:', url);
143
+ return cached;
144
+ }
145
+ this.requestMetrics.cacheMisses++;
146
+ }
147
+ // Request function
148
+ const requestFn = async () => {
149
+ const startTime = Date.now();
150
+ try {
151
+ // Build URL with params
152
+ const fullUrl = this.buildURL(url, params);
153
+ // Get auth token (with auto-refresh)
154
+ const authHeader = await this.getAuthHeader();
155
+ // Get CSRF token for state-changing requests
156
+ const isStateChangingMethod = ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method);
157
+ const csrfToken = isStateChangingMethod ? await this.fetchCsrfToken() : null;
158
+ // Determine if data is FormData using robust detection
159
+ const isFormData = this.isFormData(data);
160
+ // Make fetch request
161
+ const controller = new AbortController();
162
+ const timeoutId = timeout ? setTimeout(() => controller.abort(), timeout) : null;
163
+ if (signal) {
164
+ signal.addEventListener('abort', () => controller.abort());
165
+ }
166
+ // Build headers - start with defaults
167
+ const headers = {
168
+ 'Accept': 'application/json',
169
+ };
170
+ // Only set Content-Type for non-FormData requests (FormData sets it automatically with boundary)
171
+ if (!isFormData) {
172
+ headers['Content-Type'] = 'application/json';
173
+ }
174
+ // Add authorization header if available
175
+ if (authHeader) {
176
+ headers['Authorization'] = authHeader;
177
+ }
178
+ // Add CSRF token header for state-changing requests
179
+ if (csrfToken) {
180
+ headers['X-CSRF-Token'] = csrfToken;
181
+ }
182
+ // Add native app header for React Native (required for CSRF validation)
183
+ // Native apps can't persist cookies like browsers, so the server uses
184
+ // header-only CSRF validation when this header is present
185
+ if (isNativeApp && isStateChangingMethod) {
186
+ headers['X-Native-App'] = 'true';
187
+ }
188
+ // Debug logging for CSRF issues
189
+ if (isStateChangingMethod && __DEV__) {
190
+ console.log('[HttpService] CSRF Debug:', {
191
+ url,
192
+ method,
193
+ isNativeApp,
194
+ platformOS: getPlatformOS(),
195
+ hasCsrfToken: !!csrfToken,
196
+ csrfTokenLength: csrfToken?.length,
197
+ hasNativeAppHeader: headers['X-Native-App'] === 'true',
198
+ });
199
+ }
200
+ // Merge custom headers if provided
201
+ if (config.headers) {
202
+ Object.entries(config.headers).forEach(([key, value]) => {
203
+ // For FormData, explicitly remove Content-Type if user tries to set it
204
+ // The browser/fetch API will set it automatically with the boundary
205
+ if (isFormData && key.toLowerCase() === 'content-type') {
206
+ this.logger.debug('Ignoring Content-Type header for FormData - will be set automatically');
207
+ return;
208
+ }
209
+ headers[key] = value;
210
+ });
211
+ }
212
+ const bodyValue = method !== 'GET' && data
213
+ ? (isFormData ? data : JSON.stringify(data))
214
+ : undefined;
215
+ const response = await fetch(fullUrl, {
216
+ method,
217
+ headers,
218
+ body: bodyValue,
219
+ signal: controller.signal,
220
+ credentials: 'include', // Include cookies for cross-origin requests (CSRF, session)
221
+ });
222
+ if (timeoutId)
223
+ clearTimeout(timeoutId);
224
+ // Handle response
225
+ if (!response.ok) {
226
+ if (response.status === 401) {
227
+ this.tokenStore.clearTokens();
228
+ }
229
+ // Try to parse error response (handle empty/malformed JSON)
230
+ let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
231
+ const contentType = response.headers.get('content-type');
232
+ if (contentType && contentType.includes('application/json')) {
233
+ try {
234
+ const errorData = await response.json();
235
+ // Check both 'message' and 'error' fields for backwards compatibility
236
+ if (errorData?.message) {
237
+ errorMessage = errorData.message;
238
+ }
239
+ else if (errorData?.error) {
240
+ errorMessage = errorData.error;
241
+ }
242
+ }
243
+ catch (parseError) {
244
+ // Malformed JSON or empty response - use status text
245
+ this.logger.warn('Failed to parse error response JSON:', parseError);
246
+ }
247
+ }
248
+ const error = new Error(errorMessage);
249
+ error.status = response.status;
250
+ error.response = { status: response.status, statusText: response.statusText };
251
+ throw error;
252
+ }
253
+ // Handle different response types (optimized - read response once)
254
+ const contentType = response.headers.get('content-type');
255
+ let responseData;
256
+ if (contentType && contentType.includes('application/json')) {
257
+ // Use response.json() directly for better performance
258
+ try {
259
+ responseData = await response.json();
260
+ // Handle null/undefined responses
261
+ if (responseData === null || responseData === undefined) {
262
+ responseData = null;
263
+ }
264
+ else {
265
+ // Unwrap standardized API response format for JSON
266
+ responseData = this.unwrapResponse(responseData);
267
+ }
268
+ }
269
+ catch (parseError) {
270
+ // Handle malformed JSON or empty responses gracefully
271
+ // Note: Once response.json() is called, the body is consumed and cannot be read again
272
+ // So we check the error type to determine if it's empty or malformed
273
+ if (parseError instanceof SyntaxError) {
274
+ this.logger.warn('Failed to parse JSON response (malformed or empty):', parseError);
275
+ // SyntaxError typically means empty or malformed JSON
276
+ // For empty responses, return null; for malformed JSON, throw descriptive error
277
+ responseData = null; // Treat as empty response for safety
278
+ }
279
+ else {
280
+ this.logger.warn('Failed to read response:', parseError);
281
+ throw new Error('Failed to read response from server');
282
+ }
283
+ }
284
+ }
285
+ else if (contentType && (contentType.includes('application/octet-stream') || contentType.includes('image/') || contentType.includes('video/') || contentType.includes('audio/'))) {
286
+ // For binary responses (blobs), return the blob directly without unwrapping
287
+ responseData = await response.blob();
288
+ }
289
+ else {
290
+ // For other responses, return as text
291
+ const text = await response.text();
292
+ responseData = text || null;
293
+ }
294
+ const duration = Date.now() - startTime;
295
+ this.updateMetrics(true, duration);
296
+ this.config.onRequestEnd?.(url, method, duration, true);
297
+ return responseData;
298
+ }
299
+ catch (error) {
300
+ const duration = Date.now() - startTime;
301
+ this.updateMetrics(false, duration);
302
+ this.config.onRequestEnd?.(url, method, duration, false);
303
+ this.config.onRequestError?.(url, method, error instanceof Error ? error : new Error(String(error)));
304
+ // Handle AbortError specifically for better error messages
305
+ if (error instanceof Error && error.name === 'AbortError') {
306
+ throw handleHttpError(error);
307
+ }
308
+ throw handleHttpError(error);
309
+ }
310
+ };
311
+ // Wrap with retry if enabled
312
+ const requestWithRetry = retry
313
+ ? () => retryAsync(requestFn, maxRetries, this.config.retryDelay || 1000)
314
+ : requestFn;
315
+ // Wrap with deduplication if enabled (use optimized key generation)
316
+ const dedupeKey = deduplicate ? this.generateCacheKey(method, url, data || params) : null;
317
+ const finalRequest = dedupeKey
318
+ ? () => this.deduplicator.deduplicate(dedupeKey, requestWithRetry)
319
+ : requestWithRetry;
320
+ // Execute request (with queue if needed)
321
+ const result = await this.requestQueue.enqueue(finalRequest);
322
+ // Cache the result if caching is enabled
323
+ if (cache && cacheKey && result) {
324
+ this.cache.set(cacheKey, result, cacheTTL);
325
+ }
326
+ return result;
327
+ }
328
+ /**
329
+ * Generate cache key efficiently
330
+ * Uses simple hash for large objects to avoid expensive JSON.stringify
331
+ */
332
+ generateCacheKey(method, url, data) {
333
+ if (!data || (typeof data === 'object' && Object.keys(data).length === 0)) {
334
+ return `${method}:${url}`;
335
+ }
336
+ // For small objects, use JSON.stringify
337
+ const dataStr = JSON.stringify(data);
338
+ if (dataStr.length < 1000) {
339
+ return `${method}:${url}:${dataStr}`;
340
+ }
341
+ // For large objects, use a simple hash based on keys and values length
342
+ // This avoids expensive serialization while still being unique enough
343
+ const hash = typeof data === 'object' && data !== null
344
+ ? Object.keys(data).sort().join(',') + ':' + dataStr.length
345
+ : String(data).substring(0, 100);
346
+ return `${method}:${url}:${hash}`;
347
+ }
348
+ /**
349
+ * Build full URL with query params
350
+ */
351
+ buildURL(url, params) {
352
+ const base = url.startsWith('http') ? url : `${this.baseURL}${url}`;
353
+ if (!params || Object.keys(params).length === 0) {
354
+ return base;
355
+ }
356
+ const searchParams = new URLSearchParams();
357
+ Object.entries(params).forEach(([key, value]) => {
358
+ if (value !== undefined && value !== null) {
359
+ searchParams.append(key, String(value));
360
+ }
361
+ });
362
+ const queryString = searchParams.toString();
363
+ return queryString ? `${base}${base.includes('?') ? '&' : '?'}${queryString}` : base;
364
+ }
365
+ /**
366
+ * Fetch CSRF token from server (with deduplication)
367
+ * Required for state-changing requests (POST, PUT, PATCH, DELETE)
368
+ */
369
+ async fetchCsrfToken() {
370
+ // Return cached token if available
371
+ const cachedToken = this.tokenStore.getCsrfToken();
372
+ if (cachedToken) {
373
+ if (__DEV__)
374
+ console.log('[HttpService] Using cached CSRF token');
375
+ return cachedToken;
376
+ }
377
+ // Deduplicate concurrent CSRF token fetches
378
+ const existingPromise = this.tokenStore.getCsrfTokenFetchPromise();
379
+ if (existingPromise) {
380
+ if (__DEV__)
381
+ console.log('[HttpService] Waiting for existing CSRF fetch');
382
+ return existingPromise;
383
+ }
384
+ const fetchPromise = (async () => {
385
+ try {
386
+ if (__DEV__)
387
+ console.log('[HttpService] Fetching CSRF token from:', `${this.baseURL}/api/csrf-token`);
388
+ // Use AbortController for timeout (more compatible than AbortSignal.timeout)
389
+ const controller = new AbortController();
390
+ const timeoutId = setTimeout(() => controller.abort(), 5000);
391
+ const response = await fetch(`${this.baseURL}/api/csrf-token`, {
392
+ method: 'GET',
393
+ headers: { 'Accept': 'application/json' },
394
+ credentials: 'include', // Required to receive and send cookies
395
+ signal: controller.signal,
396
+ });
397
+ clearTimeout(timeoutId);
398
+ if (__DEV__)
399
+ console.log('[HttpService] CSRF fetch response:', response.status, response.ok);
400
+ if (response.ok) {
401
+ const data = await response.json();
402
+ if (__DEV__)
403
+ console.log('[HttpService] CSRF response data:', data);
404
+ const token = data.csrfToken || null;
405
+ this.tokenStore.setCsrfToken(token);
406
+ this.logger.debug('CSRF token fetched');
407
+ return token;
408
+ }
409
+ // Also check response header for CSRF token
410
+ const headerToken = response.headers.get('X-CSRF-Token');
411
+ if (headerToken) {
412
+ this.tokenStore.setCsrfToken(headerToken);
413
+ this.logger.debug('CSRF token from header');
414
+ return headerToken;
415
+ }
416
+ if (__DEV__)
417
+ console.log('[HttpService] CSRF fetch failed with status:', response.status);
418
+ this.logger.warn('Failed to fetch CSRF token:', response.status);
419
+ return null;
420
+ }
421
+ catch (error) {
422
+ if (__DEV__)
423
+ console.log('[HttpService] CSRF fetch error:', error);
424
+ this.logger.warn('CSRF token fetch error:', error);
425
+ return null;
426
+ }
427
+ finally {
428
+ this.tokenStore.setCsrfTokenFetchPromise(null);
429
+ }
430
+ })();
431
+ this.tokenStore.setCsrfTokenFetchPromise(fetchPromise);
432
+ return fetchPromise;
433
+ }
434
+ /**
435
+ * Get auth header with automatic token refresh
436
+ */
437
+ async getAuthHeader() {
438
+ const accessToken = this.tokenStore.getAccessToken();
439
+ if (!accessToken) {
440
+ return null;
441
+ }
442
+ try {
443
+ const decoded = jwtDecode(accessToken);
444
+ const currentTime = Math.floor(Date.now() / 1000);
445
+ // If token expires in less than 60 seconds, refresh it
446
+ if (decoded.exp && decoded.exp - currentTime < 60 && decoded.sessionId) {
447
+ try {
448
+ const refreshUrl = `${this.baseURL}/api/session/token/${decoded.sessionId}`;
449
+ // Use AbortSignal.timeout for consistent timeout handling
450
+ const response = await fetch(refreshUrl, {
451
+ method: 'GET',
452
+ headers: { 'Accept': 'application/json' },
453
+ signal: AbortSignal.timeout(5000),
454
+ credentials: 'include', // Include cookies for cross-origin requests
455
+ });
456
+ if (response.ok) {
457
+ const { accessToken: newToken } = await response.json();
458
+ this.tokenStore.setTokens(newToken);
459
+ this.logger.debug('Token refreshed');
460
+ return `Bearer ${newToken}`;
461
+ }
462
+ }
463
+ catch (refreshError) {
464
+ this.logger.warn('Token refresh failed, using current token');
465
+ }
466
+ }
467
+ return `Bearer ${accessToken}`;
468
+ }
469
+ catch (error) {
470
+ this.logger.error('Error processing token:', error);
471
+ return `Bearer ${accessToken}`;
472
+ }
473
+ }
474
+ /**
475
+ * Unwrap standardized API response format
476
+ */
477
+ unwrapResponse(responseData) {
478
+ // Handle paginated responses: { data: [...], pagination: {...} }
479
+ if (responseData && typeof responseData === 'object' && 'data' in responseData && 'pagination' in responseData) {
480
+ return responseData;
481
+ }
482
+ // Handle regular success responses: { data: ... }
483
+ if (responseData && typeof responseData === 'object' && 'data' in responseData && !Array.isArray(responseData)) {
484
+ return responseData.data;
485
+ }
486
+ // Return as-is for responses that don't use sendSuccess wrapper
487
+ return responseData;
488
+ }
489
+ /**
490
+ * Update request metrics
491
+ */
492
+ updateMetrics(success, duration) {
493
+ this.requestMetrics.totalRequests++;
494
+ if (success) {
495
+ this.requestMetrics.successfulRequests++;
496
+ }
497
+ else {
498
+ this.requestMetrics.failedRequests++;
499
+ }
500
+ const alpha = 0.1;
501
+ this.requestMetrics.averageResponseTime =
502
+ this.requestMetrics.averageResponseTime * (1 - alpha) + duration * alpha;
503
+ }
504
+ // Convenience methods (for backward compatibility)
505
+ /**
506
+ * GET request convenience method
507
+ */
508
+ async get(url, config) {
509
+ const result = await this.request({ method: 'GET', url, ...config });
510
+ return { data: result };
511
+ }
512
+ /**
513
+ * POST request convenience method
514
+ * Supports FormData uploads - Content-Type will be set automatically for FormData
515
+ * @param url - Request URL
516
+ * @param data - Request body (can be FormData for file uploads)
517
+ * @param config - Request configuration including optional headers
518
+ * @example
519
+ * ```typescript
520
+ * const formData = new FormData();
521
+ * formData.append('file', file);
522
+ * await api.post('/upload', formData, { headers: { 'X-Custom-Header': 'value' } });
523
+ * ```
524
+ */
525
+ async post(url, data, config) {
526
+ const result = await this.request({ method: 'POST', url, data, ...config });
527
+ return { data: result };
528
+ }
529
+ /**
530
+ * PUT request convenience method
531
+ * Supports FormData uploads - Content-Type will be set automatically for FormData
532
+ * @param url - Request URL
533
+ * @param data - Request body (can be FormData for file uploads)
534
+ * @param config - Request configuration including optional headers
535
+ * @example
536
+ * ```typescript
537
+ * const formData = new FormData();
538
+ * formData.append('file', file);
539
+ * await api.put('/upload', formData, { headers: { 'X-Custom-Header': 'value' } });
540
+ * ```
541
+ */
542
+ async put(url, data, config) {
543
+ const result = await this.request({ method: 'PUT', url, data, ...config });
544
+ return { data: result };
545
+ }
546
+ /**
547
+ * PATCH request convenience method
548
+ * Supports FormData uploads - Content-Type will be set automatically for FormData
549
+ * @param url - Request URL
550
+ * @param data - Request body (can be FormData for file uploads)
551
+ * @param config - Request configuration including optional headers
552
+ * @example
553
+ * ```typescript
554
+ * const formData = new FormData();
555
+ * formData.append('file', file);
556
+ * await api.patch('/upload', formData, { headers: { 'X-Custom-Header': 'value' } });
557
+ * ```
558
+ */
559
+ async patch(url, data, config) {
560
+ const result = await this.request({ method: 'PATCH', url, data, ...config });
561
+ return { data: result };
562
+ }
563
+ async delete(url, config) {
564
+ const result = await this.request({ method: 'DELETE', url, ...config });
565
+ return { data: result };
566
+ }
567
+ // Token management
568
+ setTokens(accessToken, refreshToken = '') {
569
+ this.tokenStore.setTokens(accessToken, refreshToken);
570
+ }
571
+ clearTokens() {
572
+ this.tokenStore.clearTokens();
573
+ this.tokenStore.clearCsrfToken();
574
+ }
575
+ getAccessToken() {
576
+ return this.tokenStore.getAccessToken();
577
+ }
578
+ hasAccessToken() {
579
+ return this.tokenStore.hasAccessToken();
580
+ }
581
+ getBaseURL() {
582
+ return this.baseURL;
583
+ }
584
+ // Cache management
585
+ clearCache() {
586
+ this.cache.clear();
587
+ }
588
+ clearCacheEntry(key) {
589
+ this.cache.delete(key);
590
+ }
591
+ getCacheStats() {
592
+ const cacheStats = this.cache.getStats();
593
+ const total = this.requestMetrics.cacheHits + this.requestMetrics.cacheMisses;
594
+ return {
595
+ size: cacheStats.size,
596
+ hits: this.requestMetrics.cacheHits,
597
+ misses: this.requestMetrics.cacheMisses,
598
+ hitRate: total > 0 ? this.requestMetrics.cacheHits / total : 0,
599
+ };
600
+ }
601
+ getMetrics() {
602
+ return { ...this.requestMetrics };
603
+ }
604
+ // Test-only utility
605
+ static __resetTokensForTests() {
606
+ try {
607
+ TokenStore.getInstance().clearTokens();
608
+ }
609
+ catch (error) {
610
+ // Silently fail in test cleanup - this is expected behavior
611
+ // TokenStore might not be initialized in some test scenarios
612
+ }
613
+ }
614
+ }