@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
@@ -0,0 +1,217 @@
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
+
22
+ /* global __DEV__ */
23
+ declare const __DEV__: boolean | undefined;
24
+
25
+ /** Ordered severity levels. `silent` disables all output. */
26
+ export type LogLevel = 'silent' | 'error' | 'warn' | 'info' | 'debug';
27
+
28
+ /** The levels that actually emit an entry (everything but `silent`). */
29
+ export type EmittableLogLevel = Exclude<LogLevel, 'silent'>;
30
+
31
+ const LEVEL_WEIGHT: Record<LogLevel, number> = {
32
+ silent: 0,
33
+ error: 1,
34
+ warn: 2,
35
+ info: 3,
36
+ debug: 4,
37
+ };
38
+
39
+ /**
40
+ * Structured, JSON-friendly context attached to a log line. The well-known
41
+ * keys are optional hints; arbitrary keys are allowed so callers can attach
42
+ * whatever structured fields their sink cares about.
43
+ */
44
+ export interface LogContext {
45
+ component?: string;
46
+ method?: string;
47
+ userId?: string;
48
+ sessionId?: string;
49
+ requestId?: string;
50
+ [key: string]: unknown;
51
+ }
52
+
53
+ /** A single normalized log record handed to the active sink. */
54
+ export interface LogEntry {
55
+ level: EmittableLogLevel;
56
+ message: string;
57
+ /** Colon-joined namespace, e.g. `mention:feed`. Absent for the root logger. */
58
+ namespace?: string;
59
+ /** Merged structured context (logger base context + per-call context). */
60
+ context?: LogContext;
61
+ /** The error value passed to `.error(message, error, …)`, if any. */
62
+ error?: unknown;
63
+ /** Extra variadic args passed after the context. */
64
+ args: unknown[];
65
+ /** ISO-8601 timestamp of when the entry was created. */
66
+ timestamp: string;
67
+ }
68
+
69
+ /** A transport that receives every emitted (level-passing) entry. */
70
+ export type LogSink = (entry: LogEntry) => void;
71
+
72
+ /** Global logger configuration. */
73
+ export interface LoggerConfig {
74
+ level: LogLevel;
75
+ sink: LogSink;
76
+ }
77
+
78
+ /** A namespaced logger. Instances are cheap; derive children with `.child()`. */
79
+ export interface Logger {
80
+ /** The logger's colon-joined namespace, if any. */
81
+ readonly namespace?: string;
82
+ debug(message: string, context?: LogContext, ...args: unknown[]): void;
83
+ info(message: string, context?: LogContext, ...args: unknown[]): void;
84
+ warn(message: string, context?: LogContext, ...args: unknown[]): void;
85
+ error(message: string, error?: unknown, context?: LogContext, ...args: unknown[]): void;
86
+ /** Derive a child logger with an extended namespace and merged base context. */
87
+ child(namespace: string, context?: LogContext): Logger;
88
+ }
89
+
90
+ /**
91
+ * True in development. Uses React Native's `__DEV__` when present, otherwise
92
+ * falls back to `process.env.NODE_ENV === 'development'`. Never throws.
93
+ */
94
+ export function isDev(): boolean {
95
+ if (typeof __DEV__ !== 'undefined') return __DEV__ === true;
96
+ try {
97
+ return typeof process !== 'undefined' && process.env?.NODE_ENV === 'development';
98
+ } catch {
99
+ return false;
100
+ }
101
+ }
102
+
103
+ function formatPrefix(entry: LogEntry): string {
104
+ const parts = [entry.timestamp, entry.level.toUpperCase()];
105
+ if (entry.namespace) parts.push(`[${entry.namespace}]`);
106
+ return parts.join(' ');
107
+ }
108
+
109
+ /**
110
+ * Default sink: routes each entry to the matching `console` method. `info` and
111
+ * `debug` go to `console.log` (RN/browser-safe — `console.debug`/`console.info`
112
+ * are inconsistently surfaced across runtimes).
113
+ */
114
+ export const consoleSink: LogSink = (entry) => {
115
+ const extras: unknown[] = [];
116
+ if (entry.context && Object.keys(entry.context).length > 0) extras.push(entry.context);
117
+ if (entry.error !== undefined) extras.push(entry.error);
118
+ if (entry.args.length > 0) extras.push(...entry.args);
119
+
120
+ const line = `${formatPrefix(entry)} ${entry.message}`;
121
+ if (entry.level === 'error') console.error(line, ...extras);
122
+ else if (entry.level === 'warn') console.warn(line, ...extras);
123
+ else console.log(line, ...extras);
124
+ };
125
+
126
+ function defaultLevel(): LogLevel {
127
+ return isDev() ? 'debug' : 'info';
128
+ }
129
+
130
+ const config: LoggerConfig = {
131
+ level: defaultLevel(),
132
+ sink: consoleSink,
133
+ };
134
+
135
+ /**
136
+ * Update the global logger configuration. Affects every logger instance
137
+ * (root and children) immediately. Pass `{ level }` to gate output and/or
138
+ * `{ sink }` to redirect transport (pino, CloudWatch, a test capture, …).
139
+ */
140
+ export function configureLogger(partial: Partial<LoggerConfig>): void {
141
+ if (partial.level !== undefined) config.level = partial.level;
142
+ if (partial.sink !== undefined) config.sink = partial.sink;
143
+ }
144
+
145
+ /** Read a snapshot of the current global configuration. */
146
+ export function getLoggerConfig(): Readonly<LoggerConfig> {
147
+ return { level: config.level, sink: config.sink };
148
+ }
149
+
150
+ /** Restore the default level (env-derived) and the console sink. */
151
+ export function resetLoggerConfig(): void {
152
+ config.level = defaultLevel();
153
+ config.sink = consoleSink;
154
+ }
155
+
156
+ function mergeContext(base?: LogContext, extra?: LogContext): LogContext | undefined {
157
+ if (!base) return extra;
158
+ if (!extra) return base;
159
+ return { ...base, ...extra };
160
+ }
161
+
162
+ function emit(
163
+ level: EmittableLogLevel,
164
+ namespace: string | undefined,
165
+ baseContext: LogContext | undefined,
166
+ message: string,
167
+ error: unknown,
168
+ context: LogContext | undefined,
169
+ args: unknown[],
170
+ ): void {
171
+ if (LEVEL_WEIGHT[level] > LEVEL_WEIGHT[config.level]) return;
172
+ config.sink({
173
+ level,
174
+ message,
175
+ namespace,
176
+ context: mergeContext(baseContext, context),
177
+ error,
178
+ args,
179
+ timestamp: new Date().toISOString(),
180
+ });
181
+ }
182
+
183
+ function makeLogger(namespace: string | undefined, baseContext: LogContext | undefined): Logger {
184
+ return {
185
+ namespace,
186
+ debug: (message, context, ...args) =>
187
+ emit('debug', namespace, baseContext, message, undefined, context, args),
188
+ info: (message, context, ...args) =>
189
+ emit('info', namespace, baseContext, message, undefined, context, args),
190
+ warn: (message, context, ...args) =>
191
+ emit('warn', namespace, baseContext, message, undefined, context, args),
192
+ error: (message, error, context, ...args) =>
193
+ emit('error', namespace, baseContext, message, error, context, args),
194
+ child: (childNamespace, childContext) =>
195
+ makeLogger(
196
+ namespace ? `${namespace}:${childNamespace}` : childNamespace,
197
+ mergeContext(baseContext, childContext),
198
+ ),
199
+ };
200
+ }
201
+
202
+ /** The shared root logger. Prefer a namespaced `createLogger(...)` in modules. */
203
+ export const logger: Logger = makeLogger(undefined, undefined);
204
+
205
+ /**
206
+ * Create a namespaced logger.
207
+ *
208
+ * @example
209
+ * ```ts
210
+ * const log = createLogger('mention:feed');
211
+ * log.info('loaded', { count: 20 });
212
+ * const sub = log.child('prefetch'); // namespace → 'mention:feed:prefetch'
213
+ * ```
214
+ */
215
+ export function createLogger(namespace?: string, context?: LogContext): Logger {
216
+ return makeLogger(namespace, context);
217
+ }
@@ -1,7 +1,7 @@
1
1
  import type { AccountStorageUsageResponse, AssetUploadInput, AssetUrlResponse, AssetVariant, RNFileDescriptor, ServiceAssetMetadata, ServiceAssetMetadataBySha } from '../models/interfaces';
2
2
  import type { OxyServicesBase } from '../OxyServices.base';
3
3
  import { isReactNative } from '@oxyhq/protocol';
4
- import { logger } from '../utils/loggerUtils';
4
+ import { logger } from '../logger';
5
5
  import { extractErrorStatus } from '../utils/errorUtils';
6
6
 
7
7
  /**
@@ -428,8 +428,8 @@ export function OxyServicesAssetsMixin<T extends typeof OxyServicesBase>(Base: T
428
428
 
429
429
  return response;
430
430
  } catch (error) {
431
- console.error('File upload error:', error);
432
-
431
+ logger.error('File upload error', error, { component: 'OxyServices.assets' });
432
+
433
433
  let errorMessage = 'File upload failed';
434
434
 
435
435
  if (error instanceof Error) {
@@ -13,7 +13,7 @@ import { OxyAuthenticationError } from '../OxyServices.errors';
13
13
  import { KeyManager } from '../crypto/keyManager';
14
14
  import { SignatureService } from '../crypto/signatureService';
15
15
  import { loadNodeCrypto } from '@oxyhq/protocol';
16
- import { logger } from '../utils/loggerUtils';
16
+ import { logger } from '../logger';
17
17
  import { normalizeUserIdentity, normalizeUserIdentityOrNull } from '../utils/userIdentity';
18
18
 
19
19
  /**
@@ -1196,6 +1196,139 @@ export function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(Base: T)
1196
1196
  }
1197
1197
  }
1198
1198
 
1199
+ /**
1200
+ * Begin a WebAuthn / passkey REGISTRATION ceremony. Requests the
1201
+ * `PublicKeyCredentialCreationOptions` the browser's `navigator.credentials
1202
+ * .create()` (or `@simplewebauthn/browser`'s `startRegistration`) needs.
1203
+ *
1204
+ * With a bearer token planted this links a passkey to the signed-in account
1205
+ * (`username` ignored); without one it is a prospective signup and `username`
1206
+ * is the desired handle. The returned options are OPAQUE — Oxy does not own
1207
+ * their shape (the browser / `@simplewebauthn` does), so they pass through
1208
+ * as `unknown` for the caller to hand straight to the ceremony.
1209
+ */
1210
+ async webauthnRegisterOptions(username?: string): Promise<unknown> {
1211
+ try {
1212
+ return await this.makeRequest<unknown>(
1213
+ 'POST',
1214
+ '/auth/webauthn/register/options',
1215
+ { ...(username !== undefined ? { username } : {}) },
1216
+ { cache: false },
1217
+ );
1218
+ } catch (error) {
1219
+ throw this.handleError(error);
1220
+ }
1221
+ }
1222
+
1223
+ /**
1224
+ * Finish a WebAuthn / passkey REGISTRATION ceremony. Forwards the opaque
1225
+ * browser `RegistrationResponseJSON` (`response`) alongside the Oxy envelope
1226
+ * (desired `username` for signup + the device-session naming fields).
1227
+ *
1228
+ * Two server branches, disambiguated by the response shape:
1229
+ * - **Signup** (no bearer): the account is created and a session minted —
1230
+ * the response carries `sessionId`, is the SAME {@link LoginResult}
1231
+ * contract as `POST /auth/verify`, and its access token is planted here.
1232
+ * - **Link** (bearer present): the passkey is attached to the signed-in
1233
+ * account and the server returns `{ success, message }` with no session,
1234
+ * which is returned verbatim (no token planting).
1235
+ */
1236
+ async webauthnRegisterVerify(
1237
+ response: unknown,
1238
+ envelope: {
1239
+ username?: string;
1240
+ deviceName?: string;
1241
+ deviceFingerprint?: string;
1242
+ deviceId?: string;
1243
+ } = {},
1244
+ ): Promise<{ success: true; message: string } | LoginResult> {
1245
+ try {
1246
+ const res = await this.makeRequest<unknown>(
1247
+ 'POST',
1248
+ '/auth/webauthn/register/verify',
1249
+ { response, ...envelope },
1250
+ { cache: false },
1251
+ );
1252
+ if (res && typeof res === 'object') {
1253
+ const record = res as Record<string, unknown>;
1254
+ // Signup branch: mints a session (LoginSessionResult, carries
1255
+ // `sessionId`). Parse against the login contract and plant the token.
1256
+ if ('sessionId' in record) {
1257
+ const parsed = safeParseContract(loginResultSchema, record);
1258
+ if (!parsed) {
1259
+ throw new Error('auth/webauthn/register/verify returned an unexpected response shape');
1260
+ }
1261
+ if (!('twoFactorRequired' in parsed) && parsed.accessToken) {
1262
+ this.setTokens(parsed.accessToken);
1263
+ }
1264
+ return parsed;
1265
+ }
1266
+ // Link branch: passkey attached to the signed-in account, no session.
1267
+ if (record.success === true && typeof record.message === 'string') {
1268
+ return { success: true, message: record.message };
1269
+ }
1270
+ }
1271
+ throw new Error('auth/webauthn/register/verify returned an unexpected response shape');
1272
+ } catch (error) {
1273
+ throw this.handleError(error);
1274
+ }
1275
+ }
1276
+
1277
+ /**
1278
+ * Begin a WebAuthn / passkey AUTHENTICATION ceremony. Requests the
1279
+ * `PublicKeyCredentialRequestOptions` the browser's `navigator.credentials
1280
+ * .get()` (or `@simplewebauthn/browser`'s `startAuthentication`) needs.
1281
+ *
1282
+ * When `username` is present the server scopes `allowCredentials` to that
1283
+ * user's passkeys (username-first); when omitted it returns an empty
1284
+ * allow-list for the usernameless / discoverable-credential flow. The
1285
+ * returned options are OPAQUE and pass through as `unknown`.
1286
+ */
1287
+ async webauthnLoginOptions(username?: string): Promise<unknown> {
1288
+ try {
1289
+ return await this.makeRequest<unknown>(
1290
+ 'POST',
1291
+ '/auth/webauthn/login/options',
1292
+ { ...(username !== undefined ? { username } : {}) },
1293
+ { cache: false },
1294
+ );
1295
+ } catch (error) {
1296
+ throw this.handleError(error);
1297
+ }
1298
+ }
1299
+
1300
+ /**
1301
+ * Finish a WebAuthn / passkey AUTHENTICATION ceremony. Forwards the opaque
1302
+ * browser `AuthenticationResponseJSON` (`response`) alongside the
1303
+ * device-session envelope. Resolves to the SAME {@link LoginResult} contract
1304
+ * as `POST /auth/verify`; on the session arm the access token is planted
1305
+ * immediately (mirroring {@link passwordSignIn}), and the response's
1306
+ * `deviceId` + `deviceSecret` are the zero-cookie restore credential.
1307
+ */
1308
+ async webauthnLoginVerify(
1309
+ response: unknown,
1310
+ envelope: { deviceName?: string; deviceFingerprint?: string; deviceId?: string } = {},
1311
+ ): Promise<LoginResult> {
1312
+ try {
1313
+ const res = await this.makeRequest<unknown>(
1314
+ 'POST',
1315
+ '/auth/webauthn/login/verify',
1316
+ { response, ...envelope },
1317
+ { cache: false },
1318
+ );
1319
+ const parsed = safeParseContract(loginResultSchema, res);
1320
+ if (!parsed) {
1321
+ throw new Error('auth/webauthn/login/verify returned an unexpected response shape');
1322
+ }
1323
+ if (!('twoFactorRequired' in parsed) && parsed.accessToken) {
1324
+ this.setTokens(parsed.accessToken);
1325
+ }
1326
+ return parsed;
1327
+ } catch (error) {
1328
+ throw this.handleError(error);
1329
+ }
1330
+ }
1331
+
1199
1332
  /**
1200
1333
  * Exchange an OAuth authorization code (returned to the RP redirect URI
1201
1334
  * after password sign-in at auth.oxy.so) for a device-first session.
@@ -5,7 +5,7 @@ import { normalizeLocale, getPrimaryLanguage, getLanguageMetadata, getLanguageNa
5
5
  import type { SupportedLanguage } from '../utils/languageUtils';
6
6
  import type { OxyServicesBase } from '../OxyServices.base';
7
7
  import { loadAsyncStorage } from '@oxyhq/protocol';
8
- import { isDev } from '../shared/utils/debugUtils';
8
+ import { logger } from '../logger';
9
9
 
10
10
  /**
11
11
  * Cross-mixin surface consumed by the language methods. `getCurrentUser` is
@@ -48,7 +48,7 @@ export function OxyServicesLanguageMixin<T extends typeof OxyServicesBase>(Base:
48
48
  removeItem: storage.removeItem.bind(storage),
49
49
  };
50
50
  } catch (error) {
51
- console.error('AsyncStorage not available in React Native:', error);
51
+ logger.error('AsyncStorage not available in React Native', error, { component: 'OxyServices.language' });
52
52
  throw new Error('AsyncStorage is required in React Native environment');
53
53
  }
54
54
  } else {
@@ -103,9 +103,7 @@ export function OxyServicesLanguageMixin<T extends typeof OxyServicesBase>(Base:
103
103
 
104
104
  return null;
105
105
  } catch (error) {
106
- if (isDev()) {
107
- console.warn('Failed to get current language:', error);
108
- }
106
+ logger.warn('Failed to get current language', { component: 'OxyServices.language' }, error);
109
107
  return null;
110
108
  }
111
109
  }
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import type { BlockedUser, RestrictedUser } from '../models/interfaces';
5
5
  import type { OxyServicesBase } from '../OxyServices.base';
6
- import { isDev } from '../shared/utils/debugUtils';
6
+ import { logger } from '../logger';
7
7
 
8
8
  export function OxyServicesPrivacyMixin<T extends typeof OxyServicesBase>(Base: T) {
9
9
  return class extends Base {
@@ -36,9 +36,7 @@ export function OxyServicesPrivacyMixin<T extends typeof OxyServicesBase>(Base:
36
36
  });
37
37
  } catch (error) {
38
38
  // If there's an error, assume not in list to avoid breaking functionality
39
- if (isDev()) {
40
- console.warn('Error checking user list:', error);
41
- }
39
+ logger.warn('Error checking user list', { component: 'OxyServices.privacy' }, error);
42
40
  return false;
43
41
  }
44
42
  }
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import type { OxyServicesBase } from '../OxyServices.base';
5
5
  import type { SecurityActivity, SecurityActivityResponse, SecurityEventType } from '../models/interfaces';
6
- import { isDev } from '../shared/utils/debugUtils';
6
+ import { logger } from '../logger';
7
7
 
8
8
  export function OxyServicesSecurityMixin<T extends typeof OxyServicesBase>(Base: T) {
9
9
  return class extends Base {
@@ -80,11 +80,9 @@ export function OxyServicesSecurityMixin<T extends typeof OxyServicesBase>(Base:
80
80
  { cache: false }
81
81
  );
82
82
  } catch (error) {
83
- // Don't throw - logging failures shouldn't break user flow
84
- // But log for monitoring
85
- if (isDev()) {
86
- console.warn('[OxyServices] Failed to log private key exported event:', error);
87
- }
83
+ // Don't throw - logging failures shouldn't break user flow, but surface
84
+ // for monitoring via the shared logger sink.
85
+ logger.warn('[OxyServices] Failed to log private key exported event', { component: 'OxyServices.security' }, error);
88
86
  }
89
87
  }
90
88
 
@@ -102,11 +100,9 @@ export function OxyServicesSecurityMixin<T extends typeof OxyServicesBase>(Base:
102
100
  { cache: false }
103
101
  );
104
102
  } catch (error) {
105
- // Don't throw - logging failures shouldn't break user flow
106
- // But log for monitoring
107
- if (isDev()) {
108
- console.warn('[OxyServices] Failed to log backup created event:', error);
109
- }
103
+ // Don't throw - logging failures shouldn't break user flow, but surface
104
+ // for monitoring via the shared logger sink.
105
+ logger.warn('[OxyServices] Failed to log backup created event', { component: 'OxyServices.security' }, error);
110
106
  }
111
107
  }
112
108
  };
@@ -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
  import { CACHE_TIMES } from './mixinHelpers';
9
9
 
10
10
  export function OxyServicesTopicsMixin<T extends typeof OxyServicesBase>(Base: T) {
@@ -22,10 +22,15 @@ export function OxyServicesTopicsMixin<T extends typeof OxyServicesBase>(Base: T
22
22
  try {
23
23
  const params: Record<string, string> = {};
24
24
  if (locale) params.locale = locale;
25
- return await this.makeRequest('GET', '/topics/categories', params, {
26
- cache: true,
27
- cacheTTL: CACHE_TIMES.EXTRA_LONG,
28
- });
25
+ // `GET /topics/categories` returns `{ categories: TopicData[] }` — the
26
+ // SDK's `unwrapResponse` only unwraps `{ data }`, so unwrap here.
27
+ const response = await this.makeRequest<{ categories: TopicData[] }>(
28
+ 'GET',
29
+ '/topics/categories',
30
+ params,
31
+ { cache: true, cacheTTL: CACHE_TIMES.EXTRA_LONG }
32
+ );
33
+ return response.categories ?? [];
29
34
  } catch (error) {
30
35
  throw this.handleError(error);
31
36
  }
@@ -41,9 +46,14 @@ export function OxyServicesTopicsMixin<T extends typeof OxyServicesBase>(Base: T
41
46
  try {
42
47
  const params: Record<string, string | number> = { q: query };
43
48
  if (limit) params.limit = limit;
44
- return await this.makeRequest('GET', '/topics/search', params, {
45
- cache: false,
46
- });
49
+ // `GET /topics/search` returns `{ topics: TopicData[] }` — unwrap it.
50
+ const response = await this.makeRequest<{ topics: TopicData[] }>(
51
+ 'GET',
52
+ '/topics/search',
53
+ params,
54
+ { cache: false }
55
+ );
56
+ return response.topics ?? [];
47
57
  } catch (error) {
48
58
  throw this.handleError(error);
49
59
  }
@@ -52,7 +62,7 @@ export function OxyServicesTopicsMixin<T extends typeof OxyServicesBase>(Base: T
52
62
  /**
53
63
  * List topics with optional filters
54
64
  * @param options - Filter and pagination options
55
- * @returns List of topics
65
+ * @returns Paginated topics envelope (`topics` plus `total`/`limit`/`offset`)
56
66
  */
57
67
  async listTopics(options?: {
58
68
  type?: string;
@@ -60,7 +70,7 @@ export function OxyServicesTopicsMixin<T extends typeof OxyServicesBase>(Base: T
60
70
  limit?: number;
61
71
  offset?: number;
62
72
  locale?: string;
63
- }): Promise<TopicData[]> {
73
+ }): Promise<TopicListResult> {
64
74
  try {
65
75
  const params: Record<string, string | number> = {};
66
76
  if (options?.type) params.type = options.type;
@@ -68,10 +78,23 @@ export function OxyServicesTopicsMixin<T extends typeof OxyServicesBase>(Base: T
68
78
  if (options?.limit) params.limit = options.limit;
69
79
  if (options?.offset) params.offset = options.offset;
70
80
  if (options?.locale) params.locale = options.locale;
71
- return await this.makeRequest('GET', '/topics', params, {
72
- cache: true,
73
- cacheTTL: CACHE_TIMES.SHORT,
74
- });
81
+ // `GET /topics` returns `{ topics, total, limit, offset }`. The pagination
82
+ // fields matter to callers, so return the whole envelope (typed) rather
83
+ // than throwing them away.
84
+ const response = await this.makeRequest<Partial<TopicListResult>>(
85
+ 'GET',
86
+ '/topics',
87
+ params,
88
+ { cache: true, cacheTTL: CACHE_TIMES.SHORT }
89
+ );
90
+ const requestedLimit = typeof params.limit === 'number' ? params.limit : 0;
91
+ const requestedOffset = typeof params.offset === 'number' ? params.offset : 0;
92
+ return {
93
+ topics: response.topics ?? [],
94
+ total: response.total ?? response.topics?.length ?? 0,
95
+ limit: response.limit ?? requestedLimit,
96
+ offset: response.offset ?? requestedOffset,
97
+ };
75
98
  } catch (error) {
76
99
  throw this.handleError(error);
77
100
  }
@@ -102,9 +125,16 @@ export function OxyServicesTopicsMixin<T extends typeof OxyServicesBase>(Base: T
102
125
  names: Array<{ name: string; type: string }>
103
126
  ): Promise<TopicData[]> {
104
127
  try {
105
- return await this.makeRequest('POST', '/topics/resolve', { names }, {
106
- cache: false,
107
- });
128
+ // `POST /topics/resolve` returns `{ topics: Record<name, TopicData> }`
129
+ // (a name-keyed map, not an array). Unwrap and flatten to the resolved
130
+ // topics; each TopicData carries its own `name` for re-keying.
131
+ const response = await this.makeRequest<{ topics: Record<string, TopicData> }>(
132
+ 'POST',
133
+ '/topics/resolve',
134
+ { names },
135
+ { cache: false }
136
+ );
137
+ return Object.values(response.topics ?? {});
108
138
  } catch (error) {
109
139
  throw this.handleError(error);
110
140
  }
@@ -22,7 +22,7 @@ import { buildSearchParams, buildPaginationParams, type PaginationParams } from
22
22
  import { KeyManager } from '../crypto/keyManager';
23
23
  import { SignatureService } from '../crypto/signatureService';
24
24
  import { normalizeUserIdentity, normalizeUserIdentityOrNull } from '../utils/userIdentity';
25
- import { logger } from '../utils/loggerUtils';
25
+ import { logger } from '../logger';
26
26
  import { extractErrorStatus } from '../utils/errorUtils';
27
27
 
28
28
  /**
@@ -10,7 +10,7 @@ import type { ApiError, User } from '../models/interfaces';
10
10
  import type { OxyServicesBase } from '../OxyServices.base';
11
11
  import { loadNodeCrypto } from '@oxyhq/protocol';
12
12
  import { buildUrl } from '../utils/apiUtils';
13
- import { logger } from '../utils/loggerUtils';
13
+ import { logger } from '../logger';
14
14
  import { CACHE_TIMES } from './mixinHelpers';
15
15
 
16
16
  interface JwtPayload {
@@ -13,7 +13,7 @@
13
13
  */
14
14
 
15
15
  import { OxyServices } from '../../OxyServices';
16
- import { logger } from '../../utils/loggerUtils';
16
+ import { logger } from '../../logger';
17
17
 
18
18
  /** A JSON success `Response` mimicking the API's `{ data: ... }` envelope. */
19
19
  function jsonResponse(data: unknown): Response {