@oxyhq/core 1.11.11 → 1.11.13

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 (130) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/CrossDomainAuth.js +3 -1
  3. package/dist/cjs/HttpService.js +227 -51
  4. package/dist/cjs/OxyServices.base.js +9 -0
  5. package/dist/cjs/OxyServices.js +8 -3
  6. package/dist/cjs/crypto/index.js +3 -1
  7. package/dist/cjs/crypto/keyManager.js +476 -172
  8. package/dist/cjs/crypto/polyfill.js +14 -65
  9. package/dist/cjs/crypto/recoveryPhrase.js +30 -11
  10. package/dist/cjs/crypto/signatureService.js +25 -60
  11. package/dist/cjs/i18n/locales/en-US.json +46 -1
  12. package/dist/cjs/i18n/locales/es-ES.json +46 -1
  13. package/dist/cjs/i18n/locales/locales/en-US.json +46 -1
  14. package/dist/cjs/i18n/locales/locales/es-ES.json +46 -1
  15. package/dist/cjs/index.js +7 -2
  16. package/dist/cjs/mixins/OxyServices.assets.js +9 -4
  17. package/dist/cjs/mixins/OxyServices.auth.js +27 -0
  18. package/dist/cjs/mixins/OxyServices.contacts.js +50 -0
  19. package/dist/cjs/mixins/OxyServices.features.js +0 -11
  20. package/dist/cjs/mixins/OxyServices.fedcm.js +4 -3
  21. package/dist/cjs/mixins/OxyServices.language.js +5 -36
  22. package/dist/cjs/mixins/OxyServices.redirect.js +6 -2
  23. package/dist/cjs/mixins/OxyServices.security.js +13 -2
  24. package/dist/cjs/mixins/OxyServices.user.js +70 -38
  25. package/dist/cjs/mixins/OxyServices.utility.js +19 -43
  26. package/dist/cjs/mixins/index.js +11 -3
  27. package/dist/cjs/utils/accountUtils.js +71 -2
  28. package/dist/cjs/utils/asyncUtils.js +34 -5
  29. package/dist/cjs/utils/deviceManager.js +5 -36
  30. package/dist/cjs/utils/platformCrypto.js +165 -0
  31. package/dist/cjs/utils/platformCrypto.native.js +123 -0
  32. package/dist/esm/.tsbuildinfo +1 -1
  33. package/dist/esm/CrossDomainAuth.js +3 -1
  34. package/dist/esm/HttpService.js +228 -52
  35. package/dist/esm/OxyServices.base.js +9 -0
  36. package/dist/esm/OxyServices.js +8 -3
  37. package/dist/esm/crypto/index.js +1 -1
  38. package/dist/esm/crypto/keyManager.js +473 -138
  39. package/dist/esm/crypto/polyfill.js +14 -32
  40. package/dist/esm/crypto/recoveryPhrase.js +30 -11
  41. package/dist/esm/crypto/signatureService.js +25 -27
  42. package/dist/esm/i18n/locales/en-US.json +46 -1
  43. package/dist/esm/i18n/locales/es-ES.json +46 -1
  44. package/dist/esm/i18n/locales/locales/en-US.json +46 -1
  45. package/dist/esm/i18n/locales/locales/es-ES.json +46 -1
  46. package/dist/esm/index.js +2 -2
  47. package/dist/esm/mixins/OxyServices.assets.js +9 -4
  48. package/dist/esm/mixins/OxyServices.auth.js +27 -0
  49. package/dist/esm/mixins/OxyServices.contacts.js +47 -0
  50. package/dist/esm/mixins/OxyServices.features.js +0 -11
  51. package/dist/esm/mixins/OxyServices.fedcm.js +4 -3
  52. package/dist/esm/mixins/OxyServices.language.js +5 -3
  53. package/dist/esm/mixins/OxyServices.redirect.js +6 -2
  54. package/dist/esm/mixins/OxyServices.security.js +13 -2
  55. package/dist/esm/mixins/OxyServices.user.js +70 -38
  56. package/dist/esm/mixins/OxyServices.utility.js +19 -10
  57. package/dist/esm/mixins/index.js +11 -3
  58. package/dist/esm/utils/accountUtils.js +67 -1
  59. package/dist/esm/utils/asyncUtils.js +34 -5
  60. package/dist/esm/utils/deviceManager.js +5 -3
  61. package/dist/esm/utils/platformCrypto.js +125 -0
  62. package/dist/esm/utils/platformCrypto.native.js +80 -0
  63. package/dist/types/.tsbuildinfo +1 -1
  64. package/dist/types/HttpService.d.ts +47 -3
  65. package/dist/types/OxyServices.base.d.ts +7 -0
  66. package/dist/types/OxyServices.d.ts +36 -3
  67. package/dist/types/crypto/index.d.ts +1 -1
  68. package/dist/types/crypto/keyManager.d.ts +110 -9
  69. package/dist/types/crypto/polyfill.d.ts +3 -1
  70. package/dist/types/crypto/recoveryPhrase.d.ts +31 -7
  71. package/dist/types/crypto/signatureService.d.ts +4 -0
  72. package/dist/types/index.d.ts +4 -3
  73. package/dist/types/mixins/OxyServices.analytics.d.ts +1 -0
  74. package/dist/types/mixins/OxyServices.assets.d.ts +6 -10
  75. package/dist/types/mixins/OxyServices.auth.d.ts +16 -0
  76. package/dist/types/mixins/OxyServices.contacts.d.ts +99 -0
  77. package/dist/types/mixins/OxyServices.developer.d.ts +1 -0
  78. package/dist/types/mixins/OxyServices.devices.d.ts +1 -0
  79. package/dist/types/mixins/OxyServices.features.d.ts +2 -7
  80. package/dist/types/mixins/OxyServices.fedcm.d.ts +1 -0
  81. package/dist/types/mixins/OxyServices.karma.d.ts +1 -0
  82. package/dist/types/mixins/OxyServices.language.d.ts +1 -0
  83. package/dist/types/mixins/OxyServices.location.d.ts +1 -0
  84. package/dist/types/mixins/OxyServices.managedAccounts.d.ts +1 -0
  85. package/dist/types/mixins/OxyServices.payment.d.ts +1 -0
  86. package/dist/types/mixins/OxyServices.popup.d.ts +1 -0
  87. package/dist/types/mixins/OxyServices.privacy.d.ts +1 -0
  88. package/dist/types/mixins/OxyServices.redirect.d.ts +1 -0
  89. package/dist/types/mixins/OxyServices.security.d.ts +1 -0
  90. package/dist/types/mixins/OxyServices.topics.d.ts +1 -0
  91. package/dist/types/mixins/OxyServices.user.d.ts +40 -11
  92. package/dist/types/mixins/OxyServices.utility.d.ts +1 -0
  93. package/dist/types/mixins/index.d.ts +52 -4
  94. package/dist/types/models/interfaces.d.ts +62 -3
  95. package/dist/types/utils/accountUtils.d.ts +41 -1
  96. package/dist/types/utils/asyncUtils.d.ts +6 -2
  97. package/dist/types/utils/platformCrypto.d.ts +87 -0
  98. package/dist/types/utils/platformCrypto.native.d.ts +54 -0
  99. package/package.json +28 -1
  100. package/src/CrossDomainAuth.ts +12 -10
  101. package/src/HttpService.ts +264 -51
  102. package/src/OxyServices.base.ts +10 -0
  103. package/src/OxyServices.ts +9 -4
  104. package/src/crypto/__tests__/keyManager.test.ts +336 -0
  105. package/src/crypto/index.ts +6 -1
  106. package/src/crypto/keyManager.ts +529 -151
  107. package/src/crypto/polyfill.ts +14 -34
  108. package/src/crypto/recoveryPhrase.ts +56 -17
  109. package/src/crypto/signatureService.ts +25 -29
  110. package/src/i18n/locales/en-US.json +46 -1
  111. package/src/i18n/locales/es-ES.json +46 -1
  112. package/src/index.ts +16 -3
  113. package/src/mixins/OxyServices.assets.ts +15 -11
  114. package/src/mixins/OxyServices.auth.ts +28 -0
  115. package/src/mixins/OxyServices.contacts.ts +73 -0
  116. package/src/mixins/OxyServices.features.ts +2 -12
  117. package/src/mixins/OxyServices.fedcm.ts +4 -3
  118. package/src/mixins/OxyServices.language.ts +6 -4
  119. package/src/mixins/OxyServices.redirect.ts +6 -2
  120. package/src/mixins/OxyServices.security.ts +18 -8
  121. package/src/mixins/OxyServices.user.ts +90 -49
  122. package/src/mixins/OxyServices.utility.ts +19 -10
  123. package/src/mixins/index.ts +58 -7
  124. package/src/models/interfaces.ts +65 -3
  125. package/src/utils/__tests__/asyncUtils.test.ts +187 -0
  126. package/src/utils/accountUtils.ts +82 -2
  127. package/src/utils/asyncUtils.ts +39 -9
  128. package/src/utils/deviceManager.ts +7 -4
  129. package/src/utils/platformCrypto.native.ts +101 -0
  130. package/src/utils/platformCrypto.ts +145 -0
@@ -164,7 +164,9 @@ export class CrossDomainAuth {
164
164
  * Check if FedCM is supported in current browser
165
165
  */
166
166
  isFedCMSupported() {
167
- return this.oxyServices.constructor.isFedCMSupported?.() || false;
167
+ // FedCM support is exposed both as a static and an instance method on
168
+ // OxyServices; the instance method is reliable across mixin composition.
169
+ return this.oxyServices.isFedCMSupported?.() || false;
168
170
  }
169
171
  /**
170
172
  * Get recommended authentication method for current environment
@@ -16,14 +16,39 @@ import { TTLCache, registerCacheForCleanup } from './utils/cache.js';
16
16
  import { RequestDeduplicator, RequestQueue, SimpleLogger } from './utils/requestUtils.js';
17
17
  import { retryAsync } from './utils/asyncUtils.js';
18
18
  import { handleHttpError } from './utils/errorUtils.js';
19
- import { isDev } from './shared/utils/debugUtils.js';
20
19
  import { jwtDecode } from 'jwt-decode';
21
- import { isNative, getPlatformOS } from './utils/platform.js';
20
+ import { isNative, isReactNative, getPlatformOS } from './utils/platform.js';
22
21
  /**
23
22
  * Check if we're running in a native app environment (React Native, not web)
24
23
  * This is used to determine CSRF handling mode
25
24
  */
26
25
  const isNativeApp = isNative();
26
+ /**
27
+ * FNV-1a 32-bit non-cryptographic hash.
28
+ *
29
+ * Used by the cache-key generator for large payloads where full JSON
30
+ * inclusion would balloon the cache map keys. Content-addressed: every
31
+ * byte of the input contributes to the digest, so two payloads with the
32
+ * same top-level shape but different field values produce different keys
33
+ * (the previous `keys + length` heuristic collided on these).
34
+ *
35
+ * Trade-offs:
36
+ * - 32 bits is ample for an in-process cache (collision risk negligible
37
+ * at our key counts; we also prefix with method + url which further
38
+ * partitions the keyspace).
39
+ * - Not cryptographically secure — never use for security decisions.
40
+ * - Zero dependencies, branch-free hot loop, ~1 GiB/s on V8.
41
+ */
42
+ function fnv1a32(str) {
43
+ let h = 0x811c9dc5;
44
+ for (let i = 0; i < str.length; i++) {
45
+ h ^= str.charCodeAt(i);
46
+ // h * 16777619 mod 2^32, written as shift-and-add for portability and
47
+ // to avoid 53-bit JS number truncation in the intermediate multiply.
48
+ h = (h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24))) >>> 0;
49
+ }
50
+ return h.toString(16).padStart(8, '0');
51
+ }
27
52
  /**
28
53
  * Token store for authentication (instance-based)
29
54
  * Each HttpService gets its own TokenStore to prevent conflicts
@@ -103,31 +128,53 @@ export class HttpService {
103
128
  this.requestQueue = new RequestQueue(config.maxConcurrentRequests || 10, config.requestQueueSize || 100);
104
129
  }
105
130
  /**
106
- * Robust FormData detection that works in browser and Node.js environments
107
- * Checks multiple conditions to handle different FormData implementations
131
+ * Robust FormData detection that works in browser, React Native, and
132
+ * Node.js polyfill environments.
133
+ *
134
+ * Why we don't use `instanceof FormData` alone:
135
+ * - React Native's FormData is a separate class, not the browser one —
136
+ * `instanceof FormData` is true only inside the JS runtime that
137
+ * instantiated the value (browser-side polyfills also have their own).
138
+ * - The Node.js `form-data` polyfill ships its own constructor.
139
+ *
140
+ * Why we explicitly reject `URLSearchParams`:
141
+ * - `URLSearchParams` ALSO exposes `append` / `get` / `has`, so the
142
+ * duck-type fallback below would have misidentified it as FormData.
143
+ * - We want urlencoded payloads to take the JSON-stringify path so the
144
+ * server receives them as `application/x-www-form-urlencoded` instead
145
+ * of an empty multipart body.
108
146
  */
109
147
  isFormData(data) {
110
- if (!data) {
148
+ if (!data || typeof data !== 'object') {
111
149
  return false;
112
150
  }
113
- // Primary check: instanceof FormData (works in browser and Node.js with proper polyfills)
114
- if (data instanceof FormData) {
151
+ // Reject URLSearchParams up front: it shares the duck-typed surface
152
+ // (append / get / has) but is a fundamentally different content type.
153
+ // The caller routes URLSearchParams through the regular body path.
154
+ if (typeof URLSearchParams !== 'undefined' && data instanceof URLSearchParams) {
155
+ return false;
156
+ }
157
+ // Primary check: instanceof FormData. Works whenever the value was
158
+ // constructed by the same runtime/realm that exposes `FormData`.
159
+ if (typeof FormData !== 'undefined' && data instanceof FormData) {
115
160
  return true;
116
161
  }
117
- // Fallback: Check constructor name (handles Node.js polyfills like form-data)
118
- if (typeof data === 'object' && data !== null) {
119
- const constructorName = data.constructor?.name;
120
- if (constructorName === 'FormData' || constructorName === 'FormDataImpl') {
121
- return true;
122
- }
123
- // Additional check: Look for FormData-like methods
124
- if (typeof data.append === 'function' &&
125
- typeof data.get === 'function' &&
126
- typeof data.has === 'function') {
127
- return true;
128
- }
162
+ // Fallback: detect Node / RN polyfills by constructor name. Limited to
163
+ // the small handful of known names so we don't accept arbitrary
164
+ // user-supplied objects with a coincidental `name`.
165
+ const constructorName = data.constructor?.name;
166
+ if (constructorName === 'FormData' || constructorName === 'FormDataImpl') {
167
+ return true;
129
168
  }
130
- return false;
169
+ // Last-resort duck typing — require the full FormData write surface
170
+ // (`append`, `get`, `has`, `getAll`, `delete`) so plain objects with
171
+ // an `append` method don't accidentally match.
172
+ const candidate = data;
173
+ return (typeof candidate.append === 'function' &&
174
+ typeof candidate.get === 'function' &&
175
+ typeof candidate.has === 'function' &&
176
+ typeof candidate.getAll === 'function' &&
177
+ typeof candidate.delete === 'function');
131
178
  }
132
179
  /**
133
180
  * Main request method - handles everything in one place
@@ -187,9 +234,12 @@ export class HttpService {
187
234
  if (isNativeApp && isStateChangingMethod) {
188
235
  headers['X-Native-App'] = 'true';
189
236
  }
190
- // Debug logging for CSRF issues
191
- if (isStateChangingMethod && isDev()) {
192
- console.log('[HttpService] CSRF Debug:', {
237
+ // Debug logging for CSRF issues — routed through the SimpleLogger so
238
+ // it only fires when consumers opt in via `enableLogging`. Previously
239
+ // this was a bare console.log that leaked noise into every host app's
240
+ // stdout in development.
241
+ if (isStateChangingMethod) {
242
+ this.logger.debug('CSRF Debug:', {
193
243
  url,
194
244
  method,
195
245
  isNativeApp,
@@ -218,13 +268,22 @@ export class HttpService {
218
268
  const bodyValue = method !== 'GET' && data
219
269
  ? (isFormData ? data : JSON.stringify(data))
220
270
  : undefined;
221
- const response = await fetch(fullUrl, {
222
- method,
223
- headers,
224
- body: bodyValue,
225
- signal: controller.signal,
226
- credentials: 'include', // Include cookies for cross-origin requests (CSRF, session)
227
- });
271
+ // React Native FormData workaround:
272
+ // Expo SDK 56's "winter fetch" rejects RN file descriptors `{uri, type, name}`
273
+ // in FormDataPart conversion (`Unsupported FormDataPart implementation`).
274
+ // RN's native XMLHttpRequest handles those descriptors correctly, so we
275
+ // route multipart uploads through XHR on RN only. JSON, text, etc. still
276
+ // use fetch on every platform.
277
+ const useXhrForUpload = isFormData && isReactNative() && typeof XMLHttpRequest !== 'undefined';
278
+ const response = useXhrForUpload
279
+ ? await this.uploadViaXHR(fullUrl, method, headers, bodyValue, controller.signal, timeout)
280
+ : await fetch(fullUrl, {
281
+ method,
282
+ headers,
283
+ body: bodyValue,
284
+ signal: controller.signal,
285
+ credentials: 'include', // Include cookies for cross-origin requests (CSRF, session)
286
+ });
228
287
  if (timeoutId)
229
288
  clearTimeout(timeoutId);
230
289
  // Handle response
@@ -364,25 +423,131 @@ export class HttpService {
364
423
  }
365
424
  return result;
366
425
  }
426
+ /**
427
+ * Upload via XMLHttpRequest (React Native FormData workaround).
428
+ *
429
+ * Expo SDK 56's "winter fetch" cannot serialize RN file descriptors
430
+ * (`{uri, type, name}`) — `convertFormDataAsync` rejects them as
431
+ * `Unsupported FormDataPart implementation`. RN's native XHR streams
432
+ * the file from disk correctly, so multipart uploads go through XHR
433
+ * on RN only.
434
+ *
435
+ * Returns a standard `Response` so downstream parsing in `request()`
436
+ * (status checks, 401/403 retries, JSON/blob/text parsing) is identical
437
+ * to the fetch path.
438
+ */
439
+ uploadViaXHR(url, method, headers, body, abortSignal, timeout) {
440
+ return new Promise((resolve, reject) => {
441
+ const xhr = new XMLHttpRequest();
442
+ xhr.open(method, url, true);
443
+ // withCredentials mirrors fetch's `credentials: 'include'` so the
444
+ // session cookie and CSRF cookie continue to flow.
445
+ xhr.withCredentials = true;
446
+ // Forward headers but skip Content-Type — XHR sets the multipart
447
+ // boundary automatically and overriding it breaks the upload.
448
+ for (const [key, value] of Object.entries(headers)) {
449
+ if (key.toLowerCase() === 'content-type')
450
+ continue;
451
+ try {
452
+ xhr.setRequestHeader(key, value);
453
+ }
454
+ catch (headerError) {
455
+ // Some headers (e.g. forbidden header names) cannot be set —
456
+ // log and continue rather than failing the whole upload.
457
+ this.logger.warn('XHR setRequestHeader failed:', key, headerError);
458
+ }
459
+ }
460
+ xhr.responseType = 'text';
461
+ if (timeout > 0) {
462
+ xhr.timeout = timeout;
463
+ }
464
+ const onAbort = () => {
465
+ try {
466
+ xhr.abort();
467
+ }
468
+ catch { /* xhr already finished */ }
469
+ };
470
+ if (abortSignal.aborted) {
471
+ reject(new DOMException('The user aborted a request.', 'AbortError'));
472
+ return;
473
+ }
474
+ abortSignal.addEventListener('abort', onAbort);
475
+ const cleanup = () => {
476
+ abortSignal.removeEventListener('abort', onAbort);
477
+ };
478
+ xhr.onload = () => {
479
+ cleanup();
480
+ const responseHeaders = HttpService.parseXHRHeaders(xhr.getAllResponseHeaders());
481
+ resolve(new Response(xhr.responseText, {
482
+ status: xhr.status,
483
+ statusText: xhr.statusText,
484
+ headers: responseHeaders,
485
+ }));
486
+ };
487
+ xhr.onerror = () => {
488
+ cleanup();
489
+ reject(new TypeError('Network request failed'));
490
+ };
491
+ xhr.ontimeout = () => {
492
+ cleanup();
493
+ reject(new DOMException('The request timed out.', 'TimeoutError'));
494
+ };
495
+ xhr.onabort = () => {
496
+ cleanup();
497
+ reject(new DOMException('The user aborted a request.', 'AbortError'));
498
+ };
499
+ xhr.send(body);
500
+ });
501
+ }
502
+ /**
503
+ * Parse raw header string from `XMLHttpRequest.getAllResponseHeaders()`
504
+ * into a `Headers`-compatible object.
505
+ */
506
+ static parseXHRHeaders(rawHeaders) {
507
+ const headers = new Headers();
508
+ if (!rawHeaders)
509
+ return headers;
510
+ // RFC 7230 line terminator is CRLF; some XHR implementations use LF only.
511
+ const lines = rawHeaders.trim().split(/\r?\n/);
512
+ for (const line of lines) {
513
+ const colonIndex = line.indexOf(':');
514
+ if (colonIndex <= 0)
515
+ continue;
516
+ const key = line.slice(0, colonIndex).trim();
517
+ const value = line.slice(colonIndex + 1).trim();
518
+ if (key) {
519
+ try {
520
+ headers.append(key, value);
521
+ }
522
+ catch {
523
+ // Invalid header name/value — skip.
524
+ }
525
+ }
526
+ }
527
+ return headers;
528
+ }
367
529
  /**
368
530
  * Generate cache key efficiently
369
- * Uses simple hash for large objects to avoid expensive JSON.stringify
531
+ * Uses a content-addressed hash for large payloads so two requests with
532
+ * the same shape but different values never collide on the same key
533
+ * (which would silently serve stale data — e.g. paginated search results,
534
+ * large object updates).
370
535
  */
371
536
  generateCacheKey(method, url, data) {
372
537
  if (!data || (typeof data === 'object' && Object.keys(data).length === 0)) {
373
538
  return `${method}:${url}`;
374
539
  }
375
- // For small objects, use JSON.stringify
540
+ // For small objects, the full serialization IS the key — fastest and
541
+ // guaranteed to be content-addressed.
376
542
  const dataStr = JSON.stringify(data);
377
543
  if (dataStr.length < 1000) {
378
544
  return `${method}:${url}:${dataStr}`;
379
545
  }
380
- // For large objects, use a simple hash based on keys and values length
381
- // This avoids expensive serialization while still being unique enough
382
- const hash = typeof data === 'object' && data !== null
383
- ? Object.keys(data).sort().join(',') + ':' + dataStr.length
384
- : String(data).substring(0, 100);
385
- return `${method}:${url}:${hash}`;
546
+ // For large payloads, hash the full serialized string so the key remains
547
+ // content-addressed (any byte change yields a different hash). Previous
548
+ // implementation hashed `keys + length` which collided for any two
549
+ // payloads with the same top-level keys and serialized length.
550
+ return `${method}:${url}:${fnv1a32(dataStr)}`;
386
551
  }
387
552
  /**
388
553
  * Build full URL with query params
@@ -409,23 +574,20 @@ export class HttpService {
409
574
  // Return cached token if available
410
575
  const cachedToken = this.tokenStore.getCsrfToken();
411
576
  if (cachedToken) {
412
- if (isDev())
413
- console.log('[HttpService] Using cached CSRF token');
577
+ this.logger.debug('Using cached CSRF token');
414
578
  return cachedToken;
415
579
  }
416
580
  // Deduplicate concurrent CSRF token fetches
417
581
  const existingPromise = this.tokenStore.getCsrfTokenFetchPromise();
418
582
  if (existingPromise) {
419
- if (isDev())
420
- console.log('[HttpService] Waiting for existing CSRF fetch');
583
+ this.logger.debug('Waiting for existing CSRF fetch');
421
584
  return existingPromise;
422
585
  }
423
586
  const fetchPromise = (async () => {
424
587
  const maxAttempts = 2;
425
588
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
426
589
  try {
427
- if (isDev())
428
- console.log('[HttpService] Fetching CSRF token from:', `${this.baseURL}/csrf-token`, `(attempt ${attempt})`);
590
+ this.logger.debug('Fetching CSRF token from:', `${this.baseURL}/csrf-token`, `(attempt ${attempt})`);
429
591
  // Use AbortController for timeout (more compatible than AbortSignal.timeout)
430
592
  const controller = new AbortController();
431
593
  const timeoutId = setTimeout(() => controller.abort(), 5000);
@@ -436,12 +598,10 @@ export class HttpService {
436
598
  signal: controller.signal,
437
599
  });
438
600
  clearTimeout(timeoutId);
439
- if (isDev())
440
- console.log('[HttpService] CSRF fetch response:', response.status, response.ok);
601
+ this.logger.debug('CSRF fetch response:', response.status, response.ok);
441
602
  if (response.ok) {
442
603
  const data = await response.json();
443
- if (isDev())
444
- console.log('[HttpService] CSRF response data:', data);
604
+ this.logger.debug('CSRF response data:', data);
445
605
  const token = data.csrfToken || null;
446
606
  this.tokenStore.setCsrfToken(token);
447
607
  this.logger.debug('CSRF token fetched');
@@ -454,13 +614,11 @@ export class HttpService {
454
614
  this.logger.debug('CSRF token from header');
455
615
  return headerToken;
456
616
  }
457
- if (isDev())
458
- console.log('[HttpService] CSRF fetch failed with status:', response.status);
617
+ this.logger.debug('CSRF fetch failed with status:', response.status);
459
618
  this.logger.warn('Failed to fetch CSRF token:', response.status);
460
619
  }
461
620
  catch (error) {
462
- if (isDev())
463
- console.log('[HttpService] CSRF fetch error:', error);
621
+ this.logger.debug('CSRF fetch error:', error);
464
622
  this.logger.warn('CSRF token fetch error:', error);
465
623
  }
466
624
  // Wait before retry (500ms)
@@ -619,6 +777,24 @@ export class HttpService {
619
777
  clearCacheEntry(key) {
620
778
  this.cache.delete(key);
621
779
  }
780
+ /**
781
+ * Delete every cache entry whose key starts with `prefix`.
782
+ *
783
+ * Used by mutations that don't know the exact downstream cache keys —
784
+ * e.g. `updateProfile` invalidating all `GET:/session/user/*` entries
785
+ * without having to track every active session ID. Returns the number of
786
+ * deleted entries (for observability in tests).
787
+ */
788
+ clearCacheByPrefix(prefix) {
789
+ let removed = 0;
790
+ for (const key of this.cache.keys()) {
791
+ if (key.startsWith(prefix)) {
792
+ this.cache.delete(key);
793
+ removed++;
794
+ }
795
+ }
796
+ return removed;
797
+ }
622
798
  getCacheStats() {
623
799
  const cacheStats = this.cache.getStats();
624
800
  const total = this.requestMetrics.cacheHits + this.requestMetrics.cacheMisses;
@@ -75,6 +75,15 @@ export class OxyServicesBase {
75
75
  clearCacheEntry(key) {
76
76
  this.httpService.clearCacheEntry(key);
77
77
  }
78
+ /**
79
+ * Clear every cache entry whose key starts with `prefix`.
80
+ * Useful for mutations that invalidate a family of GET responses
81
+ * without enumerating each one (e.g. all session-user lookups after
82
+ * a profile update).
83
+ */
84
+ clearCacheByPrefix(prefix) {
85
+ return this.httpService.clearCacheByPrefix(prefix);
86
+ }
78
87
  /**
79
88
  * Get cache statistics
80
89
  */
@@ -34,9 +34,14 @@ import { composeOxyServices } from './mixins/index.js';
34
34
  * });
35
35
  * ```
36
36
  */
37
- // Compose all mixins into the final OxyServices class
37
+ // Compose all mixins into the final OxyServices class. The composed runtime
38
+ // class augments OxyServicesBase with every mixin's methods (see mixins/index.ts).
39
+ // Statically, TypeScript sees this as a constructor producing OxyServicesBase;
40
+ // the additional methods are exposed via interface merging on `OxyServices` below.
38
41
  const OxyServicesComposed = composeOxyServices();
39
- // Export as a named class to avoid TypeScript issues with anonymous class types
42
+ // Export as a named class to avoid TypeScript issues with anonymous class types.
43
+ // We extend the composed constructor directly — its public surface is broadened
44
+ // to the full mixin set via the interface declaration that follows.
40
45
  export class OxyServices extends OxyServicesComposed {
41
46
  constructor(config) {
42
47
  super(config);
@@ -45,7 +50,7 @@ export class OxyServices extends OxyServicesComposed {
45
50
  // Re-export error classes for convenience
46
51
  export { OxyAuthenticationError, OxyAuthenticationTimeoutError };
47
52
  /**
48
- * Export the default Oxy Cloud URL (for backward compatibility)
53
+ * Default Oxy Cloud URL used when no `cloudURL` is provided to OxyServices.
49
54
  */
50
55
  export const OXY_CLOUD_URL = 'https://cloud.oxy.so';
51
56
  /**
@@ -6,7 +6,7 @@
6
6
  */
7
7
  // Import polyfills first - this ensures Buffer is available for bip39 and other libraries
8
8
  import './polyfill.js';
9
- export { KeyManager } from './keyManager.js';
9
+ export { KeyManager, IdentityAlreadyExistsError, IdentityPersistError, } from './keyManager.js';
10
10
  export { SignatureService } from './signatureService.js';
11
11
  export { RecoveryPhraseService } from './recoveryPhrase.js';
12
12
  // Re-export for convenience