@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.
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/boot/sessionColdBoot.js +3 -3
- package/dist/cjs/crypto/keyManager.js +68 -69
- package/dist/cjs/crypto/signatureService.js +2 -2
- package/dist/cjs/index.js +13 -21
- package/dist/cjs/logger/index.js +149 -0
- package/dist/cjs/mixins/OxyServices.assets.js +4 -4
- package/dist/cjs/mixins/OxyServices.auth.js +2 -2
- package/dist/cjs/mixins/OxyServices.language.js +3 -5
- package/dist/cjs/mixins/OxyServices.privacy.js +2 -4
- package/dist/cjs/mixins/OxyServices.security.js +7 -11
- package/dist/cjs/mixins/OxyServices.topics.js +25 -15
- package/dist/cjs/mixins/OxyServices.user.js +4 -4
- package/dist/cjs/mixins/OxyServices.utility.js +18 -18
- package/dist/cjs/server/rateLimit.js +100 -6
- package/dist/cjs/session/SessionClient.js +17 -17
- package/dist/cjs/session/accountDialogController.js +12 -12
- package/dist/cjs/session/authStateStore.js +5 -5
- package/dist/cjs/session/refresh.js +4 -4
- package/dist/cjs/session/socketLoader.js +2 -2
- package/dist/cjs/utils/asyncUtils.js +2 -2
- package/dist/cjs/utils/avatarUtils.js +2 -1
- package/dist/cjs/utils/deviceManager.js +7 -5
- package/dist/cjs/utils/errorUtils.js +3 -3
- package/dist/cjs/utils/oauthPkce.js +3 -3
- package/dist/cjs/utils/requestUtils.js +1 -1
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/boot/sessionColdBoot.js +1 -1
- package/dist/esm/crypto/keyManager.js +1 -2
- package/dist/esm/crypto/signatureService.js +1 -1
- package/dist/esm/index.js +2 -3
- package/dist/esm/logger/index.js +140 -0
- package/dist/esm/mixins/OxyServices.assets.js +2 -2
- package/dist/esm/mixins/OxyServices.auth.js +1 -1
- package/dist/esm/mixins/OxyServices.language.js +3 -5
- package/dist/esm/mixins/OxyServices.privacy.js +2 -4
- package/dist/esm/mixins/OxyServices.security.js +7 -11
- package/dist/esm/mixins/OxyServices.topics.js +25 -15
- package/dist/esm/mixins/OxyServices.user.js +1 -1
- package/dist/esm/mixins/OxyServices.utility.js +1 -1
- package/dist/esm/server/rateLimit.js +100 -6
- package/dist/esm/session/SessionClient.js +1 -1
- package/dist/esm/session/accountDialogController.js +1 -1
- package/dist/esm/session/authStateStore.js +1 -1
- package/dist/esm/session/refresh.js +1 -1
- package/dist/esm/session/socketLoader.js +1 -1
- package/dist/esm/utils/asyncUtils.js +1 -1
- package/dist/esm/utils/avatarUtils.js +2 -1
- package/dist/esm/utils/deviceManager.js +7 -5
- package/dist/esm/utils/errorUtils.js +1 -1
- package/dist/esm/utils/oauthPkce.js +1 -1
- package/dist/esm/utils/requestUtils.js +1 -1
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/index.d.ts +3 -4
- package/dist/types/logger/index.d.ts +104 -0
- package/dist/types/mixins/OxyServices.topics.d.ts +3 -3
- package/dist/types/models/Topic.d.ts +10 -0
- package/dist/types/utils/requestUtils.d.ts +1 -1
- package/package.json +16 -1
- package/src/boot/sessionColdBoot.ts +1 -1
- package/src/crypto/keyManager.ts +1 -2
- package/src/crypto/signatureService.ts +1 -1
- package/src/index.ts +17 -19
- package/src/logger/__tests__/logger.test.ts +207 -0
- package/src/logger/index.ts +217 -0
- package/src/mixins/OxyServices.assets.ts +3 -3
- package/src/mixins/OxyServices.auth.ts +1 -1
- package/src/mixins/OxyServices.language.ts +3 -5
- package/src/mixins/OxyServices.privacy.ts +2 -4
- package/src/mixins/OxyServices.security.ts +7 -11
- package/src/mixins/OxyServices.topics.ts +47 -17
- package/src/mixins/OxyServices.user.ts +1 -1
- package/src/mixins/OxyServices.utility.ts +1 -1
- package/src/mixins/__tests__/discoveryErrorHandling.test.ts +1 -1
- package/src/mixins/__tests__/topics.test.ts +156 -0
- package/src/models/Topic.ts +11 -0
- package/src/server/__tests__/rateLimit.test.ts +98 -4
- package/src/server/rateLimit.ts +105 -6
- package/src/session/SessionClient.ts +1 -1
- package/src/session/__tests__/SessionClient.diagnostics.test.ts +1 -1
- package/src/session/__tests__/accountDialogController.test.ts +1 -1
- package/src/session/accountDialogController.ts +1 -1
- package/src/session/authStateStore.ts +1 -1
- package/src/session/refresh.ts +1 -1
- package/src/session/socketLoader.ts +1 -1
- package/src/utils/asyncUtils.ts +1 -1
- package/src/utils/avatarUtils.ts +3 -1
- package/src/utils/deviceManager.ts +8 -5
- package/src/utils/errorUtils.ts +1 -1
- package/src/utils/oauthPkce.ts +1 -1
- package/src/utils/requestUtils.ts +1 -1
- package/dist/cjs/shared/utils/debugUtils.js +0 -80
- package/dist/cjs/utils/loggerUtils.js +0 -126
- package/dist/esm/shared/utils/debugUtils.js +0 -72
- package/dist/esm/utils/loggerUtils.js +0 -115
- package/dist/types/shared/utils/debugUtils.d.ts +0 -48
- package/dist/types/utils/loggerUtils.d.ts +0 -48
- package/src/shared/utils/__tests__/debugUtils.test.ts +0 -55
- package/src/shared/utils/debugUtils.ts +0 -78
- package/src/utils/loggerUtils.ts +0 -153
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import { runColdBoot } from '../utils/coldBoot.js';
|
|
24
24
|
import { isNative as detectNative } from '../utils/platform.js';
|
|
25
|
-
import { logger } from '../
|
|
25
|
+
import { logger } from '../logger/index.js';
|
|
26
26
|
import { TOKEN_REFRESH_LEAD_MS, refreshDeviceSecretArm } from '../session/refresh.js';
|
|
27
27
|
/**
|
|
28
28
|
* Run the device-first cold boot. Resolves to the `runColdBoot` outcome and, as
|
|
@@ -8,8 +8,7 @@ import _cjs_elliptic from 'elliptic';
|
|
|
8
8
|
const { ec: EC } = _cjs_elliptic;
|
|
9
9
|
import { isWeb, isIOS, isAndroid } from '../utils/platform.js';
|
|
10
10
|
import { isReactNative, isNodeJS, loadExpoCrypto, loadNodeCrypto, loadSecureStore, loadSharedIdentityBridge } from '@oxyhq/protocol';
|
|
11
|
-
import { logger } from '../
|
|
12
|
-
import { isDev } from '../shared/utils/debugUtils.js';
|
|
11
|
+
import { isDev, logger } from '../logger/index.js';
|
|
13
12
|
/**
|
|
14
13
|
* Thrown when an identity-mutating operation (createIdentity / importKeyPair)
|
|
15
14
|
* is invoked while a valid identity already exists on the device.
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { signEnvelope, signMessage, verifySignature, sha256, loadExpoCrypto, loadNodeCrypto, isReactNative, isNodeJS, } from '@oxyhq/protocol';
|
|
11
11
|
import { KeyManager } from './keyManager.js';
|
|
12
|
-
import { logger } from '../
|
|
12
|
+
import { logger } from '../logger/index.js';
|
|
13
13
|
export class SignatureService {
|
|
14
14
|
/**
|
|
15
15
|
* Generate a random challenge string (for offline use)
|
package/dist/esm/index.js
CHANGED
|
@@ -90,7 +90,6 @@ export { normalizeTheme, normalizeColorScheme, getOppositeTheme, systemPrefersDa
|
|
|
90
90
|
// ---------------------------------------------------------------------------
|
|
91
91
|
export { HttpStatus, getErrorStatus, getErrorMessage, isAlreadyRegisteredError, isUnauthorizedError, isForbiddenError, isNotFoundError, isRateLimitError, isServerError, isNetworkError, isRetryableError, } from './shared/utils/errorUtils.js';
|
|
92
92
|
export { DEFAULT_CIRCUIT_BREAKER_CONFIG, createCircuitBreakerState, calculateBackoffInterval, recordFailure, recordSuccess, shouldAllowRequest, delay, withRetry, } from './shared/utils/networkUtils.js';
|
|
93
|
-
export { isDev, debugLog, debugWarn, debugError, createDebugLogger, } from './shared/utils/debugUtils.js';
|
|
94
93
|
// ---------------------------------------------------------------------------
|
|
95
94
|
// i18n
|
|
96
95
|
// ---------------------------------------------------------------------------
|
|
@@ -110,9 +109,9 @@ export { EMAIL_REGEX, USERNAME_REGEX, PASSWORD_REGEX, isValidEmail, isValidUsern
|
|
|
110
109
|
// ---------------------------------------------------------------------------
|
|
111
110
|
export { normalizeInlineText, normalizeMultilineText, } from './utils/textNormalization.js';
|
|
112
111
|
// ---------------------------------------------------------------------------
|
|
113
|
-
// Logging
|
|
112
|
+
// Logging — the ecosystem-wide chokepoint (also at subpath `@oxyhq/core/logger`)
|
|
114
113
|
// ---------------------------------------------------------------------------
|
|
115
|
-
export { logger,
|
|
114
|
+
export { logger, createLogger, configureLogger, getLoggerConfig, resetLoggerConfig, consoleSink, isDev, } from './logger/index.js';
|
|
116
115
|
// ---------------------------------------------------------------------------
|
|
117
116
|
// Avatars
|
|
118
117
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,140 @@
|
|
|
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
|
+
const LEVEL_WEIGHT = {
|
|
22
|
+
silent: 0,
|
|
23
|
+
error: 1,
|
|
24
|
+
warn: 2,
|
|
25
|
+
info: 3,
|
|
26
|
+
debug: 4,
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* True in development. Uses React Native's `__DEV__` when present, otherwise
|
|
30
|
+
* falls back to `process.env.NODE_ENV === 'development'`. Never throws.
|
|
31
|
+
*/
|
|
32
|
+
export function isDev() {
|
|
33
|
+
if (typeof __DEV__ !== 'undefined')
|
|
34
|
+
return __DEV__ === true;
|
|
35
|
+
try {
|
|
36
|
+
return typeof process !== 'undefined' && process.env?.NODE_ENV === 'development';
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function formatPrefix(entry) {
|
|
43
|
+
const parts = [entry.timestamp, entry.level.toUpperCase()];
|
|
44
|
+
if (entry.namespace)
|
|
45
|
+
parts.push(`[${entry.namespace}]`);
|
|
46
|
+
return parts.join(' ');
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Default sink: routes each entry to the matching `console` method. `info` and
|
|
50
|
+
* `debug` go to `console.log` (RN/browser-safe — `console.debug`/`console.info`
|
|
51
|
+
* are inconsistently surfaced across runtimes).
|
|
52
|
+
*/
|
|
53
|
+
export const consoleSink = (entry) => {
|
|
54
|
+
const extras = [];
|
|
55
|
+
if (entry.context && Object.keys(entry.context).length > 0)
|
|
56
|
+
extras.push(entry.context);
|
|
57
|
+
if (entry.error !== undefined)
|
|
58
|
+
extras.push(entry.error);
|
|
59
|
+
if (entry.args.length > 0)
|
|
60
|
+
extras.push(...entry.args);
|
|
61
|
+
const line = `${formatPrefix(entry)} ${entry.message}`;
|
|
62
|
+
if (entry.level === 'error')
|
|
63
|
+
console.error(line, ...extras);
|
|
64
|
+
else if (entry.level === 'warn')
|
|
65
|
+
console.warn(line, ...extras);
|
|
66
|
+
else
|
|
67
|
+
console.log(line, ...extras);
|
|
68
|
+
};
|
|
69
|
+
function defaultLevel() {
|
|
70
|
+
return isDev() ? 'debug' : 'info';
|
|
71
|
+
}
|
|
72
|
+
const config = {
|
|
73
|
+
level: defaultLevel(),
|
|
74
|
+
sink: consoleSink,
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Update the global logger configuration. Affects every logger instance
|
|
78
|
+
* (root and children) immediately. Pass `{ level }` to gate output and/or
|
|
79
|
+
* `{ sink }` to redirect transport (pino, CloudWatch, a test capture, …).
|
|
80
|
+
*/
|
|
81
|
+
export function configureLogger(partial) {
|
|
82
|
+
if (partial.level !== undefined)
|
|
83
|
+
config.level = partial.level;
|
|
84
|
+
if (partial.sink !== undefined)
|
|
85
|
+
config.sink = partial.sink;
|
|
86
|
+
}
|
|
87
|
+
/** Read a snapshot of the current global configuration. */
|
|
88
|
+
export function getLoggerConfig() {
|
|
89
|
+
return { level: config.level, sink: config.sink };
|
|
90
|
+
}
|
|
91
|
+
/** Restore the default level (env-derived) and the console sink. */
|
|
92
|
+
export function resetLoggerConfig() {
|
|
93
|
+
config.level = defaultLevel();
|
|
94
|
+
config.sink = consoleSink;
|
|
95
|
+
}
|
|
96
|
+
function mergeContext(base, extra) {
|
|
97
|
+
if (!base)
|
|
98
|
+
return extra;
|
|
99
|
+
if (!extra)
|
|
100
|
+
return base;
|
|
101
|
+
return { ...base, ...extra };
|
|
102
|
+
}
|
|
103
|
+
function emit(level, namespace, baseContext, message, error, context, args) {
|
|
104
|
+
if (LEVEL_WEIGHT[level] > LEVEL_WEIGHT[config.level])
|
|
105
|
+
return;
|
|
106
|
+
config.sink({
|
|
107
|
+
level,
|
|
108
|
+
message,
|
|
109
|
+
namespace,
|
|
110
|
+
context: mergeContext(baseContext, context),
|
|
111
|
+
error,
|
|
112
|
+
args,
|
|
113
|
+
timestamp: new Date().toISOString(),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
function makeLogger(namespace, baseContext) {
|
|
117
|
+
return {
|
|
118
|
+
namespace,
|
|
119
|
+
debug: (message, context, ...args) => emit('debug', namespace, baseContext, message, undefined, context, args),
|
|
120
|
+
info: (message, context, ...args) => emit('info', namespace, baseContext, message, undefined, context, args),
|
|
121
|
+
warn: (message, context, ...args) => emit('warn', namespace, baseContext, message, undefined, context, args),
|
|
122
|
+
error: (message, error, context, ...args) => emit('error', namespace, baseContext, message, error, context, args),
|
|
123
|
+
child: (childNamespace, childContext) => makeLogger(namespace ? `${namespace}:${childNamespace}` : childNamespace, mergeContext(baseContext, childContext)),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/** The shared root logger. Prefer a namespaced `createLogger(...)` in modules. */
|
|
127
|
+
export const logger = makeLogger(undefined, undefined);
|
|
128
|
+
/**
|
|
129
|
+
* Create a namespaced logger.
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* ```ts
|
|
133
|
+
* const log = createLogger('mention:feed');
|
|
134
|
+
* log.info('loaded', { count: 20 });
|
|
135
|
+
* const sub = log.child('prefetch'); // namespace → 'mention:feed:prefetch'
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
export function createLogger(namespace, context) {
|
|
139
|
+
return makeLogger(namespace, context);
|
|
140
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isReactNative } from '@oxyhq/protocol';
|
|
2
|
-
import { logger } from '../
|
|
2
|
+
import { logger } from '../logger/index.js';
|
|
3
3
|
import { extractErrorStatus } from '../utils/errorUtils.js';
|
|
4
4
|
/**
|
|
5
5
|
* Maximum number of ids sent per `POST /assets/service/by-ids` request. Matches
|
|
@@ -359,7 +359,7 @@ export function OxyServicesAssetsMixin(Base) {
|
|
|
359
359
|
return response;
|
|
360
360
|
}
|
|
361
361
|
catch (error) {
|
|
362
|
-
|
|
362
|
+
logger.error('File upload error', error, { component: 'OxyServices.assets' });
|
|
363
363
|
let errorMessage = 'File upload failed';
|
|
364
364
|
if (error instanceof Error) {
|
|
365
365
|
errorMessage = error.message || errorMessage;
|
|
@@ -3,7 +3,7 @@ import { OxyAuthenticationError } from '../OxyServices.errors.js';
|
|
|
3
3
|
import { KeyManager } from '../crypto/keyManager.js';
|
|
4
4
|
import { SignatureService } from '../crypto/signatureService.js';
|
|
5
5
|
import { loadNodeCrypto } from '@oxyhq/protocol';
|
|
6
|
-
import { logger } from '../
|
|
6
|
+
import { logger } from '../logger/index.js';
|
|
7
7
|
import { normalizeUserIdentity, normalizeUserIdentityOrNull } from '../utils/userIdentity.js';
|
|
8
8
|
/**
|
|
9
9
|
* Default lifetime of a "Sign in with Oxy" device-flow session / authorize code.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { normalizeLocale, getPrimaryLanguage, getLanguageMetadata, getLanguageName, getNativeLanguageName } from '../utils/languageUtils.js';
|
|
5
5
|
import { loadAsyncStorage } from '@oxyhq/protocol';
|
|
6
|
-
import {
|
|
6
|
+
import { logger } from '../logger/index.js';
|
|
7
7
|
export function OxyServicesLanguageMixin(Base) {
|
|
8
8
|
return class extends Base {
|
|
9
9
|
constructor(...args) {
|
|
@@ -28,7 +28,7 @@ export function OxyServicesLanguageMixin(Base) {
|
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
30
|
catch (error) {
|
|
31
|
-
|
|
31
|
+
logger.error('AsyncStorage not available in React Native', error, { component: 'OxyServices.language' });
|
|
32
32
|
throw new Error('AsyncStorage is required in React Native environment');
|
|
33
33
|
}
|
|
34
34
|
}
|
|
@@ -83,9 +83,7 @@ export function OxyServicesLanguageMixin(Base) {
|
|
|
83
83
|
return null;
|
|
84
84
|
}
|
|
85
85
|
catch (error) {
|
|
86
|
-
|
|
87
|
-
console.warn('Failed to get current language:', error);
|
|
88
|
-
}
|
|
86
|
+
logger.warn('Failed to get current language', { component: 'OxyServices.language' }, error);
|
|
89
87
|
return null;
|
|
90
88
|
}
|
|
91
89
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { logger } from '../logger/index.js';
|
|
2
2
|
export function OxyServicesPrivacyMixin(Base) {
|
|
3
3
|
return class extends Base {
|
|
4
4
|
constructor(...args) {
|
|
@@ -26,9 +26,7 @@ export function OxyServicesPrivacyMixin(Base) {
|
|
|
26
26
|
}
|
|
27
27
|
catch (error) {
|
|
28
28
|
// If there's an error, assume not in list to avoid breaking functionality
|
|
29
|
-
|
|
30
|
-
console.warn('Error checking user list:', error);
|
|
31
|
-
}
|
|
29
|
+
logger.warn('Error checking user list', { component: 'OxyServices.privacy' }, error);
|
|
32
30
|
return false;
|
|
33
31
|
}
|
|
34
32
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { logger } from '../logger/index.js';
|
|
2
2
|
export function OxyServicesSecurityMixin(Base) {
|
|
3
3
|
return class extends Base {
|
|
4
4
|
constructor(...args) {
|
|
@@ -62,11 +62,9 @@ export function OxyServicesSecurityMixin(Base) {
|
|
|
62
62
|
await this.makeRequest('POST', '/security/activity/private-key-exported', { deviceId }, { cache: false });
|
|
63
63
|
}
|
|
64
64
|
catch (error) {
|
|
65
|
-
// Don't throw - logging failures shouldn't break user flow
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
console.warn('[OxyServices] Failed to log private key exported event:', error);
|
|
69
|
-
}
|
|
65
|
+
// Don't throw - logging failures shouldn't break user flow, but surface
|
|
66
|
+
// for monitoring via the shared logger sink.
|
|
67
|
+
logger.warn('[OxyServices] Failed to log private key exported event', { component: 'OxyServices.security' }, error);
|
|
70
68
|
}
|
|
71
69
|
}
|
|
72
70
|
/**
|
|
@@ -79,11 +77,9 @@ export function OxyServicesSecurityMixin(Base) {
|
|
|
79
77
|
await this.makeRequest('POST', '/security/activity/backup-created', { deviceId }, { cache: false });
|
|
80
78
|
}
|
|
81
79
|
catch (error) {
|
|
82
|
-
// Don't throw - logging failures shouldn't break user flow
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
console.warn('[OxyServices] Failed to log backup created event:', error);
|
|
86
|
-
}
|
|
80
|
+
// Don't throw - logging failures shouldn't break user flow, but surface
|
|
81
|
+
// for monitoring via the shared logger sink.
|
|
82
|
+
logger.warn('[OxyServices] Failed to log backup created event', { component: 'OxyServices.security' }, error);
|
|
87
83
|
}
|
|
88
84
|
}
|
|
89
85
|
};
|
|
@@ -14,10 +14,10 @@ export function OxyServicesTopicsMixin(Base) {
|
|
|
14
14
|
const params = {};
|
|
15
15
|
if (locale)
|
|
16
16
|
params.locale = locale;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
// `GET /topics/categories` returns `{ categories: TopicData[] }` — the
|
|
18
|
+
// SDK's `unwrapResponse` only unwraps `{ data }`, so unwrap here.
|
|
19
|
+
const response = await this.makeRequest('GET', '/topics/categories', params, { cache: true, cacheTTL: CACHE_TIMES.EXTRA_LONG });
|
|
20
|
+
return response.categories ?? [];
|
|
21
21
|
}
|
|
22
22
|
catch (error) {
|
|
23
23
|
throw this.handleError(error);
|
|
@@ -34,9 +34,9 @@ export function OxyServicesTopicsMixin(Base) {
|
|
|
34
34
|
const params = { q: query };
|
|
35
35
|
if (limit)
|
|
36
36
|
params.limit = limit;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
// `GET /topics/search` returns `{ topics: TopicData[] }` — unwrap it.
|
|
38
|
+
const response = await this.makeRequest('GET', '/topics/search', params, { cache: false });
|
|
39
|
+
return response.topics ?? [];
|
|
40
40
|
}
|
|
41
41
|
catch (error) {
|
|
42
42
|
throw this.handleError(error);
|
|
@@ -45,7 +45,7 @@ export function OxyServicesTopicsMixin(Base) {
|
|
|
45
45
|
/**
|
|
46
46
|
* List topics with optional filters
|
|
47
47
|
* @param options - Filter and pagination options
|
|
48
|
-
* @returns
|
|
48
|
+
* @returns Paginated topics envelope (`topics` plus `total`/`limit`/`offset`)
|
|
49
49
|
*/
|
|
50
50
|
async listTopics(options) {
|
|
51
51
|
try {
|
|
@@ -60,10 +60,18 @@ export function OxyServicesTopicsMixin(Base) {
|
|
|
60
60
|
params.offset = options.offset;
|
|
61
61
|
if (options?.locale)
|
|
62
62
|
params.locale = options.locale;
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
});
|
|
63
|
+
// `GET /topics` returns `{ topics, total, limit, offset }`. The pagination
|
|
64
|
+
// fields matter to callers, so return the whole envelope (typed) rather
|
|
65
|
+
// than throwing them away.
|
|
66
|
+
const response = await this.makeRequest('GET', '/topics', params, { cache: true, cacheTTL: CACHE_TIMES.SHORT });
|
|
67
|
+
const requestedLimit = typeof params.limit === 'number' ? params.limit : 0;
|
|
68
|
+
const requestedOffset = typeof params.offset === 'number' ? params.offset : 0;
|
|
69
|
+
return {
|
|
70
|
+
topics: response.topics ?? [],
|
|
71
|
+
total: response.total ?? response.topics?.length ?? 0,
|
|
72
|
+
limit: response.limit ?? requestedLimit,
|
|
73
|
+
offset: response.offset ?? requestedOffset,
|
|
74
|
+
};
|
|
67
75
|
}
|
|
68
76
|
catch (error) {
|
|
69
77
|
throw this.handleError(error);
|
|
@@ -92,9 +100,11 @@ export function OxyServicesTopicsMixin(Base) {
|
|
|
92
100
|
*/
|
|
93
101
|
async resolveTopicNames(names) {
|
|
94
102
|
try {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
103
|
+
// `POST /topics/resolve` returns `{ topics: Record<name, TopicData> }`
|
|
104
|
+
// (a name-keyed map, not an array). Unwrap and flatten to the resolved
|
|
105
|
+
// topics; each TopicData carries its own `name` for re-keying.
|
|
106
|
+
const response = await this.makeRequest('POST', '/topics/resolve', { names }, { cache: false });
|
|
107
|
+
return Object.values(response.topics ?? {});
|
|
98
108
|
}
|
|
99
109
|
catch (error) {
|
|
100
110
|
throw this.handleError(error);
|
|
@@ -3,7 +3,7 @@ import { buildSearchParams, buildPaginationParams } from '../utils/apiUtils.js';
|
|
|
3
3
|
import { KeyManager } from '../crypto/keyManager.js';
|
|
4
4
|
import { SignatureService } from '../crypto/signatureService.js';
|
|
5
5
|
import { normalizeUserIdentity, normalizeUserIdentityOrNull } from '../utils/userIdentity.js';
|
|
6
|
-
import { logger } from '../
|
|
6
|
+
import { logger } from '../logger/index.js';
|
|
7
7
|
import { extractErrorStatus } from '../utils/errorUtils.js';
|
|
8
8
|
/**
|
|
9
9
|
* Maximum number of ids sent per `POST /users/by-ids` request. Matches the
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { jwtDecode } from 'jwt-decode';
|
|
8
8
|
import { loadNodeCrypto } from '@oxyhq/protocol';
|
|
9
9
|
import { buildUrl } from '../utils/apiUtils.js';
|
|
10
|
-
import { logger } from '../
|
|
10
|
+
import { logger } from '../logger/index.js';
|
|
11
11
|
/**
|
|
12
12
|
* Expected JWT audience for tokens issued by the Oxy auth service.
|
|
13
13
|
*/
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createHmac } from 'node:crypto';
|
|
2
|
+
import { isIPv4, isIPv6 } from 'node:net';
|
|
1
3
|
import rateLimit from 'express-rate-limit';
|
|
2
4
|
/**
|
|
3
5
|
* Built-in exemptions. A media app's cover-art/avatar fan-out and HLS
|
|
@@ -15,9 +17,98 @@ function isBuiltInExempt(req) {
|
|
|
15
17
|
path === '/health' ||
|
|
16
18
|
path.endsWith('/health'));
|
|
17
19
|
}
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Anonymous rate-limit keys must be PRIVACY-PRESERVING: the raw client IP must
|
|
22
|
+
* never reach a store at rest (in-memory or Redis). We therefore HMAC-hash the
|
|
23
|
+
* IP into a short, transient-only bucket key. Two IPv6-specific concerns shape
|
|
24
|
+
* the pre-hash normalization:
|
|
25
|
+
*
|
|
26
|
+
* - IPv6 hosts are typically handed an entire /64 (often a /56), so a single
|
|
27
|
+
* host can rotate through an enormous address space and evade a per-address
|
|
28
|
+
* limit. We bucket IPv6 to its /56 prefix BEFORE hashing.
|
|
29
|
+
* - express-rate-limit only exposes an `ipKeyGenerator` /56 helper from v8
|
|
30
|
+
* onwards; `@oxyhq/core` pins v7 (peer `^7.0.0`), so the masking is
|
|
31
|
+
* implemented here rather than pulling a major-version bump of a
|
|
32
|
+
* security-critical dependency (and its rate-limit-redis compatibility) into
|
|
33
|
+
* an unrelated privacy change. This mirrors `packages/api/src/utils/ipKey.ts`.
|
|
34
|
+
*/
|
|
35
|
+
const IPV6_SUBNET_BITS = 56;
|
|
36
|
+
/** Expand an IPv6 literal (handling `::` and embedded IPv4) to 8 numeric hextets, or null if unparseable. */
|
|
37
|
+
function ipv6Hextets(ip) {
|
|
38
|
+
let addr = ip;
|
|
39
|
+
const zone = addr.indexOf('%');
|
|
40
|
+
if (zone !== -1) {
|
|
41
|
+
addr = addr.slice(0, zone);
|
|
42
|
+
}
|
|
43
|
+
// Embedded IPv4 tail (e.g. `::ffff:203.0.113.7`) → fold the dotted quad into two hextets.
|
|
44
|
+
const lastColon = addr.lastIndexOf(':');
|
|
45
|
+
if (lastColon !== -1 && addr.slice(lastColon + 1).includes('.')) {
|
|
46
|
+
const v4 = addr.slice(lastColon + 1);
|
|
47
|
+
if (!isIPv4(v4)) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
const octets = v4.split('.').map((part) => Number.parseInt(part, 10));
|
|
51
|
+
const high = ((octets[0] << 8) | octets[1]).toString(16);
|
|
52
|
+
const low = ((octets[2] << 8) | octets[3]).toString(16);
|
|
53
|
+
addr = `${addr.slice(0, lastColon + 1)}${high}:${low}`;
|
|
54
|
+
}
|
|
55
|
+
const halves = addr.split('::');
|
|
56
|
+
if (halves.length > 2) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const head = halves[0] ? halves[0].split(':') : [];
|
|
60
|
+
const tail = halves.length === 2 && halves[1] ? halves[1].split(':') : [];
|
|
61
|
+
let groups;
|
|
62
|
+
if (halves.length === 1) {
|
|
63
|
+
groups = head;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
const missing = 8 - (head.length + tail.length);
|
|
67
|
+
if (missing < 0) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
groups = [...head, ...new Array(missing).fill('0'), ...tail];
|
|
71
|
+
}
|
|
72
|
+
if (groups.length !== 8) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const hextets = groups.map((group) => Number.parseInt(group || '0', 16));
|
|
76
|
+
if (hextets.some((value) => Number.isNaN(value) || value < 0 || value > 0xffff)) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
return hextets;
|
|
80
|
+
}
|
|
81
|
+
/** Mask an IPv6 address to its /{bits} prefix, returned as a canonical hex string. */
|
|
82
|
+
function maskIPv6(ip, bits) {
|
|
83
|
+
const hextets = ipv6Hextets(ip);
|
|
84
|
+
if (!hextets) {
|
|
85
|
+
return ip;
|
|
86
|
+
}
|
|
87
|
+
const masked = hextets.map((hextet, index) => {
|
|
88
|
+
const groupStart = index * 16;
|
|
89
|
+
if (groupStart >= bits) {
|
|
90
|
+
return 0;
|
|
91
|
+
}
|
|
92
|
+
const keepBits = Math.min(16, bits - groupStart);
|
|
93
|
+
const mask = keepBits >= 16 ? 0xffff : (0xffff << (16 - keepBits)) & 0xffff;
|
|
94
|
+
return hextet & mask;
|
|
95
|
+
});
|
|
96
|
+
return `${masked.map((hextet) => hextet.toString(16)).join(':')}/${bits}`;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Hash a client IP into a privacy-preserving bucket key. IPv6 is bucketed to its
|
|
100
|
+
* /56 prefix first (so a single v6 host can't rotate through its allocation to
|
|
101
|
+
* mint fresh keys), then HMAC'd with the server-side salt. The salt is resolved
|
|
102
|
+
* at CALL time (`IP_HASH_SALT`, else `DEVICE_ID_SALT`, else empty) — an empty
|
|
103
|
+
* salt still hashes, which beats storing a raw IP; backends SHOULD set one of
|
|
104
|
+
* those envs. The `rl|` namespace ensures a rate-limit key can never collide
|
|
105
|
+
* with, or be correlated against, a deviceId derivation that reuses the same
|
|
106
|
+
* salt. The result is a short hex digest with no colons, so it is Redis-safe.
|
|
107
|
+
*/
|
|
108
|
+
function hashAnonymousIp(ip) {
|
|
109
|
+
const normalized = isIPv6(ip) && !ip.startsWith('::ffff:') ? maskIPv6(ip, IPV6_SUBNET_BITS) : ip;
|
|
110
|
+
const salt = process.env.IP_HASH_SALT || process.env.DEVICE_ID_SALT || '';
|
|
111
|
+
return createHmac('sha256', salt).update(`rl|${normalized}`).digest('hex').slice(0, 24);
|
|
21
112
|
}
|
|
22
113
|
/**
|
|
23
114
|
* Resolve the trusted authenticated rate-limit key.
|
|
@@ -43,14 +134,17 @@ function resolveTrustedAuthenticatedKey(req) {
|
|
|
43
134
|
}
|
|
44
135
|
return null;
|
|
45
136
|
}
|
|
46
|
-
/** Resolve the rate-limit key: per trusted authenticated identity, else per (IPv6-
|
|
137
|
+
/** Resolve the rate-limit key: per trusted authenticated identity, else per hashed (IPv6-bucketed) IP. */
|
|
47
138
|
function resolveKey(req) {
|
|
48
139
|
const authenticatedKey = resolveTrustedAuthenticatedKey(req);
|
|
49
140
|
if (authenticatedKey) {
|
|
50
141
|
return authenticatedKey;
|
|
51
142
|
}
|
|
52
|
-
const ip = req.ip || req.socket.remoteAddress
|
|
53
|
-
|
|
143
|
+
const ip = req.ip || req.socket.remoteAddress;
|
|
144
|
+
if (!ip) {
|
|
145
|
+
return 'unknown';
|
|
146
|
+
}
|
|
147
|
+
return hashAnonymousIp(ip);
|
|
54
148
|
}
|
|
55
149
|
/**
|
|
56
150
|
* Build the composed Oxy rate-limit middleware. See module docs for rationale.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { deviceSessionStateSchema, deviceSessionSyncSchema, safeParseContract, SESSION_ACCOUNTS_CHANGED_EVENT, sessionAccountsChangedEventSchema, } from '@oxyhq/contracts';
|
|
2
|
-
import { logger } from '../
|
|
2
|
+
import { logger } from '../logger/index.js';
|
|
3
3
|
import { getSocketIO } from './socketLoader.js';
|
|
4
4
|
/**
|
|
5
5
|
* Same-origin `BroadcastChannel` name for instant, network-free session-state
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* (auth.oxy.so). {@link AccountDialogController.openPasswordAtOxyAuth} only
|
|
26
26
|
* builds the hand-off URL; device-first convergence syncs the session back.
|
|
27
27
|
*/
|
|
28
|
-
import { logger } from '../
|
|
28
|
+
import { logger } from '../logger/index.js';
|
|
29
29
|
import { extractErrorStatus } from '../utils/errorUtils.js';
|
|
30
30
|
import { CENTRAL_IDP_APEX } from '../utils/authWebUrl.js';
|
|
31
31
|
import { generateOAuthState, generatePkcePair, normalizeOAuthRedirectUri, persistOAuthHandshake, } from '../utils/oauthPkce.js';
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*
|
|
17
17
|
* ESM-safe (no `require()`).
|
|
18
18
|
*/
|
|
19
|
-
import { logger } from '../
|
|
19
|
+
import { logger } from '../logger/index.js';
|
|
20
20
|
/**
|
|
21
21
|
* Versioned DURABLE storage key. Holds ONLY the small, re-mint-critical fields
|
|
22
22
|
* (`sessionId`, `userId`, `deviceId`, `deviceSecret`) — never the large JWT
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isNative } from '../utils/platform.js';
|
|
2
2
|
import { extractErrorStatus } from '../utils/errorUtils.js';
|
|
3
|
-
import { logger } from '../
|
|
3
|
+
import { logger } from '../logger/index.js';
|
|
4
4
|
/**
|
|
5
5
|
* Lead time (ms) before access-token expiry at which the proactive scheduler
|
|
6
6
|
* re-mints. Mirrors `HttpService`'s per-request `TOKEN_REFRESH_LEAD_SECONDS`
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Async utilities for common asynchronous patterns and error handling
|
|
3
3
|
*/
|
|
4
|
-
import { logger } from '
|
|
4
|
+
import { logger } from '../logger/index.js';
|
|
5
5
|
/**
|
|
6
6
|
* Wrapper for async operations with automatic error handling
|
|
7
7
|
* Returns null on error instead of throwing
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { logger } from '../logger/index.js';
|
|
1
2
|
/**
|
|
2
3
|
* Updates file visibility to public for avatar use.
|
|
3
4
|
* Logs non-404 errors to help debug upload issues.
|
|
@@ -19,7 +20,7 @@ export async function updateAvatarVisibility(fileId, oxyServices, contextName =
|
|
|
19
20
|
? visError.status
|
|
20
21
|
: undefined;
|
|
21
22
|
if (status !== 404) {
|
|
22
|
-
|
|
23
|
+
logger.error(`[${contextName}] Failed to update avatar visibility for ${fileId}`, visError, { component: contextName });
|
|
23
24
|
}
|
|
24
25
|
}
|
|
25
26
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { loadAsyncStorage } from '@oxyhq/protocol';
|
|
2
|
+
import { createLogger } from '../logger/index.js';
|
|
3
|
+
const log = createLogger('DeviceManager');
|
|
2
4
|
/**
|
|
3
5
|
* Client-side device management utility
|
|
4
6
|
* Handles persistent device identification across app sessions
|
|
@@ -28,7 +30,7 @@ export class DeviceManager {
|
|
|
28
30
|
};
|
|
29
31
|
}
|
|
30
32
|
catch (error) {
|
|
31
|
-
|
|
33
|
+
log.error('AsyncStorage not available in React Native', error);
|
|
32
34
|
throw new Error('AsyncStorage is required in React Native environment');
|
|
33
35
|
}
|
|
34
36
|
}
|
|
@@ -79,7 +81,7 @@ export class DeviceManager {
|
|
|
79
81
|
return await this.createNewDeviceInfo();
|
|
80
82
|
}
|
|
81
83
|
catch (error) {
|
|
82
|
-
|
|
84
|
+
log.error('Error getting device info', error);
|
|
83
85
|
return await this.createNewDeviceInfo();
|
|
84
86
|
}
|
|
85
87
|
}
|
|
@@ -105,7 +107,7 @@ export class DeviceManager {
|
|
|
105
107
|
await storage.setItem(this.DEVICE_KEY, JSON.stringify(deviceInfo));
|
|
106
108
|
}
|
|
107
109
|
catch (error) {
|
|
108
|
-
|
|
110
|
+
log.error('Error saving device info', error);
|
|
109
111
|
}
|
|
110
112
|
}
|
|
111
113
|
/**
|
|
@@ -118,7 +120,7 @@ export class DeviceManager {
|
|
|
118
120
|
await this.saveDeviceInfo(deviceInfo);
|
|
119
121
|
}
|
|
120
122
|
catch (error) {
|
|
121
|
-
|
|
123
|
+
log.error('Error updating device name', error);
|
|
122
124
|
}
|
|
123
125
|
}
|
|
124
126
|
/**
|
|
@@ -130,7 +132,7 @@ export class DeviceManager {
|
|
|
130
132
|
await storage.removeItem(this.DEVICE_KEY);
|
|
131
133
|
}
|
|
132
134
|
catch (error) {
|
|
133
|
-
|
|
135
|
+
log.error('Error clearing device info', error);
|
|
134
136
|
}
|
|
135
137
|
}
|
|
136
138
|
/**
|