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