@oxyhq/core 10.2.0 → 11.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 (96) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/boot/sessionColdBoot.js +3 -3
  3. package/dist/cjs/crypto/keyManager.js +68 -69
  4. package/dist/cjs/crypto/signatureService.js +2 -2
  5. package/dist/cjs/index.js +13 -21
  6. package/dist/cjs/logger/index.js +149 -0
  7. package/dist/cjs/mixins/OxyServices.assets.js +4 -4
  8. package/dist/cjs/mixins/OxyServices.auth.js +2 -2
  9. package/dist/cjs/mixins/OxyServices.language.js +3 -5
  10. package/dist/cjs/mixins/OxyServices.privacy.js +2 -4
  11. package/dist/cjs/mixins/OxyServices.security.js +7 -11
  12. package/dist/cjs/mixins/OxyServices.topics.js +25 -15
  13. package/dist/cjs/mixins/OxyServices.user.js +4 -4
  14. package/dist/cjs/mixins/OxyServices.utility.js +18 -18
  15. package/dist/cjs/session/SessionClient.js +17 -17
  16. package/dist/cjs/session/accountDialogController.js +12 -12
  17. package/dist/cjs/session/authStateStore.js +5 -5
  18. package/dist/cjs/session/refresh.js +4 -4
  19. package/dist/cjs/session/socketLoader.js +2 -2
  20. package/dist/cjs/utils/asyncUtils.js +2 -2
  21. package/dist/cjs/utils/avatarUtils.js +2 -1
  22. package/dist/cjs/utils/deviceManager.js +7 -5
  23. package/dist/cjs/utils/errorUtils.js +3 -3
  24. package/dist/cjs/utils/oauthPkce.js +3 -3
  25. package/dist/cjs/utils/requestUtils.js +1 -1
  26. package/dist/esm/.tsbuildinfo +1 -1
  27. package/dist/esm/boot/sessionColdBoot.js +1 -1
  28. package/dist/esm/crypto/keyManager.js +1 -2
  29. package/dist/esm/crypto/signatureService.js +1 -1
  30. package/dist/esm/index.js +2 -3
  31. package/dist/esm/logger/index.js +140 -0
  32. package/dist/esm/mixins/OxyServices.assets.js +2 -2
  33. package/dist/esm/mixins/OxyServices.auth.js +1 -1
  34. package/dist/esm/mixins/OxyServices.language.js +3 -5
  35. package/dist/esm/mixins/OxyServices.privacy.js +2 -4
  36. package/dist/esm/mixins/OxyServices.security.js +7 -11
  37. package/dist/esm/mixins/OxyServices.topics.js +25 -15
  38. package/dist/esm/mixins/OxyServices.user.js +1 -1
  39. package/dist/esm/mixins/OxyServices.utility.js +1 -1
  40. package/dist/esm/session/SessionClient.js +1 -1
  41. package/dist/esm/session/accountDialogController.js +1 -1
  42. package/dist/esm/session/authStateStore.js +1 -1
  43. package/dist/esm/session/refresh.js +1 -1
  44. package/dist/esm/session/socketLoader.js +1 -1
  45. package/dist/esm/utils/asyncUtils.js +1 -1
  46. package/dist/esm/utils/avatarUtils.js +2 -1
  47. package/dist/esm/utils/deviceManager.js +7 -5
  48. package/dist/esm/utils/errorUtils.js +1 -1
  49. package/dist/esm/utils/oauthPkce.js +1 -1
  50. package/dist/esm/utils/requestUtils.js +1 -1
  51. package/dist/types/.tsbuildinfo +1 -1
  52. package/dist/types/index.d.ts +3 -4
  53. package/dist/types/logger/index.d.ts +104 -0
  54. package/dist/types/mixins/OxyServices.topics.d.ts +3 -3
  55. package/dist/types/models/Topic.d.ts +10 -0
  56. package/dist/types/utils/requestUtils.d.ts +1 -1
  57. package/package.json +16 -1
  58. package/src/boot/sessionColdBoot.ts +1 -1
  59. package/src/crypto/keyManager.ts +1 -2
  60. package/src/crypto/signatureService.ts +1 -1
  61. package/src/index.ts +17 -19
  62. package/src/logger/__tests__/logger.test.ts +207 -0
  63. package/src/logger/index.ts +217 -0
  64. package/src/mixins/OxyServices.assets.ts +3 -3
  65. package/src/mixins/OxyServices.auth.ts +1 -1
  66. package/src/mixins/OxyServices.language.ts +3 -5
  67. package/src/mixins/OxyServices.privacy.ts +2 -4
  68. package/src/mixins/OxyServices.security.ts +7 -11
  69. package/src/mixins/OxyServices.topics.ts +47 -17
  70. package/src/mixins/OxyServices.user.ts +1 -1
  71. package/src/mixins/OxyServices.utility.ts +1 -1
  72. package/src/mixins/__tests__/discoveryErrorHandling.test.ts +1 -1
  73. package/src/mixins/__tests__/topics.test.ts +156 -0
  74. package/src/models/Topic.ts +11 -0
  75. package/src/session/SessionClient.ts +1 -1
  76. package/src/session/__tests__/SessionClient.diagnostics.test.ts +1 -1
  77. package/src/session/__tests__/accountDialogController.test.ts +1 -1
  78. package/src/session/accountDialogController.ts +1 -1
  79. package/src/session/authStateStore.ts +1 -1
  80. package/src/session/refresh.ts +1 -1
  81. package/src/session/socketLoader.ts +1 -1
  82. package/src/utils/asyncUtils.ts +1 -1
  83. package/src/utils/avatarUtils.ts +3 -1
  84. package/src/utils/deviceManager.ts +8 -5
  85. package/src/utils/errorUtils.ts +1 -1
  86. package/src/utils/oauthPkce.ts +1 -1
  87. package/src/utils/requestUtils.ts +1 -1
  88. package/dist/cjs/shared/utils/debugUtils.js +0 -80
  89. package/dist/cjs/utils/loggerUtils.js +0 -126
  90. package/dist/esm/shared/utils/debugUtils.js +0 -72
  91. package/dist/esm/utils/loggerUtils.js +0 -115
  92. package/dist/types/shared/utils/debugUtils.d.ts +0 -48
  93. package/dist/types/utils/loggerUtils.d.ts +0 -48
  94. package/src/shared/utils/__tests__/debugUtils.test.ts +0 -55
  95. package/src/shared/utils/debugUtils.ts +0 -78
  96. package/src/utils/loggerUtils.ts +0 -153
@@ -57,7 +57,7 @@ export type { DeviceFingerprint, StoredDeviceInfo } from './utils/deviceManager'
57
57
  export type { OxyConfig, PrivacySettings, NotificationPreferences, UserPreferences, User, LoginResponse, Notification, Wallet, Transaction, BlockedUser, RestrictedUser, TransferFundsRequest, PurchaseRequest, WithdrawalRequest, TransactionResponse, PaginationInfo, SearchProfilesResponse, ApiError, PaymentMethod, PaymentRequest, PaymentResponse, AnalyticsData, FollowerDetails, ContentViewer, FileMetadata, FileUploadResponse, FileListResponse, FileUpdateRequest, FileDeleteResponse, RNFileDescriptor, AssetUploadInput, FileVisibility, AssetLink, AssetMetadata, AssetVariant, Asset, AssetInitRequest, AssetInitResponse, AssetCompleteRequest, AssetLinkRequest, AssetUnlinkRequest, AssetUrlResponse, AssetDeleteSummary, AssetUpdateVisibilityRequest, AssetUpdateVisibilityResponse, ServiceAssetMetadata, ServiceAssetMetadataBySha, AccountStorageCategoryUsage, AccountStorageUsageResponse, SecurityEventType, SecurityEventSeverity, SecurityActivity, SecurityActivityResponse, AssetUploadProgress, DeviceLinkedSession, DeviceLinkedSessionsResponse, DeviceLinkedSessionLogoutResponse, UpdateDeviceNameResponse, } from './models/interfaces';
58
58
  export { SECURITY_EVENT_SEVERITY_MAP } from './models/interfaces';
59
59
  export { TopicType, TopicSource } from './models/Topic';
60
- export type { TopicData, TopicTranslation } from './models/Topic';
60
+ export type { TopicData, TopicTranslation, TopicListResult } from './models/Topic';
61
61
  export { SUPPORTED_LANGUAGES, FALLBACK_LOCALE, getBaseLanguage, normalizeLocale, isSupportedLocale, getLanguageMetadata, getLanguageName, getNativeLanguageName, isRTLLocale, getUserLanguages, getPrimaryLanguage, } from './utils/languageUtils';
62
62
  export type { SupportedLanguage } from './utils/languageUtils';
63
63
  export { getPlatformOS, setPlatformOS, isWeb, isNative, isIOS, isAndroid, isWebBrowser, } from './utils/platform';
@@ -68,7 +68,6 @@ export type { ThemeValue } from './shared/utils/themeUtils';
68
68
  export { HttpStatus, getErrorStatus, getErrorMessage, isAlreadyRegisteredError, isUnauthorizedError, isForbiddenError, isNotFoundError, isRateLimitError, isServerError, isNetworkError, isRetryableError, } from './shared/utils/errorUtils';
69
69
  export { DEFAULT_CIRCUIT_BREAKER_CONFIG, createCircuitBreakerState, calculateBackoffInterval, recordFailure, recordSuccess, shouldAllowRequest, delay, withRetry, } from './shared/utils/networkUtils';
70
70
  export type { CircuitBreakerState, CircuitBreakerConfig } from './shared/utils/networkUtils';
71
- export { isDev, debugLog, debugWarn, debugError, createDebugLogger, } from './shared/utils/debugUtils';
72
71
  export { translate } from './i18n';
73
72
  export { buildSearchParams, buildUrl, buildPaginationParams, safeJsonParse, } from './utils/apiUtils';
74
73
  export type { PaginationParams, ApiResponse, ErrorResponse, } from './utils/apiUtils';
@@ -76,8 +75,8 @@ export { ErrorCodes, createApiError, handleHttpError, validateRequiredFields, }
76
75
  export { retryAsync } from './utils/asyncUtils';
77
76
  export { EMAIL_REGEX, USERNAME_REGEX, PASSWORD_REGEX, isValidEmail, isValidUsername, isValidPassword, isValidDisplayName, isRequiredString, isRequiredNumber, isRequiredBoolean, isValidArray, isValidObject, isValidUUID, isValidURL, isValidDate, isValidFileSize, isValidFileType, sanitizeString, sanitizeHTML, isValidObjectId, validateAndSanitizeUserInput, } from './utils/validationUtils';
78
77
  export { normalizeInlineText, normalizeMultilineText, } from './utils/textNormalization';
79
- export { logger, LogLevel, logAuth, logApi, logSession, logUser, logDevice, logPayment, logPerformance, } from './utils/loggerUtils';
80
- export type { LogContext } from './utils/loggerUtils';
78
+ export { logger, createLogger, configureLogger, getLoggerConfig, resetLoggerConfig, consoleSink, isDev, } from './logger';
79
+ export type { Logger, LogLevel, EmittableLogLevel, LogContext, LogEntry, LogSink, LoggerConfig, } from './logger';
81
80
  export { updateAvatarVisibility } from './utils/avatarUtils';
82
81
  export { buildAccountsArray, createQuickAccount, getAccountDisplayName, getAccountFallbackHandle, formatPublicKeyHandle, getAccountColor, } from './utils/accountUtils';
83
82
  export type { QuickAccount, DisplayNameUserShape } from './utils/accountUtils';
@@ -0,0 +1,104 @@
1
+ /**
2
+ * @oxyhq/core/logger — the ecosystem-wide logging chokepoint.
3
+ *
4
+ * A tiny, dependency-free, universal logger that works unchanged in React
5
+ * Native, browsers, Node, and Bun. Every Oxy app and package should log
6
+ * through here instead of calling `console.*` directly, so that level,
7
+ * formatting, and transport are controlled in ONE place.
8
+ *
9
+ * Design goals:
10
+ * - Zero dependencies. No Node-only imports, no `process.stdout` assumptions.
11
+ * - Four levels (`debug` | `info` | `warn` | `error`) plus `silent`.
12
+ * - Namespaced child loggers via `createLogger('mention:feed')` / `.child()`.
13
+ * - Structured context objects carried through to the sink.
14
+ * - A single pluggable sink so backends can pipe to pino / CloudWatch / etc.
15
+ * without touching any call site.
16
+ * - Debug is off in production by default (`__DEV__` on RN, `NODE_ENV`
17
+ * elsewhere); override globally with `configureLogger({ level })`.
18
+ *
19
+ * @module logger
20
+ */
21
+ /** Ordered severity levels. `silent` disables all output. */
22
+ export type LogLevel = 'silent' | 'error' | 'warn' | 'info' | 'debug';
23
+ /** The levels that actually emit an entry (everything but `silent`). */
24
+ export type EmittableLogLevel = Exclude<LogLevel, 'silent'>;
25
+ /**
26
+ * Structured, JSON-friendly context attached to a log line. The well-known
27
+ * keys are optional hints; arbitrary keys are allowed so callers can attach
28
+ * whatever structured fields their sink cares about.
29
+ */
30
+ export interface LogContext {
31
+ component?: string;
32
+ method?: string;
33
+ userId?: string;
34
+ sessionId?: string;
35
+ requestId?: string;
36
+ [key: string]: unknown;
37
+ }
38
+ /** A single normalized log record handed to the active sink. */
39
+ export interface LogEntry {
40
+ level: EmittableLogLevel;
41
+ message: string;
42
+ /** Colon-joined namespace, e.g. `mention:feed`. Absent for the root logger. */
43
+ namespace?: string;
44
+ /** Merged structured context (logger base context + per-call context). */
45
+ context?: LogContext;
46
+ /** The error value passed to `.error(message, error, …)`, if any. */
47
+ error?: unknown;
48
+ /** Extra variadic args passed after the context. */
49
+ args: unknown[];
50
+ /** ISO-8601 timestamp of when the entry was created. */
51
+ timestamp: string;
52
+ }
53
+ /** A transport that receives every emitted (level-passing) entry. */
54
+ export type LogSink = (entry: LogEntry) => void;
55
+ /** Global logger configuration. */
56
+ export interface LoggerConfig {
57
+ level: LogLevel;
58
+ sink: LogSink;
59
+ }
60
+ /** A namespaced logger. Instances are cheap; derive children with `.child()`. */
61
+ export interface Logger {
62
+ /** The logger's colon-joined namespace, if any. */
63
+ readonly namespace?: string;
64
+ debug(message: string, context?: LogContext, ...args: unknown[]): void;
65
+ info(message: string, context?: LogContext, ...args: unknown[]): void;
66
+ warn(message: string, context?: LogContext, ...args: unknown[]): void;
67
+ error(message: string, error?: unknown, context?: LogContext, ...args: unknown[]): void;
68
+ /** Derive a child logger with an extended namespace and merged base context. */
69
+ child(namespace: string, context?: LogContext): Logger;
70
+ }
71
+ /**
72
+ * True in development. Uses React Native's `__DEV__` when present, otherwise
73
+ * falls back to `process.env.NODE_ENV === 'development'`. Never throws.
74
+ */
75
+ export declare function isDev(): boolean;
76
+ /**
77
+ * Default sink: routes each entry to the matching `console` method. `info` and
78
+ * `debug` go to `console.log` (RN/browser-safe — `console.debug`/`console.info`
79
+ * are inconsistently surfaced across runtimes).
80
+ */
81
+ export declare const consoleSink: LogSink;
82
+ /**
83
+ * Update the global logger configuration. Affects every logger instance
84
+ * (root and children) immediately. Pass `{ level }` to gate output and/or
85
+ * `{ sink }` to redirect transport (pino, CloudWatch, a test capture, …).
86
+ */
87
+ export declare function configureLogger(partial: Partial<LoggerConfig>): void;
88
+ /** Read a snapshot of the current global configuration. */
89
+ export declare function getLoggerConfig(): Readonly<LoggerConfig>;
90
+ /** Restore the default level (env-derived) and the console sink. */
91
+ export declare function resetLoggerConfig(): void;
92
+ /** The shared root logger. Prefer a namespaced `createLogger(...)` in modules. */
93
+ export declare const logger: Logger;
94
+ /**
95
+ * Create a namespaced logger.
96
+ *
97
+ * @example
98
+ * ```ts
99
+ * const log = createLogger('mention:feed');
100
+ * log.info('loaded', { count: 20 });
101
+ * const sub = log.child('prefetch'); // namespace → 'mention:feed:prefetch'
102
+ * ```
103
+ */
104
+ export declare function createLogger(namespace?: string, context?: LogContext): Logger;
@@ -4,7 +4,7 @@
4
4
  * Provides methods for topic discovery and management
5
5
  */
6
6
  import type { OxyServicesBase } from '../OxyServices.base';
7
- import type { TopicData, TopicTranslation } from '../models/Topic';
7
+ import type { TopicData, TopicListResult, TopicTranslation } from '../models/Topic';
8
8
  export declare function OxyServicesTopicsMixin<T extends typeof OxyServicesBase>(Base: T): {
9
9
  new (...args: any[]): {
10
10
  /**
@@ -23,7 +23,7 @@ export declare function OxyServicesTopicsMixin<T extends typeof OxyServicesBase>
23
23
  /**
24
24
  * List topics with optional filters
25
25
  * @param options - Filter and pagination options
26
- * @returns List of topics
26
+ * @returns Paginated topics envelope (`topics` plus `total`/`limit`/`offset`)
27
27
  */
28
28
  listTopics(options?: {
29
29
  type?: string;
@@ -31,7 +31,7 @@ export declare function OxyServicesTopicsMixin<T extends typeof OxyServicesBase>
31
31
  limit?: number;
32
32
  offset?: number;
33
33
  locale?: string;
34
- }): Promise<TopicData[]>;
34
+ }): Promise<TopicListResult>;
35
35
  /**
36
36
  * Get a single topic by slug
37
37
  * @param slug - Topic slug
@@ -30,3 +30,13 @@ export interface TopicData {
30
30
  createdAt: string;
31
31
  updatedAt: string;
32
32
  }
33
+ /**
34
+ * Paginated result of {@link OxyServices.listTopics}. Mirrors the `GET /topics`
35
+ * envelope so callers keep access to `total`/`offset` for pagination.
36
+ */
37
+ export interface TopicListResult {
38
+ topics: TopicData[];
39
+ total: number;
40
+ limit: number;
41
+ offset: number;
42
+ }
@@ -93,7 +93,7 @@ export type LogLevel = 'none' | 'error' | 'warn' | 'info' | 'debug';
93
93
  * Simple logger with level support
94
94
  *
95
95
  * Lightweight logger for HTTP clients and utilities.
96
- * For more advanced logging, use loggerUtils.ts
96
+ * For structured, namespaced, level-gated logging, use `@oxyhq/core/logger`.
97
97
  *
98
98
  * @example
99
99
  * ```typescript
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/core",
3
- "version": "10.2.0",
3
+ "version": "11.0.0",
4
4
  "description": "OxyHQ SDK Foundation — API client, authentication, cryptographic identity, and shared utilities",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -9,6 +9,9 @@
9
9
  "*": {
10
10
  "server": [
11
11
  "dist/types/server/index.d.ts"
12
+ ],
13
+ "logger": [
14
+ "dist/types/logger/index.d.ts"
12
15
  ]
13
16
  }
14
17
  },
@@ -41,6 +44,18 @@
41
44
  },
42
45
  "default": "./dist/esm/server/index.js"
43
46
  },
47
+ "./logger": {
48
+ "react-native": "./dist/esm/logger/index.js",
49
+ "import": {
50
+ "types": "./dist/types/logger/index.d.ts",
51
+ "default": "./dist/esm/logger/index.js"
52
+ },
53
+ "require": {
54
+ "types": "./dist/types/logger/index.d.ts",
55
+ "default": "./dist/cjs/logger/index.js"
56
+ },
57
+ "default": "./dist/esm/logger/index.js"
58
+ },
44
59
  "./package.json": "./package.json"
45
60
  },
46
61
  "files": [
@@ -22,7 +22,7 @@
22
22
  */
23
23
  import { runColdBoot, type ColdBootOutcome, type ColdBootStep } from '../utils/coldBoot';
24
24
  import { isNative as detectNative } from '../utils/platform';
25
- import { logger } from '../utils/loggerUtils';
25
+ import { logger } from '../logger';
26
26
  import { TOKEN_REFRESH_LEAD_MS, refreshDeviceSecretArm } from '../session/refresh';
27
27
  import type { OxyServices } from '../OxyServices';
28
28
  import type { AuthStateStore } from '../session/authStateStore';
@@ -9,8 +9,7 @@ import { ec as EC } from 'elliptic';
9
9
  import type { ECKeyPair } from 'elliptic';
10
10
  import { isWeb, isIOS, isAndroid } from '../utils/platform';
11
11
  import { type ExpoCryptoLike, type ExpoSecureStoreLike, isReactNative, isNodeJS, loadExpoCrypto, loadNodeCrypto, loadSecureStore, loadSharedIdentityBridge } from '@oxyhq/protocol';
12
- import { logger } from '../utils/loggerUtils';
13
- import { isDev } from '../shared/utils/debugUtils';
12
+ import { isDev, logger } from '../logger';
14
13
 
15
14
  /**
16
15
  * Options for expo-secure-store calls made by KeyManager.
@@ -20,7 +20,7 @@ import {
20
20
  isNodeJS,
21
21
  } from '@oxyhq/protocol';
22
22
  import { KeyManager } from './keyManager';
23
- import { logger } from '../utils/loggerUtils';
23
+ import { logger } from '../logger';
24
24
 
25
25
  export interface SignedMessage {
26
26
  message: string;
package/src/index.ts CHANGED
@@ -317,7 +317,7 @@ export { SECURITY_EVENT_SEVERITY_MAP } from './models/interfaces';
317
317
 
318
318
  // Topic enums + type
319
319
  export { TopicType, TopicSource } from './models/Topic';
320
- export type { TopicData, TopicTranslation } from './models/Topic';
320
+ export type { TopicData, TopicTranslation, TopicListResult } from './models/Topic';
321
321
 
322
322
  // ---------------------------------------------------------------------------
323
323
  // Languages
@@ -402,14 +402,6 @@ export {
402
402
  } from './shared/utils/networkUtils';
403
403
  export type { CircuitBreakerState, CircuitBreakerConfig } from './shared/utils/networkUtils';
404
404
 
405
- export {
406
- isDev,
407
- debugLog,
408
- debugWarn,
409
- debugError,
410
- createDebugLogger,
411
- } from './shared/utils/debugUtils';
412
-
413
405
  // ---------------------------------------------------------------------------
414
406
  // i18n
415
407
  // ---------------------------------------------------------------------------
@@ -475,20 +467,26 @@ export {
475
467
  } from './utils/textNormalization';
476
468
 
477
469
  // ---------------------------------------------------------------------------
478
- // Logging
470
+ // Logging — the ecosystem-wide chokepoint (also at subpath `@oxyhq/core/logger`)
479
471
  // ---------------------------------------------------------------------------
480
472
  export {
481
473
  logger,
474
+ createLogger,
475
+ configureLogger,
476
+ getLoggerConfig,
477
+ resetLoggerConfig,
478
+ consoleSink,
479
+ isDev,
480
+ } from './logger';
481
+ export type {
482
+ Logger,
482
483
  LogLevel,
483
- logAuth,
484
- logApi,
485
- logSession,
486
- logUser,
487
- logDevice,
488
- logPayment,
489
- logPerformance,
490
- } from './utils/loggerUtils';
491
- export type { LogContext } from './utils/loggerUtils';
484
+ EmittableLogLevel,
485
+ LogContext,
486
+ LogEntry,
487
+ LogSink,
488
+ LoggerConfig,
489
+ } from './logger';
492
490
 
493
491
  // ---------------------------------------------------------------------------
494
492
  // Avatars
@@ -0,0 +1,207 @@
1
+ import {
2
+ configureLogger,
3
+ consoleSink,
4
+ createLogger,
5
+ getLoggerConfig,
6
+ isDev,
7
+ logger,
8
+ resetLoggerConfig,
9
+ type LogEntry,
10
+ } from '../index';
11
+
12
+ /** Install a capturing sink and return the collected entries. */
13
+ function captureEntries(): LogEntry[] {
14
+ const entries: LogEntry[] = [];
15
+ configureLogger({ sink: (entry) => entries.push(entry) });
16
+ return entries;
17
+ }
18
+
19
+ describe('logger', () => {
20
+ afterEach(() => {
21
+ resetLoggerConfig();
22
+ });
23
+
24
+ describe('level gating', () => {
25
+ it('emits only levels at or below the configured level', () => {
26
+ const entries = captureEntries();
27
+ configureLogger({ level: 'warn' });
28
+
29
+ logger.debug('d');
30
+ logger.info('i');
31
+ logger.warn('w');
32
+ logger.error('e');
33
+
34
+ expect(entries.map((entry) => entry.level)).toEqual(['warn', 'error']);
35
+ });
36
+
37
+ it('emits everything at debug level', () => {
38
+ const entries = captureEntries();
39
+ configureLogger({ level: 'debug' });
40
+
41
+ logger.debug('d');
42
+ logger.info('i');
43
+ logger.warn('w');
44
+ logger.error('e');
45
+
46
+ expect(entries.map((entry) => entry.level)).toEqual(['debug', 'info', 'warn', 'error']);
47
+ });
48
+
49
+ it('emits nothing at silent level', () => {
50
+ const entries = captureEntries();
51
+ configureLogger({ level: 'silent' });
52
+
53
+ logger.debug('d');
54
+ logger.info('i');
55
+ logger.warn('w');
56
+ logger.error('e');
57
+
58
+ expect(entries).toHaveLength(0);
59
+ });
60
+ });
61
+
62
+ describe('namespacing', () => {
63
+ it('root logger has no namespace', () => {
64
+ const entries = captureEntries();
65
+ configureLogger({ level: 'debug' });
66
+
67
+ logger.info('hello');
68
+
69
+ expect(entries[0].namespace).toBeUndefined();
70
+ });
71
+
72
+ it('createLogger sets the namespace', () => {
73
+ const entries = captureEntries();
74
+ configureLogger({ level: 'debug' });
75
+
76
+ createLogger('mention:feed').info('loaded');
77
+
78
+ expect(entries[0].namespace).toBe('mention:feed');
79
+ });
80
+
81
+ it('child() colon-joins nested namespaces', () => {
82
+ const entries = captureEntries();
83
+ configureLogger({ level: 'debug' });
84
+
85
+ createLogger('mention:feed').child('prefetch').info('done');
86
+
87
+ expect(entries[0].namespace).toBe('mention:feed:prefetch');
88
+ });
89
+ });
90
+
91
+ describe('structured context', () => {
92
+ it('passes per-call context through to the sink', () => {
93
+ const entries = captureEntries();
94
+ configureLogger({ level: 'debug' });
95
+
96
+ logger.info('hi', { userId: 'u1', requestId: 'r1' });
97
+
98
+ expect(entries[0].context).toEqual({ userId: 'u1', requestId: 'r1' });
99
+ });
100
+
101
+ it('merges base context with per-call context (per-call wins)', () => {
102
+ const entries = captureEntries();
103
+ configureLogger({ level: 'debug' });
104
+
105
+ createLogger('svc', { component: 'Svc', userId: 'base' }).warn('x', { userId: 'call', method: 'm' });
106
+
107
+ expect(entries[0].context).toEqual({ component: 'Svc', userId: 'call', method: 'm' });
108
+ });
109
+
110
+ it('captures the error value on error()', () => {
111
+ const entries = captureEntries();
112
+ configureLogger({ level: 'debug' });
113
+ const boom = new Error('boom');
114
+
115
+ logger.error('failed', boom, { component: 'Svc' });
116
+
117
+ expect(entries[0].error).toBe(boom);
118
+ expect(entries[0].context).toEqual({ component: 'Svc' });
119
+ });
120
+
121
+ it('forwards trailing variadic args', () => {
122
+ const entries = captureEntries();
123
+ configureLogger({ level: 'debug' });
124
+
125
+ logger.warn('x', { component: 'Svc' }, 1, 'two');
126
+
127
+ expect(entries[0].args).toEqual([1, 'two']);
128
+ });
129
+ });
130
+
131
+ describe('sink override', () => {
132
+ it('routes all output through the configured sink', () => {
133
+ const sink = jest.fn();
134
+ configureLogger({ level: 'debug', sink });
135
+
136
+ logger.info('routed');
137
+
138
+ expect(sink).toHaveBeenCalledTimes(1);
139
+ expect(sink.mock.calls[0][0]).toMatchObject({ level: 'info', message: 'routed' });
140
+ });
141
+
142
+ it('resetLoggerConfig restores the console sink', () => {
143
+ configureLogger({ sink: jest.fn() });
144
+ resetLoggerConfig();
145
+ expect(getLoggerConfig().sink).toBe(consoleSink);
146
+ });
147
+ });
148
+
149
+ describe('consoleSink', () => {
150
+ it('routes error to console.error and warn to console.warn', () => {
151
+ const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
152
+ const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
153
+ const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
154
+ const now = new Date().toISOString();
155
+
156
+ consoleSink({ level: 'error', message: 'e', args: [], timestamp: now });
157
+ consoleSink({ level: 'warn', message: 'w', args: [], timestamp: now });
158
+ consoleSink({ level: 'info', message: 'i', args: [], timestamp: now });
159
+
160
+ expect(errorSpy).toHaveBeenCalledTimes(1);
161
+ expect(warnSpy).toHaveBeenCalledTimes(1);
162
+ expect(logSpy).toHaveBeenCalledTimes(1);
163
+
164
+ errorSpy.mockRestore();
165
+ warnSpy.mockRestore();
166
+ logSpy.mockRestore();
167
+ });
168
+ });
169
+
170
+ describe('isDev', () => {
171
+ // `isDev` reads the RN-injected global `__DEV__` first; model it as an
172
+ // optional global so we can exercise the NODE_ENV fallback without `as any`.
173
+ const globalWithDev = globalThis as typeof globalThis & { __DEV__?: boolean };
174
+ const originalDev = globalWithDev.__DEV__;
175
+ const originalNodeEnv = process.env.NODE_ENV;
176
+
177
+ afterEach(() => {
178
+ // Setting to `undefined` makes `typeof __DEV__` read as 'undefined', the
179
+ // same signal `isDev` keys off — no need for the `delete` operator.
180
+ globalWithDev.__DEV__ = originalDev;
181
+ process.env.NODE_ENV = originalNodeEnv;
182
+ });
183
+
184
+ it('returns true when __DEV__ is true (React Native)', () => {
185
+ globalWithDev.__DEV__ = true;
186
+ expect(isDev()).toBe(true);
187
+ });
188
+
189
+ it('returns false when __DEV__ is false regardless of NODE_ENV', () => {
190
+ globalWithDev.__DEV__ = false;
191
+ process.env.NODE_ENV = 'development';
192
+ expect(isDev()).toBe(false);
193
+ });
194
+
195
+ it('falls back to NODE_ENV=development when __DEV__ is undefined', () => {
196
+ globalWithDev.__DEV__ = undefined;
197
+ process.env.NODE_ENV = 'development';
198
+ expect(isDev()).toBe(true);
199
+ });
200
+
201
+ it('returns false when NODE_ENV is production and __DEV__ is undefined', () => {
202
+ globalWithDev.__DEV__ = undefined;
203
+ process.env.NODE_ENV = 'production';
204
+ expect(isDev()).toBe(false);
205
+ });
206
+ });
207
+ });