@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.
@@ -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
- if (isStateChangingMethod && isDev()) {
192
- console.log('[HttpService] CSRF Debug:', {
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
- if (isDev())
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
- if (isDev())
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
- if (isDev())
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
- if (isDev())
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
- if (isDev())
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
- if (isDev())
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
- if (isDev())
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 import for expo-crypto
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 import('crypto');
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 cryptoModuleName = 'crypto';
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
- * Use shouldRetry callback to customize retry behavior.
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
- // Don't retry on 4xx errors (client errors)
44
- if (error?.response?.status >= 400 && error?.response?.status < 500) {
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),