@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.
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/CrossDomainAuth.js +3 -1
- package/dist/cjs/HttpService.js +227 -51
- package/dist/cjs/OxyServices.base.js +9 -0
- package/dist/cjs/OxyServices.js +8 -3
- package/dist/cjs/crypto/index.js +3 -1
- package/dist/cjs/crypto/keyManager.js +476 -172
- package/dist/cjs/crypto/polyfill.js +14 -65
- package/dist/cjs/crypto/recoveryPhrase.js +30 -11
- package/dist/cjs/crypto/signatureService.js +25 -60
- package/dist/cjs/i18n/locales/en-US.json +46 -1
- package/dist/cjs/i18n/locales/es-ES.json +46 -1
- package/dist/cjs/i18n/locales/locales/en-US.json +46 -1
- package/dist/cjs/i18n/locales/locales/es-ES.json +46 -1
- package/dist/cjs/index.js +7 -2
- package/dist/cjs/mixins/OxyServices.assets.js +9 -4
- package/dist/cjs/mixins/OxyServices.auth.js +27 -0
- package/dist/cjs/mixins/OxyServices.contacts.js +50 -0
- package/dist/cjs/mixins/OxyServices.features.js +0 -11
- package/dist/cjs/mixins/OxyServices.fedcm.js +4 -3
- package/dist/cjs/mixins/OxyServices.language.js +5 -36
- package/dist/cjs/mixins/OxyServices.redirect.js +6 -2
- package/dist/cjs/mixins/OxyServices.security.js +13 -2
- package/dist/cjs/mixins/OxyServices.user.js +70 -38
- package/dist/cjs/mixins/OxyServices.utility.js +19 -43
- package/dist/cjs/mixins/index.js +11 -3
- package/dist/cjs/utils/accountUtils.js +71 -2
- package/dist/cjs/utils/asyncUtils.js +34 -5
- package/dist/cjs/utils/deviceManager.js +5 -36
- package/dist/cjs/utils/platformCrypto.js +165 -0
- package/dist/cjs/utils/platformCrypto.native.js +123 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/CrossDomainAuth.js +3 -1
- package/dist/esm/HttpService.js +228 -52
- package/dist/esm/OxyServices.base.js +9 -0
- package/dist/esm/OxyServices.js +8 -3
- package/dist/esm/crypto/index.js +1 -1
- package/dist/esm/crypto/keyManager.js +473 -138
- package/dist/esm/crypto/polyfill.js +14 -32
- package/dist/esm/crypto/recoveryPhrase.js +30 -11
- package/dist/esm/crypto/signatureService.js +25 -27
- package/dist/esm/i18n/locales/en-US.json +46 -1
- package/dist/esm/i18n/locales/es-ES.json +46 -1
- package/dist/esm/i18n/locales/locales/en-US.json +46 -1
- package/dist/esm/i18n/locales/locales/es-ES.json +46 -1
- package/dist/esm/index.js +2 -2
- package/dist/esm/mixins/OxyServices.assets.js +9 -4
- package/dist/esm/mixins/OxyServices.auth.js +27 -0
- package/dist/esm/mixins/OxyServices.contacts.js +47 -0
- package/dist/esm/mixins/OxyServices.features.js +0 -11
- package/dist/esm/mixins/OxyServices.fedcm.js +4 -3
- package/dist/esm/mixins/OxyServices.language.js +5 -3
- package/dist/esm/mixins/OxyServices.redirect.js +6 -2
- package/dist/esm/mixins/OxyServices.security.js +13 -2
- package/dist/esm/mixins/OxyServices.user.js +70 -38
- package/dist/esm/mixins/OxyServices.utility.js +19 -10
- package/dist/esm/mixins/index.js +11 -3
- package/dist/esm/utils/accountUtils.js +67 -1
- package/dist/esm/utils/asyncUtils.js +34 -5
- package/dist/esm/utils/deviceManager.js +5 -3
- package/dist/esm/utils/platformCrypto.js +125 -0
- package/dist/esm/utils/platformCrypto.native.js +80 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/HttpService.d.ts +47 -3
- package/dist/types/OxyServices.base.d.ts +7 -0
- package/dist/types/OxyServices.d.ts +36 -3
- package/dist/types/crypto/index.d.ts +1 -1
- package/dist/types/crypto/keyManager.d.ts +110 -9
- package/dist/types/crypto/polyfill.d.ts +3 -1
- package/dist/types/crypto/recoveryPhrase.d.ts +31 -7
- package/dist/types/crypto/signatureService.d.ts +4 -0
- package/dist/types/index.d.ts +4 -3
- package/dist/types/mixins/OxyServices.analytics.d.ts +1 -0
- package/dist/types/mixins/OxyServices.assets.d.ts +6 -10
- package/dist/types/mixins/OxyServices.auth.d.ts +16 -0
- package/dist/types/mixins/OxyServices.contacts.d.ts +99 -0
- package/dist/types/mixins/OxyServices.developer.d.ts +1 -0
- package/dist/types/mixins/OxyServices.devices.d.ts +1 -0
- package/dist/types/mixins/OxyServices.features.d.ts +2 -7
- package/dist/types/mixins/OxyServices.fedcm.d.ts +1 -0
- package/dist/types/mixins/OxyServices.karma.d.ts +1 -0
- package/dist/types/mixins/OxyServices.language.d.ts +1 -0
- package/dist/types/mixins/OxyServices.location.d.ts +1 -0
- package/dist/types/mixins/OxyServices.managedAccounts.d.ts +1 -0
- package/dist/types/mixins/OxyServices.payment.d.ts +1 -0
- package/dist/types/mixins/OxyServices.popup.d.ts +1 -0
- package/dist/types/mixins/OxyServices.privacy.d.ts +1 -0
- package/dist/types/mixins/OxyServices.redirect.d.ts +1 -0
- package/dist/types/mixins/OxyServices.security.d.ts +1 -0
- package/dist/types/mixins/OxyServices.topics.d.ts +1 -0
- package/dist/types/mixins/OxyServices.user.d.ts +40 -11
- package/dist/types/mixins/OxyServices.utility.d.ts +1 -0
- package/dist/types/mixins/index.d.ts +52 -4
- package/dist/types/models/interfaces.d.ts +62 -3
- package/dist/types/utils/accountUtils.d.ts +41 -1
- package/dist/types/utils/asyncUtils.d.ts +6 -2
- package/dist/types/utils/platformCrypto.d.ts +87 -0
- package/dist/types/utils/platformCrypto.native.d.ts +54 -0
- package/package.json +28 -1
- package/src/CrossDomainAuth.ts +12 -10
- package/src/HttpService.ts +264 -51
- package/src/OxyServices.base.ts +10 -0
- package/src/OxyServices.ts +9 -4
- package/src/crypto/__tests__/keyManager.test.ts +336 -0
- package/src/crypto/index.ts +6 -1
- package/src/crypto/keyManager.ts +529 -151
- package/src/crypto/polyfill.ts +14 -34
- package/src/crypto/recoveryPhrase.ts +56 -17
- package/src/crypto/signatureService.ts +25 -29
- package/src/i18n/locales/en-US.json +46 -1
- package/src/i18n/locales/es-ES.json +46 -1
- package/src/index.ts +16 -3
- package/src/mixins/OxyServices.assets.ts +15 -11
- package/src/mixins/OxyServices.auth.ts +28 -0
- package/src/mixins/OxyServices.contacts.ts +73 -0
- package/src/mixins/OxyServices.features.ts +2 -12
- package/src/mixins/OxyServices.fedcm.ts +4 -3
- package/src/mixins/OxyServices.language.ts +6 -4
- package/src/mixins/OxyServices.redirect.ts +6 -2
- package/src/mixins/OxyServices.security.ts +18 -8
- package/src/mixins/OxyServices.user.ts +90 -49
- package/src/mixins/OxyServices.utility.ts +19 -10
- package/src/mixins/index.ts +58 -7
- package/src/models/interfaces.ts +65 -3
- package/src/utils/__tests__/asyncUtils.test.ts +187 -0
- package/src/utils/accountUtils.ts +82 -2
- package/src/utils/asyncUtils.ts +39 -9
- package/src/utils/deviceManager.ts +7 -4
- package/src/utils/platformCrypto.native.ts +101 -0
- 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
|
-
|
|
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
|
package/dist/esm/HttpService.js
CHANGED
|
@@ -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
|
|
107
|
-
*
|
|
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
|
-
//
|
|
114
|
-
|
|
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:
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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
|
-
|
|
192
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
|
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,
|
|
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
|
|
381
|
-
//
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
*/
|
package/dist/esm/OxyServices.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
/**
|
package/dist/esm/crypto/index.js
CHANGED
|
@@ -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
|