@oxyhq/core 1.11.11 → 1.11.12
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/HttpService.js +13 -18
- package/dist/cjs/crypto/signatureService.js +11 -11
- package/dist/cjs/mixins/OxyServices.user.js +11 -0
- package/dist/cjs/utils/asyncUtils.js +34 -5
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/HttpService.js +13 -18
- package/dist/esm/crypto/keyManager.js +3 -3
- package/dist/esm/crypto/polyfill.js +1 -1
- package/dist/esm/crypto/signatureService.js +12 -12
- package/dist/esm/mixins/OxyServices.language.js +1 -1
- package/dist/esm/mixins/OxyServices.user.js +11 -0
- package/dist/esm/utils/asyncUtils.js +34 -5
- package/dist/esm/utils/deviceManager.js +1 -1
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/mixins/OxyServices.user.d.ts +12 -0
- package/dist/types/utils/asyncUtils.d.ts +6 -2
- package/package.json +1 -1
- package/src/HttpService.ts +13 -11
- package/src/crypto/keyManager.ts +3 -3
- package/src/crypto/polyfill.ts +1 -1
- package/src/crypto/signatureService.ts +13 -12
- package/src/mixins/OxyServices.language.ts +1 -1
- package/src/mixins/OxyServices.user.ts +18 -0
- package/src/utils/__tests__/asyncUtils.test.ts +187 -0
- package/src/utils/asyncUtils.ts +39 -9
- package/src/utils/deviceManager.ts +1 -1
package/dist/esm/HttpService.js
CHANGED
|
@@ -16,7 +16,6 @@ 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
20
|
import { isNative, getPlatformOS } from './utils/platform.js';
|
|
22
21
|
/**
|
|
@@ -187,9 +186,12 @@ export class HttpService {
|
|
|
187
186
|
if (isNativeApp && isStateChangingMethod) {
|
|
188
187
|
headers['X-Native-App'] = 'true';
|
|
189
188
|
}
|
|
190
|
-
// Debug logging for CSRF issues
|
|
191
|
-
|
|
192
|
-
|
|
189
|
+
// Debug logging for CSRF issues — routed through the SimpleLogger so
|
|
190
|
+
// it only fires when consumers opt in via `enableLogging`. Previously
|
|
191
|
+
// this was a bare console.log that leaked noise into every host app's
|
|
192
|
+
// stdout in development.
|
|
193
|
+
if (isStateChangingMethod) {
|
|
194
|
+
this.logger.debug('CSRF Debug:', {
|
|
193
195
|
url,
|
|
194
196
|
method,
|
|
195
197
|
isNativeApp,
|
|
@@ -409,23 +411,20 @@ export class HttpService {
|
|
|
409
411
|
// Return cached token if available
|
|
410
412
|
const cachedToken = this.tokenStore.getCsrfToken();
|
|
411
413
|
if (cachedToken) {
|
|
412
|
-
|
|
413
|
-
console.log('[HttpService] Using cached CSRF token');
|
|
414
|
+
this.logger.debug('Using cached CSRF token');
|
|
414
415
|
return cachedToken;
|
|
415
416
|
}
|
|
416
417
|
// Deduplicate concurrent CSRF token fetches
|
|
417
418
|
const existingPromise = this.tokenStore.getCsrfTokenFetchPromise();
|
|
418
419
|
if (existingPromise) {
|
|
419
|
-
|
|
420
|
-
console.log('[HttpService] Waiting for existing CSRF fetch');
|
|
420
|
+
this.logger.debug('Waiting for existing CSRF fetch');
|
|
421
421
|
return existingPromise;
|
|
422
422
|
}
|
|
423
423
|
const fetchPromise = (async () => {
|
|
424
424
|
const maxAttempts = 2;
|
|
425
425
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
426
426
|
try {
|
|
427
|
-
|
|
428
|
-
console.log('[HttpService] Fetching CSRF token from:', `${this.baseURL}/csrf-token`, `(attempt ${attempt})`);
|
|
427
|
+
this.logger.debug('Fetching CSRF token from:', `${this.baseURL}/csrf-token`, `(attempt ${attempt})`);
|
|
429
428
|
// Use AbortController for timeout (more compatible than AbortSignal.timeout)
|
|
430
429
|
const controller = new AbortController();
|
|
431
430
|
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
@@ -436,12 +435,10 @@ export class HttpService {
|
|
|
436
435
|
signal: controller.signal,
|
|
437
436
|
});
|
|
438
437
|
clearTimeout(timeoutId);
|
|
439
|
-
|
|
440
|
-
console.log('[HttpService] CSRF fetch response:', response.status, response.ok);
|
|
438
|
+
this.logger.debug('CSRF fetch response:', response.status, response.ok);
|
|
441
439
|
if (response.ok) {
|
|
442
440
|
const data = await response.json();
|
|
443
|
-
|
|
444
|
-
console.log('[HttpService] CSRF response data:', data);
|
|
441
|
+
this.logger.debug('CSRF response data:', data);
|
|
445
442
|
const token = data.csrfToken || null;
|
|
446
443
|
this.tokenStore.setCsrfToken(token);
|
|
447
444
|
this.logger.debug('CSRF token fetched');
|
|
@@ -454,13 +451,11 @@ export class HttpService {
|
|
|
454
451
|
this.logger.debug('CSRF token from header');
|
|
455
452
|
return headerToken;
|
|
456
453
|
}
|
|
457
|
-
|
|
458
|
-
console.log('[HttpService] CSRF fetch failed with status:', response.status);
|
|
454
|
+
this.logger.debug('CSRF fetch failed with status:', response.status);
|
|
459
455
|
this.logger.warn('Failed to fetch CSRF token:', response.status);
|
|
460
456
|
}
|
|
461
457
|
catch (error) {
|
|
462
|
-
|
|
463
|
-
console.log('[HttpService] CSRF fetch error:', error);
|
|
458
|
+
this.logger.debug('CSRF fetch error:', error);
|
|
464
459
|
this.logger.warn('CSRF token fetch error:', error);
|
|
465
460
|
}
|
|
466
461
|
// Wait before retry (500ms)
|
|
@@ -45,7 +45,7 @@ async function initSecureStore() {
|
|
|
45
45
|
try {
|
|
46
46
|
// Variable indirection prevents bundlers (Vite, webpack) from statically resolving this
|
|
47
47
|
const moduleName = 'expo-secure-store';
|
|
48
|
-
SecureStore = await import(moduleName);
|
|
48
|
+
SecureStore = await import(/* @vite-ignore */ moduleName);
|
|
49
49
|
}
|
|
50
50
|
catch (error) {
|
|
51
51
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -68,7 +68,7 @@ async function initExpoCrypto() {
|
|
|
68
68
|
if (!ExpoCrypto) {
|
|
69
69
|
// Variable indirection prevents bundlers (Vite, webpack) from statically resolving this
|
|
70
70
|
const moduleName = 'expo-crypto';
|
|
71
|
-
ExpoCrypto = await import(moduleName);
|
|
71
|
+
ExpoCrypto = await import(/* @vite-ignore */ moduleName);
|
|
72
72
|
}
|
|
73
73
|
return ExpoCrypto;
|
|
74
74
|
}
|
|
@@ -94,7 +94,7 @@ async function getSecureRandomBytes(length) {
|
|
|
94
94
|
// Variable indirection prevents bundlers (Vite, webpack) from statically resolving this
|
|
95
95
|
try {
|
|
96
96
|
const cryptoModuleName = 'crypto';
|
|
97
|
-
const nodeCrypto = await import(cryptoModuleName);
|
|
97
|
+
const nodeCrypto = await import(/* @vite-ignore */ cryptoModuleName);
|
|
98
98
|
return new Uint8Array(nodeCrypto.randomBytes(length));
|
|
99
99
|
}
|
|
100
100
|
catch (error) {
|
|
@@ -40,7 +40,7 @@ function startExpoCryptoLoad() {
|
|
|
40
40
|
expoCryptoLoadPromise = (async () => {
|
|
41
41
|
try {
|
|
42
42
|
const moduleName = 'expo-crypto';
|
|
43
|
-
expoCryptoModule = await import(moduleName);
|
|
43
|
+
expoCryptoModule = await import(/* @vite-ignore */ moduleName);
|
|
44
44
|
}
|
|
45
45
|
catch {
|
|
46
46
|
// expo-crypto not available — expected in non-RN environments
|
|
@@ -8,20 +8,24 @@ import _cjs_elliptic from 'elliptic';
|
|
|
8
8
|
const { ec: EC } = _cjs_elliptic;
|
|
9
9
|
import { KeyManager } from './keyManager.js';
|
|
10
10
|
import { isReactNative, isNodeJS } from '../utils/platform.js';
|
|
11
|
-
// Lazy
|
|
11
|
+
// Lazy imports for platform-specific crypto
|
|
12
12
|
let ExpoCrypto = null;
|
|
13
|
+
let NodeCrypto = null;
|
|
13
14
|
const ec = new EC('secp256k1');
|
|
14
|
-
/**
|
|
15
|
-
* Initialize expo-crypto module
|
|
16
|
-
*/
|
|
17
15
|
async function initExpoCrypto() {
|
|
18
16
|
if (!ExpoCrypto) {
|
|
19
|
-
// Variable indirection prevents bundlers (Vite, webpack) from statically resolving this
|
|
20
17
|
const moduleName = 'expo-crypto';
|
|
21
|
-
ExpoCrypto = await import(moduleName);
|
|
18
|
+
ExpoCrypto = await import(/* @vite-ignore */ moduleName);
|
|
22
19
|
}
|
|
23
20
|
return ExpoCrypto;
|
|
24
21
|
}
|
|
22
|
+
async function initNodeCrypto() {
|
|
23
|
+
if (!NodeCrypto) {
|
|
24
|
+
const moduleName = 'crypto';
|
|
25
|
+
NodeCrypto = await import(/* @vite-ignore */ moduleName);
|
|
26
|
+
}
|
|
27
|
+
return NodeCrypto;
|
|
28
|
+
}
|
|
25
29
|
/**
|
|
26
30
|
* Compute SHA-256 hash of a string
|
|
27
31
|
*/
|
|
@@ -31,10 +35,9 @@ async function sha256(message) {
|
|
|
31
35
|
const Crypto = await initExpoCrypto();
|
|
32
36
|
return Crypto.digestStringAsync(Crypto.CryptoDigestAlgorithm.SHA256, message);
|
|
33
37
|
}
|
|
34
|
-
// In Node.js, use Node's crypto module
|
|
35
38
|
if (isNodeJS()) {
|
|
36
39
|
try {
|
|
37
|
-
const nodeCrypto = await
|
|
40
|
+
const nodeCrypto = await initNodeCrypto();
|
|
38
41
|
return nodeCrypto.createHash('sha256').update(message).digest('hex');
|
|
39
42
|
}
|
|
40
43
|
catch {
|
|
@@ -62,12 +65,9 @@ export class SignatureService {
|
|
|
62
65
|
.map((b) => b.toString(16).padStart(2, '0'))
|
|
63
66
|
.join('');
|
|
64
67
|
}
|
|
65
|
-
// In Node.js, use Node's crypto module
|
|
66
|
-
// Variable indirection prevents bundlers (Vite, webpack) from statically resolving this
|
|
67
68
|
if (isNodeJS()) {
|
|
68
69
|
try {
|
|
69
|
-
const
|
|
70
|
-
const nodeCrypto = await import(cryptoModuleName);
|
|
70
|
+
const nodeCrypto = await initNodeCrypto();
|
|
71
71
|
return nodeCrypto.randomBytes(32).toString('hex');
|
|
72
72
|
}
|
|
73
73
|
catch {
|
|
@@ -17,7 +17,7 @@ export function OxyServicesLanguageMixin(Base) {
|
|
|
17
17
|
try {
|
|
18
18
|
// Variable indirection prevents bundlers (Vite, webpack) from statically resolving this
|
|
19
19
|
const moduleName = '@react-native-async-storage/async-storage';
|
|
20
|
-
const asyncStorageModule = await import(moduleName);
|
|
20
|
+
const asyncStorageModule = await import(/* @vite-ignore */ moduleName);
|
|
21
21
|
const storage = asyncStorageModule.default;
|
|
22
22
|
return {
|
|
23
23
|
getItem: storage.getItem.bind(storage),
|
|
@@ -18,6 +18,17 @@ export function OxyServicesUserMixin(Base) {
|
|
|
18
18
|
throw this.handleError(error);
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Lightweight username lookup for login flows.
|
|
23
|
+
* Returns minimal public info: exists, color, avatar, displayName.
|
|
24
|
+
* Faster than getProfileByUsername — no stats, no formatting.
|
|
25
|
+
*/
|
|
26
|
+
async lookupUsername(username) {
|
|
27
|
+
return await this.makeRequest('GET', `/auth/lookup/${encodeURIComponent(username)}`, undefined, {
|
|
28
|
+
cache: true,
|
|
29
|
+
cacheTTL: 60 * 1000, // 1 minute cache
|
|
30
|
+
});
|
|
31
|
+
}
|
|
21
32
|
/**
|
|
22
33
|
* Search user profiles
|
|
23
34
|
*/
|
|
@@ -30,18 +30,47 @@ export async function parallelWithErrorHandling(operations, errorHandler) {
|
|
|
30
30
|
const results = await Promise.allSettled(operations.map((op, index) => withErrorHandling(op, error => errorHandler?.(error, index))));
|
|
31
31
|
return results.map(result => result.status === 'fulfilled' ? result.value : null);
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Extract an HTTP status code from an error value, tolerating both the
|
|
35
|
+
* axios-style nested shape (`error.response.status`) and the flat shape
|
|
36
|
+
* produced by {@link handleHttpError} / fetch-based clients (`error.status`).
|
|
37
|
+
*
|
|
38
|
+
* Centralising this lookup prevents retry predicates from silently falling
|
|
39
|
+
* through when one of the two shapes is missing, which previously caused
|
|
40
|
+
* @oxyhq/core to retry 4xx responses and turn sub-10ms failures into
|
|
41
|
+
* multi-second stalls for every missing-resource lookup.
|
|
42
|
+
*/
|
|
43
|
+
function extractHttpStatus(error) {
|
|
44
|
+
if (!error || typeof error !== 'object')
|
|
45
|
+
return undefined;
|
|
46
|
+
const candidate = error;
|
|
47
|
+
const flat = candidate.status;
|
|
48
|
+
if (typeof flat === 'number' && Number.isFinite(flat))
|
|
49
|
+
return flat;
|
|
50
|
+
const nested = candidate.response?.status;
|
|
51
|
+
if (typeof nested === 'number' && Number.isFinite(nested))
|
|
52
|
+
return nested;
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
33
55
|
/**
|
|
34
56
|
* Retry an async operation with exponential backoff
|
|
35
57
|
*
|
|
36
|
-
* By default, does not retry on 4xx errors (client errors).
|
|
37
|
-
*
|
|
58
|
+
* By default, does not retry on 4xx errors (client errors). The default
|
|
59
|
+
* predicate accepts both the axios-style `error.response.status` and the
|
|
60
|
+
* flat `error.status` shape produced by {@link handleHttpError}, so callers
|
|
61
|
+
* never accidentally retry a deterministic client failure.
|
|
62
|
+
*
|
|
63
|
+
* Use the `shouldRetry` callback to customize retry behavior.
|
|
38
64
|
*/
|
|
39
65
|
export async function retryAsync(operation, maxRetries = 3, baseDelay = 1000, shouldRetry) {
|
|
40
66
|
let lastError;
|
|
41
|
-
// Default shouldRetry: don't retry on 4xx errors
|
|
67
|
+
// Default shouldRetry: don't retry on 4xx errors (client errors).
|
|
68
|
+
// Checks BOTH `error.status` (flat shape from handleHttpError / fetch
|
|
69
|
+
// clients) AND `error.response.status` (axios-style shape) so neither
|
|
70
|
+
// representation can leak a client error into the retry loop.
|
|
42
71
|
const defaultShouldRetry = (error) => {
|
|
43
|
-
|
|
44
|
-
if (
|
|
72
|
+
const status = extractHttpStatus(error);
|
|
73
|
+
if (status !== undefined && status >= 400 && status < 500) {
|
|
45
74
|
return false;
|
|
46
75
|
}
|
|
47
76
|
return true;
|
|
@@ -17,7 +17,7 @@ export class DeviceManager {
|
|
|
17
17
|
try {
|
|
18
18
|
// Variable indirection prevents bundlers (Vite, webpack) from statically resolving this
|
|
19
19
|
const moduleName = '@react-native-async-storage/async-storage';
|
|
20
|
-
const asyncStorageModule = await import(moduleName);
|
|
20
|
+
const asyncStorageModule = await import(/* @vite-ignore */ moduleName);
|
|
21
21
|
const storage = asyncStorageModule.default;
|
|
22
22
|
return {
|
|
23
23
|
getItem: storage.getItem.bind(storage),
|