@oxyhq/core 10.2.0 → 11.0.1

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 (103) 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 +17 -20
  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 +104 -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/cjs/utils/webauthnOrigin.js +51 -0
  27. package/dist/esm/.tsbuildinfo +1 -1
  28. package/dist/esm/boot/sessionColdBoot.js +1 -1
  29. package/dist/esm/crypto/keyManager.js +1 -2
  30. package/dist/esm/crypto/signatureService.js +1 -1
  31. package/dist/esm/index.js +6 -3
  32. package/dist/esm/logger/index.js +140 -0
  33. package/dist/esm/mixins/OxyServices.assets.js +2 -2
  34. package/dist/esm/mixins/OxyServices.auth.js +103 -1
  35. package/dist/esm/mixins/OxyServices.language.js +3 -5
  36. package/dist/esm/mixins/OxyServices.privacy.js +2 -4
  37. package/dist/esm/mixins/OxyServices.security.js +7 -11
  38. package/dist/esm/mixins/OxyServices.topics.js +25 -15
  39. package/dist/esm/mixins/OxyServices.user.js +1 -1
  40. package/dist/esm/mixins/OxyServices.utility.js +1 -1
  41. package/dist/esm/session/SessionClient.js +1 -1
  42. package/dist/esm/session/accountDialogController.js +1 -1
  43. package/dist/esm/session/authStateStore.js +1 -1
  44. package/dist/esm/session/refresh.js +1 -1
  45. package/dist/esm/session/socketLoader.js +1 -1
  46. package/dist/esm/utils/asyncUtils.js +1 -1
  47. package/dist/esm/utils/avatarUtils.js +2 -1
  48. package/dist/esm/utils/deviceManager.js +7 -5
  49. package/dist/esm/utils/errorUtils.js +1 -1
  50. package/dist/esm/utils/oauthPkce.js +1 -1
  51. package/dist/esm/utils/requestUtils.js +1 -1
  52. package/dist/esm/utils/webauthnOrigin.js +48 -0
  53. package/dist/types/.tsbuildinfo +1 -1
  54. package/dist/types/index.d.ts +4 -4
  55. package/dist/types/logger/index.d.ts +104 -0
  56. package/dist/types/mixins/OxyServices.auth.d.ts +58 -0
  57. package/dist/types/mixins/OxyServices.topics.d.ts +3 -3
  58. package/dist/types/models/Topic.d.ts +10 -0
  59. package/dist/types/utils/requestUtils.d.ts +1 -1
  60. package/dist/types/utils/webauthnOrigin.d.ts +32 -0
  61. package/package.json +17 -2
  62. package/src/boot/sessionColdBoot.ts +1 -1
  63. package/src/crypto/keyManager.ts +1 -2
  64. package/src/crypto/signatureService.ts +1 -1
  65. package/src/index.ts +22 -19
  66. package/src/logger/__tests__/logger.test.ts +207 -0
  67. package/src/logger/index.ts +217 -0
  68. package/src/mixins/OxyServices.assets.ts +3 -3
  69. package/src/mixins/OxyServices.auth.ts +134 -1
  70. package/src/mixins/OxyServices.language.ts +3 -5
  71. package/src/mixins/OxyServices.privacy.ts +2 -4
  72. package/src/mixins/OxyServices.security.ts +7 -11
  73. package/src/mixins/OxyServices.topics.ts +47 -17
  74. package/src/mixins/OxyServices.user.ts +1 -1
  75. package/src/mixins/OxyServices.utility.ts +1 -1
  76. package/src/mixins/__tests__/discoveryErrorHandling.test.ts +1 -1
  77. package/src/mixins/__tests__/topics.test.ts +156 -0
  78. package/src/mixins/__tests__/webauthnAuth.test.ts +206 -0
  79. package/src/models/Topic.ts +11 -0
  80. package/src/session/SessionClient.ts +1 -1
  81. package/src/session/__tests__/SessionClient.diagnostics.test.ts +1 -1
  82. package/src/session/__tests__/accountDialogController.test.ts +1 -1
  83. package/src/session/accountDialogController.ts +1 -1
  84. package/src/session/authStateStore.ts +1 -1
  85. package/src/session/refresh.ts +1 -1
  86. package/src/session/socketLoader.ts +1 -1
  87. package/src/utils/__tests__/webauthnOrigin.test.ts +83 -0
  88. package/src/utils/asyncUtils.ts +1 -1
  89. package/src/utils/avatarUtils.ts +3 -1
  90. package/src/utils/deviceManager.ts +8 -5
  91. package/src/utils/errorUtils.ts +1 -1
  92. package/src/utils/oauthPkce.ts +1 -1
  93. package/src/utils/requestUtils.ts +1 -1
  94. package/src/utils/webauthnOrigin.ts +52 -0
  95. package/dist/cjs/shared/utils/debugUtils.js +0 -80
  96. package/dist/cjs/utils/loggerUtils.js +0 -126
  97. package/dist/esm/shared/utils/debugUtils.js +0 -72
  98. package/dist/esm/utils/loggerUtils.js +0 -115
  99. package/dist/types/shared/utils/debugUtils.d.ts +0 -48
  100. package/dist/types/utils/loggerUtils.d.ts +0 -48
  101. package/src/shared/utils/__tests__/debugUtils.test.ts +0 -55
  102. package/src/shared/utils/debugUtils.ts +0 -78
  103. 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,13 +75,14 @@ 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';
84
83
  export { registrableApex } from './utils/registrableApex';
85
84
  export { CENTRAL_IDP_APEX } from './utils/authWebUrl';
85
+ export { isOxyRpOrigin } from './utils/webauthnOrigin';
86
86
  export { runColdBoot } from './utils/coldBoot';
87
87
  export type { ColdBootStep, ColdBootStepResult, ColdBootSession, ColdBootSkip, ColdBootOutcome, RunColdBootOptions, } from './utils/coldBoot';
88
88
  export { buildOAuthAuthorizeUrl, computeCodeChallenge, generateOAuthState, generatePkcePair, DEFAULT_OAUTH_SCOPE, OXY_AUTHORIZE_URL, OXY_OAUTH_STATE_STORAGE_KEY, OXY_OAUTH_CODE_VERIFIER_STORAGE_KEY, OXY_SILENT_OAUTH_ATTEMPTED_KEY, OXY_CROSS_ORIGIN_RESTORE_ATTEMPTED_KEY, normalizeOAuthRedirectUri, persistOAuthHandshake, readOAuthHandshake, clearOAuthHandshake, } from './utils/oauthPkce';
@@ -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;
@@ -494,6 +494,64 @@ export declare function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(B
494
494
  deviceName?: string;
495
495
  deviceId?: string;
496
496
  }): Promise<LoginSessionResult>;
497
+ /**
498
+ * Begin a WebAuthn / passkey REGISTRATION ceremony. Requests the
499
+ * `PublicKeyCredentialCreationOptions` the browser's `navigator.credentials
500
+ * .create()` (or `@simplewebauthn/browser`'s `startRegistration`) needs.
501
+ *
502
+ * With a bearer token planted this links a passkey to the signed-in account
503
+ * (`username` ignored); without one it is a prospective signup and `username`
504
+ * is the desired handle. The returned options are OPAQUE — Oxy does not own
505
+ * their shape (the browser / `@simplewebauthn` does), so they pass through
506
+ * as `unknown` for the caller to hand straight to the ceremony.
507
+ */
508
+ webauthnRegisterOptions(username?: string): Promise<unknown>;
509
+ /**
510
+ * Finish a WebAuthn / passkey REGISTRATION ceremony. Forwards the opaque
511
+ * browser `RegistrationResponseJSON` (`response`) alongside the Oxy envelope
512
+ * (desired `username` for signup + the device-session naming fields).
513
+ *
514
+ * Two server branches, disambiguated by the response shape:
515
+ * - **Signup** (no bearer): the account is created and a session minted —
516
+ * the response carries `sessionId`, is the SAME {@link LoginResult}
517
+ * contract as `POST /auth/verify`, and its access token is planted here.
518
+ * - **Link** (bearer present): the passkey is attached to the signed-in
519
+ * account and the server returns `{ success, message }` with no session,
520
+ * which is returned verbatim (no token planting).
521
+ */
522
+ webauthnRegisterVerify(response: unknown, envelope?: {
523
+ username?: string;
524
+ deviceName?: string;
525
+ deviceFingerprint?: string;
526
+ deviceId?: string;
527
+ }): Promise<{
528
+ success: true;
529
+ message: string;
530
+ } | LoginResult>;
531
+ /**
532
+ * Begin a WebAuthn / passkey AUTHENTICATION ceremony. Requests the
533
+ * `PublicKeyCredentialRequestOptions` the browser's `navigator.credentials
534
+ * .get()` (or `@simplewebauthn/browser`'s `startAuthentication`) needs.
535
+ *
536
+ * When `username` is present the server scopes `allowCredentials` to that
537
+ * user's passkeys (username-first); when omitted it returns an empty
538
+ * allow-list for the usernameless / discoverable-credential flow. The
539
+ * returned options are OPAQUE and pass through as `unknown`.
540
+ */
541
+ webauthnLoginOptions(username?: string): Promise<unknown>;
542
+ /**
543
+ * Finish a WebAuthn / passkey AUTHENTICATION ceremony. Forwards the opaque
544
+ * browser `AuthenticationResponseJSON` (`response`) alongside the
545
+ * device-session envelope. Resolves to the SAME {@link LoginResult} contract
546
+ * as `POST /auth/verify`; on the session arm the access token is planted
547
+ * immediately (mirroring {@link passwordSignIn}), and the response's
548
+ * `deviceId` + `deviceSecret` are the zero-cookie restore credential.
549
+ */
550
+ webauthnLoginVerify(response: unknown, envelope?: {
551
+ deviceName?: string;
552
+ deviceFingerprint?: string;
553
+ deviceId?: string;
554
+ }): Promise<LoginResult>;
497
555
  /**
498
556
  * Exchange an OAuth authorization code (returned to the RP redirect URI
499
557
  * after password sign-in at auth.oxy.so) for a device-first session.
@@ -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
@@ -0,0 +1,32 @@
1
+ /**
2
+ * WebAuthn relying-party origin guard (client side).
3
+ *
4
+ * The passkey ceremonies (`OxyServices.webauthn*`) are only meaningful when the
5
+ * page is served from a first-party Oxy web origin: a credential minted with
6
+ * `WEBAUTHN_RP_ID=oxy.so` can only be created/asserted from `oxy.so`, one of its
7
+ * subdomains, or a loopback dev server. This is the browser-side mirror of the
8
+ * server's `isOxyApexOrigin` (`packages/api/src/utils/origin.ts`), which forms
9
+ * the server's `expectedOrigin` allow-set — consumers use it to decide whether to
10
+ * even offer the passkey UI on the current page.
11
+ *
12
+ * It reads `globalThis.location` directly (no argument) because that is the only
13
+ * origin the browser will let a WebAuthn ceremony run against. On native / SSR /
14
+ * any environment without a DOM `location`, it returns `false` (there is no
15
+ * relying-party origin, so passkeys are not applicable).
16
+ */
17
+ /**
18
+ * True iff the current page's host is a first-party Oxy relying-party origin:
19
+ * `oxy.so`, any `*.oxy.so` subdomain, or a loopback dev host
20
+ * (`localhost` / `127.0.0.1` / `[::1]`).
21
+ *
22
+ * Fails closed: no `location` (native/SSR), a non-string/empty hostname, or a
23
+ * host that merely ends in the literal `oxy.so` without the dot boundary
24
+ * (`evil-oxy.so`, `oxy.so.evil.com`) all return `false`.
25
+ *
26
+ * @example
27
+ * isOxyRpOrigin() // true on https://accounts.oxy.so
28
+ * isOxyRpOrigin() // true on http://localhost:8081
29
+ * isOxyRpOrigin() // false on https://evil.com
30
+ * isOxyRpOrigin() // false in a React Native / SSR context (no location)
31
+ */
32
+ export declare function isOxyRpOrigin(): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/core",
3
- "version": "10.2.0",
3
+ "version": "11.0.1",
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": [
@@ -94,7 +109,7 @@
94
109
  }
95
110
  },
96
111
  "dependencies": {
97
- "@oxyhq/contracts": "^0.14.1",
112
+ "@oxyhq/contracts": "^0.14.2",
98
113
  "@oxyhq/protocol": "^0.1.5",
99
114
  "bip39": "^3.1.0",
100
115
  "buffer": "^6.0.3",
@@ -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
@@ -519,6 +517,11 @@ export type { QuickAccount, DisplayNameUserShape } from './utils/accountUtils';
519
517
  export { registrableApex } from './utils/registrableApex';
520
518
  export { CENTRAL_IDP_APEX } from './utils/authWebUrl';
521
519
 
520
+ // WebAuthn relying-party origin guard (client side). Mirrors the server's
521
+ // `isOxyApexOrigin` so consumers can decide whether to offer passkey UI on the
522
+ // current page (first-party Oxy origin / loopback only).
523
+ export { isOxyRpOrigin } from './utils/webauthnOrigin';
524
+
522
525
  export { runColdBoot } from './utils/coldBoot';
523
526
  export type {
524
527
  ColdBootStep,
@@ -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
+ });