@oxyhq/core 10.1.5 → 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 (100) 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/server/rateLimit.js +100 -6
  16. package/dist/cjs/session/SessionClient.js +17 -17
  17. package/dist/cjs/session/accountDialogController.js +12 -12
  18. package/dist/cjs/session/authStateStore.js +5 -5
  19. package/dist/cjs/session/refresh.js +4 -4
  20. package/dist/cjs/session/socketLoader.js +2 -2
  21. package/dist/cjs/utils/asyncUtils.js +2 -2
  22. package/dist/cjs/utils/avatarUtils.js +2 -1
  23. package/dist/cjs/utils/deviceManager.js +7 -5
  24. package/dist/cjs/utils/errorUtils.js +3 -3
  25. package/dist/cjs/utils/oauthPkce.js +3 -3
  26. package/dist/cjs/utils/requestUtils.js +1 -1
  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 +2 -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 +1 -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/server/rateLimit.js +100 -6
  42. package/dist/esm/session/SessionClient.js +1 -1
  43. package/dist/esm/session/accountDialogController.js +1 -1
  44. package/dist/esm/session/authStateStore.js +1 -1
  45. package/dist/esm/session/refresh.js +1 -1
  46. package/dist/esm/session/socketLoader.js +1 -1
  47. package/dist/esm/utils/asyncUtils.js +1 -1
  48. package/dist/esm/utils/avatarUtils.js +2 -1
  49. package/dist/esm/utils/deviceManager.js +7 -5
  50. package/dist/esm/utils/errorUtils.js +1 -1
  51. package/dist/esm/utils/oauthPkce.js +1 -1
  52. package/dist/esm/utils/requestUtils.js +1 -1
  53. package/dist/types/.tsbuildinfo +1 -1
  54. package/dist/types/index.d.ts +3 -4
  55. package/dist/types/logger/index.d.ts +104 -0
  56. package/dist/types/mixins/OxyServices.topics.d.ts +3 -3
  57. package/dist/types/models/Topic.d.ts +10 -0
  58. package/dist/types/utils/requestUtils.d.ts +1 -1
  59. package/package.json +16 -1
  60. package/src/boot/sessionColdBoot.ts +1 -1
  61. package/src/crypto/keyManager.ts +1 -2
  62. package/src/crypto/signatureService.ts +1 -1
  63. package/src/index.ts +17 -19
  64. package/src/logger/__tests__/logger.test.ts +207 -0
  65. package/src/logger/index.ts +217 -0
  66. package/src/mixins/OxyServices.assets.ts +3 -3
  67. package/src/mixins/OxyServices.auth.ts +1 -1
  68. package/src/mixins/OxyServices.language.ts +3 -5
  69. package/src/mixins/OxyServices.privacy.ts +2 -4
  70. package/src/mixins/OxyServices.security.ts +7 -11
  71. package/src/mixins/OxyServices.topics.ts +47 -17
  72. package/src/mixins/OxyServices.user.ts +1 -1
  73. package/src/mixins/OxyServices.utility.ts +1 -1
  74. package/src/mixins/__tests__/discoveryErrorHandling.test.ts +1 -1
  75. package/src/mixins/__tests__/topics.test.ts +156 -0
  76. package/src/models/Topic.ts +11 -0
  77. package/src/server/__tests__/rateLimit.test.ts +98 -4
  78. package/src/server/rateLimit.ts +105 -6
  79. package/src/session/SessionClient.ts +1 -1
  80. package/src/session/__tests__/SessionClient.diagnostics.test.ts +1 -1
  81. package/src/session/__tests__/accountDialogController.test.ts +1 -1
  82. package/src/session/accountDialogController.ts +1 -1
  83. package/src/session/authStateStore.ts +1 -1
  84. package/src/session/refresh.ts +1 -1
  85. package/src/session/socketLoader.ts +1 -1
  86. package/src/utils/asyncUtils.ts +1 -1
  87. package/src/utils/avatarUtils.ts +3 -1
  88. package/src/utils/deviceManager.ts +8 -5
  89. package/src/utils/errorUtils.ts +1 -1
  90. package/src/utils/oauthPkce.ts +1 -1
  91. package/src/utils/requestUtils.ts +1 -1
  92. package/dist/cjs/shared/utils/debugUtils.js +0 -80
  93. package/dist/cjs/utils/loggerUtils.js +0 -126
  94. package/dist/esm/shared/utils/debugUtils.js +0 -72
  95. package/dist/esm/utils/loggerUtils.js +0 -115
  96. package/dist/types/shared/utils/debugUtils.d.ts +0 -48
  97. package/dist/types/utils/loggerUtils.d.ts +0 -48
  98. package/src/shared/utils/__tests__/debugUtils.test.ts +0 -55
  99. package/src/shared/utils/debugUtils.ts +0 -78
  100. package/src/utils/loggerUtils.ts +0 -153
@@ -26,6 +26,8 @@ interface RateLimitTestRequest extends Request {
26
26
  observedKey?: string;
27
27
  }
28
28
 
29
+ const HEX24 = /^[0-9a-f]{24}$/;
30
+
29
31
  function makeOxy(authHandler: RequestHandler): OxyServices {
30
32
  return {
31
33
  auth: jest.fn(() => authHandler),
@@ -33,17 +35,37 @@ function makeOxy(authHandler: RequestHandler): OxyServices {
33
35
  }
34
36
 
35
37
  function makeRequest(overrides: Partial<RateLimitTestRequest> = {}): RateLimitTestRequest {
38
+ const ip = overrides.ip ?? '203.0.113.9';
36
39
  return {
37
40
  method: 'GET',
38
41
  path: '/api/test',
39
- ip: '203.0.113.9',
40
- socket: { remoteAddress: '203.0.113.9' },
42
+ ip,
43
+ socket: { remoteAddress: ip },
41
44
  ...overrides,
42
45
  } as RateLimitTestRequest;
43
46
  }
44
47
 
48
+ /** Run the anonymous limiter for a bare IP and return the store key it produced. */
49
+ function keyForIp(ip: string): string {
50
+ const oxy = makeOxy((_req: Request, _res: Response, next: NextFunction) => next());
51
+ const req = makeRequest({ ip });
52
+ createOxyRateLimit(oxy)(req, {} as Response, jest.fn());
53
+ if (typeof req.observedKey !== 'string') {
54
+ throw new Error('key generator did not run');
55
+ }
56
+ return req.observedKey;
57
+ }
58
+
45
59
  describe('@oxyhq/core/server rate limiter', () => {
60
+ const originalEnv = {
61
+ IP_HASH_SALT: process.env.IP_HASH_SALT,
62
+ DEVICE_ID_SALT: process.env.DEVICE_ID_SALT,
63
+ };
64
+
46
65
  beforeEach(() => {
66
+ // Isolate salt resolution from any ambient env so key assertions are deterministic.
67
+ delete process.env.IP_HASH_SALT;
68
+ delete process.env.DEVICE_ID_SALT;
47
69
  rateLimitMock.mockImplementation((options: CapturedRateLimitOptions) => {
48
70
  return (req: RateLimitTestRequest, _res: Response, next: NextFunction) => {
49
71
  req.observedMax = options.max(req);
@@ -55,6 +77,10 @@ describe('@oxyhq/core/server rate limiter', () => {
55
77
 
56
78
  afterEach(() => {
57
79
  jest.clearAllMocks();
80
+ if (originalEnv.IP_HASH_SALT === undefined) delete process.env.IP_HASH_SALT;
81
+ else process.env.IP_HASH_SALT = originalEnv.IP_HASH_SALT;
82
+ if (originalEnv.DEVICE_ID_SALT === undefined) delete process.env.DEVICE_ID_SALT;
83
+ else process.env.DEVICE_ID_SALT = originalEnv.DEVICE_ID_SALT;
58
84
  });
59
85
 
60
86
  it('does not trust locally decoded non-session JWT identities for quota or bucket keys', () => {
@@ -73,7 +99,9 @@ describe('@oxyhq/core/server rate limiter', () => {
73
99
  );
74
100
 
75
101
  expect(req.observedMax).toBe(600);
76
- expect(req.observedKey).toBe('203.0.113.9');
102
+ // Anonymous callers are bucketed by a hashed key, NEVER the raw IP.
103
+ expect(req.observedKey).toMatch(HEX24);
104
+ expect(req.observedKey).not.toContain('203.0.113.9');
77
105
  expect(next).toHaveBeenCalledTimes(1);
78
106
  });
79
107
 
@@ -93,6 +121,7 @@ describe('@oxyhq/core/server rate limiter', () => {
93
121
  );
94
122
 
95
123
  expect(req.observedMax).toBe(5000);
124
+ // Authenticated identities are keyed by the user id verbatim — NOT hashed.
96
125
  expect(req.observedKey).toBe('user:validated-user');
97
126
  });
98
127
 
@@ -110,7 +139,72 @@ describe('@oxyhq/core/server rate limiter', () => {
110
139
  );
111
140
 
112
141
  expect(req.observedMax).toBe(600);
113
- expect(req.observedKey).toBe('203.0.113.9');
142
+ expect(req.observedKey).toMatch(HEX24);
143
+ expect(req.observedKey).not.toContain('203.0.113.9');
114
144
  expect(next).toHaveBeenCalledTimes(1);
115
145
  });
146
+
147
+ describe('anonymous key hashing', () => {
148
+ it('produces a deterministic 24-hex key that never contains the raw IP', () => {
149
+ const first = keyForIp('203.0.113.9');
150
+ const second = keyForIp('203.0.113.9');
151
+
152
+ expect(first).toMatch(HEX24);
153
+ expect(first).toBe(second);
154
+ expect(first).not.toContain('203.0.113.9');
155
+ });
156
+
157
+ it('produces different keys for different IPv4 addresses', () => {
158
+ expect(keyForIp('203.0.113.9')).not.toBe(keyForIp('198.51.100.7'));
159
+ });
160
+
161
+ it('buckets IPv6 addresses in the same /56 to the same key', () => {
162
+ // 2001:db8:abcd:ee11 and 2001:db8:abcd:eeff share the /56 prefix (top byte of
163
+ // the 4th hextet is 0xee for both); the differing bits are host bits.
164
+ const a = keyForIp('2001:db8:abcd:ee11::1');
165
+ const b = keyForIp('2001:db8:abcd:eeff::9999');
166
+
167
+ expect(a).toMatch(HEX24);
168
+ expect(a).toBe(b);
169
+ });
170
+
171
+ it('produces different keys for IPv6 addresses in different /56 prefixes', () => {
172
+ const sameFiftySix = keyForIp('2001:db8:abcd:ee11::1');
173
+ const otherFiftySix = keyForIp('2001:db8:abcd:ff11::1');
174
+
175
+ expect(sameFiftySix).not.toBe(otherFiftySix);
176
+ });
177
+
178
+ it('salts the hash with IP_HASH_SALT so keys are not portable across salts', () => {
179
+ const unsalted = keyForIp('203.0.113.9');
180
+
181
+ process.env.IP_HASH_SALT = 'salt-a';
182
+ const saltedA = keyForIp('203.0.113.9');
183
+
184
+ process.env.IP_HASH_SALT = 'salt-b';
185
+ const saltedB = keyForIp('203.0.113.9');
186
+
187
+ expect(saltedA).toMatch(HEX24);
188
+ expect(saltedA).not.toBe(unsalted);
189
+ expect(saltedB).not.toBe(saltedA);
190
+ });
191
+
192
+ it('prefers IP_HASH_SALT over DEVICE_ID_SALT', () => {
193
+ process.env.DEVICE_ID_SALT = 'device-salt';
194
+ const deviceOnly = keyForIp('203.0.113.9');
195
+
196
+ process.env.IP_HASH_SALT = 'ip-salt';
197
+ const ipPreferred = keyForIp('203.0.113.9');
198
+
199
+ expect(ipPreferred).not.toBe(deviceOnly);
200
+ });
201
+
202
+ it('falls back to the literal "unknown" key when no IP is resolvable', () => {
203
+ const oxy = makeOxy((_req: Request, _res: Response, next: NextFunction) => next());
204
+ const req = makeRequest({ ip: undefined, socket: {} as Request['socket'] });
205
+ createOxyRateLimit(oxy)(req, {} as Response, jest.fn());
206
+
207
+ expect(req.observedKey).toBe('unknown');
208
+ });
209
+ });
116
210
  });
@@ -1,3 +1,5 @@
1
+ import { createHmac } from 'node:crypto';
2
+ import { isIPv4, isIPv6 } from 'node:net';
1
3
  import type { Request, RequestHandler } from 'express';
2
4
  import rateLimit, { type Store } from 'express-rate-limit';
3
5
  import type { OxyServices } from '../OxyServices';
@@ -99,9 +101,103 @@ function isBuiltInExempt(req: Request): boolean {
99
101
  );
100
102
  }
101
103
 
102
- /** IPv6-safe IP key generator (replaces colons to avoid Redis namespace issues). */
103
- function ipKeyGenerator(ip: string): string {
104
- return ip.replace(/:/g, '_');
104
+ /**
105
+ * Anonymous rate-limit keys must be PRIVACY-PRESERVING: the raw client IP must
106
+ * never reach a store at rest (in-memory or Redis). We therefore HMAC-hash the
107
+ * IP into a short, transient-only bucket key. Two IPv6-specific concerns shape
108
+ * the pre-hash normalization:
109
+ *
110
+ * - IPv6 hosts are typically handed an entire /64 (often a /56), so a single
111
+ * host can rotate through an enormous address space and evade a per-address
112
+ * limit. We bucket IPv6 to its /56 prefix BEFORE hashing.
113
+ * - express-rate-limit only exposes an `ipKeyGenerator` /56 helper from v8
114
+ * onwards; `@oxyhq/core` pins v7 (peer `^7.0.0`), so the masking is
115
+ * implemented here rather than pulling a major-version bump of a
116
+ * security-critical dependency (and its rate-limit-redis compatibility) into
117
+ * an unrelated privacy change. This mirrors `packages/api/src/utils/ipKey.ts`.
118
+ */
119
+ const IPV6_SUBNET_BITS = 56;
120
+
121
+ /** Expand an IPv6 literal (handling `::` and embedded IPv4) to 8 numeric hextets, or null if unparseable. */
122
+ function ipv6Hextets(ip: string): number[] | null {
123
+ let addr = ip;
124
+ const zone = addr.indexOf('%');
125
+ if (zone !== -1) {
126
+ addr = addr.slice(0, zone);
127
+ }
128
+
129
+ // Embedded IPv4 tail (e.g. `::ffff:203.0.113.7`) → fold the dotted quad into two hextets.
130
+ const lastColon = addr.lastIndexOf(':');
131
+ if (lastColon !== -1 && addr.slice(lastColon + 1).includes('.')) {
132
+ const v4 = addr.slice(lastColon + 1);
133
+ if (!isIPv4(v4)) {
134
+ return null;
135
+ }
136
+ const octets = v4.split('.').map((part) => Number.parseInt(part, 10));
137
+ const high = ((octets[0] << 8) | octets[1]).toString(16);
138
+ const low = ((octets[2] << 8) | octets[3]).toString(16);
139
+ addr = `${addr.slice(0, lastColon + 1)}${high}:${low}`;
140
+ }
141
+
142
+ const halves = addr.split('::');
143
+ if (halves.length > 2) {
144
+ return null;
145
+ }
146
+ const head = halves[0] ? halves[0].split(':') : [];
147
+ const tail = halves.length === 2 && halves[1] ? halves[1].split(':') : [];
148
+ let groups: string[];
149
+ if (halves.length === 1) {
150
+ groups = head;
151
+ } else {
152
+ const missing = 8 - (head.length + tail.length);
153
+ if (missing < 0) {
154
+ return null;
155
+ }
156
+ groups = [...head, ...new Array(missing).fill('0'), ...tail];
157
+ }
158
+ if (groups.length !== 8) {
159
+ return null;
160
+ }
161
+ const hextets = groups.map((group) => Number.parseInt(group || '0', 16));
162
+ if (hextets.some((value) => Number.isNaN(value) || value < 0 || value > 0xffff)) {
163
+ return null;
164
+ }
165
+ return hextets;
166
+ }
167
+
168
+ /** Mask an IPv6 address to its /{bits} prefix, returned as a canonical hex string. */
169
+ function maskIPv6(ip: string, bits: number): string {
170
+ const hextets = ipv6Hextets(ip);
171
+ if (!hextets) {
172
+ return ip;
173
+ }
174
+ const masked = hextets.map((hextet, index) => {
175
+ const groupStart = index * 16;
176
+ if (groupStart >= bits) {
177
+ return 0;
178
+ }
179
+ const keepBits = Math.min(16, bits - groupStart);
180
+ const mask = keepBits >= 16 ? 0xffff : (0xffff << (16 - keepBits)) & 0xffff;
181
+ return hextet & mask;
182
+ });
183
+ return `${masked.map((hextet) => hextet.toString(16)).join(':')}/${bits}`;
184
+ }
185
+
186
+ /**
187
+ * Hash a client IP into a privacy-preserving bucket key. IPv6 is bucketed to its
188
+ * /56 prefix first (so a single v6 host can't rotate through its allocation to
189
+ * mint fresh keys), then HMAC'd with the server-side salt. The salt is resolved
190
+ * at CALL time (`IP_HASH_SALT`, else `DEVICE_ID_SALT`, else empty) — an empty
191
+ * salt still hashes, which beats storing a raw IP; backends SHOULD set one of
192
+ * those envs. The `rl|` namespace ensures a rate-limit key can never collide
193
+ * with, or be correlated against, a deviceId derivation that reuses the same
194
+ * salt. The result is a short hex digest with no colons, so it is Redis-safe.
195
+ */
196
+ function hashAnonymousIp(ip: string): string {
197
+ const normalized =
198
+ isIPv6(ip) && !ip.startsWith('::ffff:') ? maskIPv6(ip, IPV6_SUBNET_BITS) : ip;
199
+ const salt = process.env.IP_HASH_SALT || process.env.DEVICE_ID_SALT || '';
200
+ return createHmac('sha256', salt).update(`rl|${normalized}`).digest('hex').slice(0, 24);
105
201
  }
106
202
 
107
203
  /**
@@ -132,14 +228,17 @@ function resolveTrustedAuthenticatedKey(req: OxyAuthedRequest): string | null {
132
228
  return null;
133
229
  }
134
230
 
135
- /** Resolve the rate-limit key: per trusted authenticated identity, else per (IPv6-safe) IP. */
231
+ /** Resolve the rate-limit key: per trusted authenticated identity, else per hashed (IPv6-bucketed) IP. */
136
232
  function resolveKey(req: OxyAuthedRequest): string {
137
233
  const authenticatedKey = resolveTrustedAuthenticatedKey(req);
138
234
  if (authenticatedKey) {
139
235
  return authenticatedKey;
140
236
  }
141
- const ip = req.ip || req.socket.remoteAddress || 'unknown';
142
- return ipKeyGenerator(ip);
237
+ const ip = req.ip || req.socket.remoteAddress;
238
+ if (!ip) {
239
+ return 'unknown';
240
+ }
241
+ return hashAnonymousIp(ip);
143
242
  }
144
243
 
145
244
  /**
@@ -6,7 +6,7 @@ import {
6
6
  sessionAccountsChangedEventSchema,
7
7
  type DeviceSessionState,
8
8
  } from '@oxyhq/contracts';
9
- import { logger } from '../utils/loggerUtils';
9
+ import { logger } from '../logger';
10
10
  import { getSocketIO } from './socketLoader';
11
11
  import type { MinimalSocket, SocketIOFactory } from './socketLoader';
12
12
 
@@ -1,6 +1,6 @@
1
1
  import type { DeviceSessionState } from '@oxyhq/contracts';
2
2
  import { SessionClient, type SessionClientHost } from '../SessionClient';
3
- import { logger } from '../../utils/loggerUtils';
3
+ import { logger } from '../../logger';
4
4
 
5
5
  const STATE = (rev: number): DeviceSessionState => ({
6
6
  deviceId: 'd1', accounts: [{ accountId: 'a1', sessionId: 's1', authuser: 0 }], activeAccountId: 'a1', revision: rev, updatedAt: 1720000000000,
@@ -5,7 +5,7 @@ import type { SessionLoginResponse, MinimalUserData } from '../../models/session
5
5
  import type { AccountNode } from '../../mixins/OxyServices.accounts';
6
6
  import { SessionClient, type SessionClientHost } from '../SessionClient';
7
7
  import type { MinimalSocket, SocketIOFactory } from '../socketLoader';
8
- import { logger } from '../../utils/loggerUtils';
8
+ import { logger } from '../../logger';
9
9
  import {
10
10
  AccountDialogController,
11
11
  createAccountDialogController,
@@ -29,7 +29,7 @@
29
29
  import type { OxyServices } from '../OxyServices';
30
30
  import type { SessionLoginResponse, MinimalUserData } from '../models/session';
31
31
  import type { User } from '../models/interfaces';
32
- import { logger } from '../utils/loggerUtils';
32
+ import { logger } from '../logger';
33
33
  import { extractErrorStatus } from '../utils/errorUtils';
34
34
  import { CENTRAL_IDP_APEX } from '../utils/authWebUrl';
35
35
  import {
@@ -17,7 +17,7 @@
17
17
  * ESM-safe (no `require()`).
18
18
  */
19
19
 
20
- import { logger } from '../utils/loggerUtils';
20
+ import { logger } from '../logger';
21
21
 
22
22
  /**
23
23
  * The persisted session credential set for a single origin.
@@ -26,7 +26,7 @@ import type { AuthRefreshHandler, AuthRefreshReason } from '../HttpService';
26
26
  import type { AuthStateStore, PersistedAuthState } from './authStateStore';
27
27
  import { isNative } from '../utils/platform';
28
28
  import { extractErrorStatus } from '../utils/errorUtils';
29
- import { logger } from '../utils/loggerUtils';
29
+ import { logger } from '../logger';
30
30
 
31
31
  /**
32
32
  * Lead time (ms) before access-token expiry at which the proactive scheduler
@@ -1,4 +1,4 @@
1
- import { logger } from '../utils/loggerUtils';
1
+ import { logger } from '../logger';
2
2
 
3
3
  export interface MinimalSocket {
4
4
  connected: boolean;
@@ -2,7 +2,7 @@
2
2
  * Async utilities for common asynchronous patterns and error handling
3
3
  */
4
4
 
5
- import { logger } from './loggerUtils';
5
+ import { logger } from '../logger';
6
6
 
7
7
  /**
8
8
  * Wrapper for async operations with automatic error handling
@@ -1,3 +1,5 @@
1
+ import { logger } from '../logger';
2
+
1
3
  /**
2
4
  * Minimal interface for services that can update asset visibility.
3
5
  * Kept loose to avoid mixin type-inference issues with the OxyServices class.
@@ -31,7 +33,7 @@ export async function updateAvatarVisibility(
31
33
  ? (visError as Error & { status: number }).status
32
34
  : undefined;
33
35
  if (status !== 404) {
34
- console.error(`[${contextName}] Failed to update avatar visibility for ${fileId}:`, visError);
36
+ logger.error(`[${contextName}] Failed to update avatar visibility for ${fileId}`, visError, { component: contextName });
35
37
  }
36
38
  }
37
39
  }
@@ -1,4 +1,7 @@
1
1
  import { loadAsyncStorage } from '@oxyhq/protocol';
2
+ import { createLogger } from '../logger';
3
+
4
+ const log = createLogger('DeviceManager');
2
5
 
3
6
  export interface DeviceFingerprint {
4
7
  userAgent: string;
@@ -55,7 +58,7 @@ export class DeviceManager {
55
58
  removeItem: storage.removeItem.bind(storage),
56
59
  };
57
60
  } catch (error) {
58
- console.error('AsyncStorage not available in React Native:', error);
61
+ log.error('AsyncStorage not available in React Native', error);
59
62
  throw new Error('AsyncStorage is required in React Native environment');
60
63
  }
61
64
  } else {
@@ -112,7 +115,7 @@ export class DeviceManager {
112
115
  // Create new device info
113
116
  return await this.createNewDeviceInfo();
114
117
  } catch (error) {
115
- console.error('Error getting device info:', error);
118
+ log.error('Error getting device info', error);
116
119
  return await this.createNewDeviceInfo();
117
120
  }
118
121
  }
@@ -140,7 +143,7 @@ export class DeviceManager {
140
143
  const storage = await this.getStorage();
141
144
  await storage.setItem(this.DEVICE_KEY, JSON.stringify(deviceInfo));
142
145
  } catch (error) {
143
- console.error('Error saving device info:', error);
146
+ log.error('Error saving device info', error);
144
147
  }
145
148
  }
146
149
 
@@ -153,7 +156,7 @@ export class DeviceManager {
153
156
  deviceInfo.deviceName = deviceName;
154
157
  await this.saveDeviceInfo(deviceInfo);
155
158
  } catch (error) {
156
- console.error('Error updating device name:', error);
159
+ log.error('Error updating device name', error);
157
160
  }
158
161
  }
159
162
 
@@ -165,7 +168,7 @@ export class DeviceManager {
165
168
  const storage = await this.getStorage();
166
169
  await storage.removeItem(this.DEVICE_KEY);
167
170
  } catch (error) {
168
- console.error('Error clearing device info:', error);
171
+ log.error('Error clearing device info', error);
169
172
  }
170
173
  }
171
174
 
@@ -1,5 +1,5 @@
1
1
  import type { ApiError } from '../models/interfaces';
2
- import { logger } from './loggerUtils';
2
+ import { logger } from '../logger';
3
3
 
4
4
  /**
5
5
  * Error handling utilities for consistent error processing
@@ -17,7 +17,7 @@
17
17
  */
18
18
 
19
19
  import { isNodeJS, isReactNative, loadExpoCrypto, loadNodeCrypto, sha256 } from '@oxyhq/protocol';
20
- import { logger } from './loggerUtils';
20
+ import { logger } from '../logger';
21
21
 
22
22
  /** The central Oxy IdP authorization endpoint used by default. */
23
23
  export const OXY_AUTHORIZE_URL = 'https://auth.oxy.so/authorize';
@@ -169,7 +169,7 @@ export type LogLevel = 'none' | 'error' | 'warn' | 'info' | 'debug';
169
169
  * Simple logger with level support
170
170
  *
171
171
  * Lightweight logger for HTTP clients and utilities.
172
- * For more advanced logging, use loggerUtils.ts
172
+ * For structured, namespaced, level-gated logging, use `@oxyhq/core/logger`.
173
173
  *
174
174
  * @example
175
175
  * ```typescript
@@ -1,80 +0,0 @@
1
- "use strict";
2
- /**
3
- * Debug Utilities
4
- *
5
- * Provides safe logging functions that only output in development mode.
6
- * All logs are stripped in production builds.
7
- *
8
- * @module shared/utils/debugUtils
9
- */
10
- Object.defineProperty(exports, "__esModule", { value: true });
11
- exports.createDebugLogger = exports.debugError = exports.debugWarn = exports.debugLog = exports.isDev = void 0;
12
- /**
13
- * Check if running in development mode
14
- */
15
- const isDev = () => {
16
- if (typeof __DEV__ !== 'undefined')
17
- return __DEV__;
18
- try {
19
- return typeof process !== 'undefined' && process.env?.NODE_ENV === 'development';
20
- }
21
- catch {
22
- return false;
23
- }
24
- };
25
- exports.isDev = isDev;
26
- /**
27
- * Log a debug message (only in development)
28
- * @param prefix - Log prefix (e.g., '[ColdBoot]')
29
- * @param args - Arguments to log
30
- */
31
- const debugLog = (prefix, ...args) => {
32
- if ((0, exports.isDev)()) {
33
- console.log(prefix, ...args);
34
- }
35
- };
36
- exports.debugLog = debugLog;
37
- /**
38
- * Log a debug warning (only in development)
39
- * @param prefix - Log prefix
40
- * @param args - Arguments to log
41
- */
42
- const debugWarn = (prefix, ...args) => {
43
- if ((0, exports.isDev)()) {
44
- console.warn(prefix, ...args);
45
- }
46
- };
47
- exports.debugWarn = debugWarn;
48
- /**
49
- * Log a debug error (only in development)
50
- * @param prefix - Log prefix
51
- * @param args - Arguments to log
52
- */
53
- const debugError = (prefix, ...args) => {
54
- if ((0, exports.isDev)()) {
55
- console.error(prefix, ...args);
56
- }
57
- };
58
- exports.debugError = debugError;
59
- /**
60
- * Create a namespaced debug logger
61
- * @param namespace - Logger namespace (e.g., 'ColdBoot', 'DeviceAuth')
62
- * @returns Object with log, warn, error methods
63
- *
64
- * @example
65
- * ```ts
66
- * const debug = createDebugLogger('ColdBoot');
67
- * debug.log('Starting authentication');
68
- * debug.warn('Token expires soon');
69
- * debug.error('Authentication failed', error);
70
- * ```
71
- */
72
- const createDebugLogger = (namespace) => {
73
- const prefix = `[${namespace}]`;
74
- return {
75
- log: (...args) => (0, exports.debugLog)(prefix, ...args),
76
- warn: (...args) => (0, exports.debugWarn)(prefix, ...args),
77
- error: (...args) => (0, exports.debugError)(prefix, ...args),
78
- };
79
- };
80
- exports.createDebugLogger = createDebugLogger;
@@ -1,126 +0,0 @@
1
- "use strict";
2
- /**
3
- * Centralized logging utilities for consistent logging across the application
4
- */
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.logPerformance = exports.logError = exports.logPayment = exports.logDevice = exports.logUser = exports.logSession = exports.logApi = exports.logAuth = exports.logger = exports.LogLevel = void 0;
7
- var LogLevel;
8
- (function (LogLevel) {
9
- LogLevel[LogLevel["DEBUG"] = 0] = "DEBUG";
10
- LogLevel[LogLevel["INFO"] = 1] = "INFO";
11
- LogLevel[LogLevel["WARN"] = 2] = "WARN";
12
- LogLevel[LogLevel["ERROR"] = 3] = "ERROR";
13
- LogLevel[LogLevel["NONE"] = 4] = "NONE";
14
- })(LogLevel || (exports.LogLevel = LogLevel = {}));
15
- class Logger {
16
- constructor() {
17
- this.level = LogLevel.INFO;
18
- this.isDevelopment = process.env.NODE_ENV === 'development';
19
- }
20
- setLevel(level) {
21
- this.level = level;
22
- }
23
- shouldLog(level) {
24
- return level >= this.level;
25
- }
26
- formatMessage(level, message, context) {
27
- const timestamp = new Date().toISOString();
28
- const contextStr = context ? ` [${Object.entries(context).map(([k, v]) => `${k}:${v}`).join(', ')}]` : '';
29
- return `[${timestamp}] ${level}${contextStr}: ${message}`;
30
- }
31
- debug(message, context, ...args) {
32
- if (this.shouldLog(LogLevel.DEBUG)) {
33
- const formattedMessage = this.formatMessage('DEBUG', message, context);
34
- if (this.isDevelopment) {
35
- console.log(formattedMessage, ...args);
36
- }
37
- }
38
- }
39
- info(message, context, ...args) {
40
- if (this.shouldLog(LogLevel.INFO)) {
41
- const formattedMessage = this.formatMessage('INFO', message, context);
42
- console.log(formattedMessage, ...args);
43
- }
44
- }
45
- warn(message, context, ...args) {
46
- if (this.shouldLog(LogLevel.WARN)) {
47
- const formattedMessage = this.formatMessage('WARN', message, context);
48
- console.warn(formattedMessage, ...args);
49
- }
50
- }
51
- error(message, error, context, ...args) {
52
- if (this.shouldLog(LogLevel.ERROR)) {
53
- const formattedMessage = this.formatMessage('ERROR', message, context);
54
- if (error) {
55
- console.error(formattedMessage, error, ...args);
56
- }
57
- else {
58
- console.error(formattedMessage, ...args);
59
- }
60
- }
61
- }
62
- // Specialized logging methods for common patterns
63
- auth(message, context, ...args) {
64
- this.info(`🔐 ${message}`, { ...context, category: 'auth' }, ...args);
65
- }
66
- api(message, context, ...args) {
67
- this.info(`🌐 ${message}`, { ...context, category: 'api' }, ...args);
68
- }
69
- session(message, context, ...args) {
70
- this.info(`📱 ${message}`, { ...context, category: 'session' }, ...args);
71
- }
72
- user(message, context, ...args) {
73
- this.info(`👤 ${message}`, { ...context, category: 'user' }, ...args);
74
- }
75
- device(message, context, ...args) {
76
- this.info(`📱 ${message}`, { ...context, category: 'device' }, ...args);
77
- }
78
- payment(message, context, ...args) {
79
- this.info(`💳 ${message}`, { ...context, category: 'payment' }, ...args);
80
- }
81
- // Performance logging
82
- performance(operation, duration, context) {
83
- const level = duration > 1000 ? LogLevel.WARN : LogLevel.INFO;
84
- const message = `⏱️ ${operation} completed in ${duration}ms`;
85
- if (level === LogLevel.WARN) {
86
- this.warn(message, { ...context, category: 'performance', duration });
87
- }
88
- else {
89
- this.info(message, { ...context, category: 'performance', duration });
90
- }
91
- }
92
- // Error logging with stack trace
93
- errorWithStack(message, error, context) {
94
- this.error(message, error, { ...context, stack: error.stack });
95
- }
96
- // Group related log messages
97
- group(label, fn) {
98
- if (this.isDevelopment && this.shouldLog(LogLevel.DEBUG)) {
99
- console.group(label);
100
- fn();
101
- console.groupEnd();
102
- }
103
- else {
104
- fn();
105
- }
106
- }
107
- }
108
- // Create singleton instance
109
- exports.logger = new Logger();
110
- // Convenience functions for common logging patterns
111
- const logAuth = (message, context, ...args) => exports.logger.auth(message, context, ...args);
112
- exports.logAuth = logAuth;
113
- const logApi = (message, context, ...args) => exports.logger.api(message, context, ...args);
114
- exports.logApi = logApi;
115
- const logSession = (message, context, ...args) => exports.logger.session(message, context, ...args);
116
- exports.logSession = logSession;
117
- const logUser = (message, context, ...args) => exports.logger.user(message, context, ...args);
118
- exports.logUser = logUser;
119
- const logDevice = (message, context, ...args) => exports.logger.device(message, context, ...args);
120
- exports.logDevice = logDevice;
121
- const logPayment = (message, context, ...args) => exports.logger.payment(message, context, ...args);
122
- exports.logPayment = logPayment;
123
- const logError = (message, error, context, ...args) => exports.logger.error(message, error, context, ...args);
124
- exports.logError = logError;
125
- const logPerformance = (operation, duration, context) => exports.logger.performance(operation, duration, context);
126
- exports.logPerformance = logPerformance;