@oxyhq/core 1.11.12 → 1.11.14

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 (130) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/CrossDomainAuth.js +3 -1
  3. package/dist/cjs/HttpService.js +214 -33
  4. package/dist/cjs/OxyServices.base.js +9 -0
  5. package/dist/cjs/OxyServices.js +8 -3
  6. package/dist/cjs/crypto/index.js +3 -1
  7. package/dist/cjs/crypto/keyManager.js +476 -172
  8. package/dist/cjs/crypto/polyfill.js +14 -65
  9. package/dist/cjs/crypto/recoveryPhrase.js +30 -11
  10. package/dist/cjs/crypto/signatureService.js +25 -60
  11. package/dist/cjs/i18n/locales/en-US.json +46 -1
  12. package/dist/cjs/i18n/locales/es-ES.json +46 -1
  13. package/dist/cjs/i18n/locales/locales/en-US.json +46 -1
  14. package/dist/cjs/i18n/locales/locales/es-ES.json +46 -1
  15. package/dist/cjs/index.js +10 -2
  16. package/dist/cjs/mixins/OxyServices.assets.js +9 -4
  17. package/dist/cjs/mixins/OxyServices.auth.js +147 -14
  18. package/dist/cjs/mixins/OxyServices.contacts.js +50 -0
  19. package/dist/cjs/mixins/OxyServices.features.js +0 -11
  20. package/dist/cjs/mixins/OxyServices.fedcm.js +4 -3
  21. package/dist/cjs/mixins/OxyServices.language.js +5 -36
  22. package/dist/cjs/mixins/OxyServices.redirect.js +6 -2
  23. package/dist/cjs/mixins/OxyServices.security.js +13 -2
  24. package/dist/cjs/mixins/OxyServices.user.js +59 -38
  25. package/dist/cjs/mixins/OxyServices.utility.js +416 -110
  26. package/dist/cjs/mixins/index.js +11 -3
  27. package/dist/cjs/utils/accountUtils.js +71 -2
  28. package/dist/cjs/utils/deviceManager.js +5 -36
  29. package/dist/cjs/utils/languageUtils.js +22 -0
  30. package/dist/cjs/utils/platformCrypto.js +165 -0
  31. package/dist/cjs/utils/platformCrypto.native.js +123 -0
  32. package/dist/esm/.tsbuildinfo +1 -1
  33. package/dist/esm/CrossDomainAuth.js +3 -1
  34. package/dist/esm/HttpService.js +215 -34
  35. package/dist/esm/OxyServices.base.js +9 -0
  36. package/dist/esm/OxyServices.js +8 -3
  37. package/dist/esm/crypto/index.js +1 -1
  38. package/dist/esm/crypto/keyManager.js +473 -138
  39. package/dist/esm/crypto/polyfill.js +14 -32
  40. package/dist/esm/crypto/recoveryPhrase.js +30 -11
  41. package/dist/esm/crypto/signatureService.js +25 -27
  42. package/dist/esm/i18n/locales/en-US.json +46 -1
  43. package/dist/esm/i18n/locales/es-ES.json +46 -1
  44. package/dist/esm/i18n/locales/locales/en-US.json +46 -1
  45. package/dist/esm/i18n/locales/locales/es-ES.json +46 -1
  46. package/dist/esm/index.js +4 -3
  47. package/dist/esm/mixins/OxyServices.assets.js +9 -4
  48. package/dist/esm/mixins/OxyServices.auth.js +145 -14
  49. package/dist/esm/mixins/OxyServices.contacts.js +47 -0
  50. package/dist/esm/mixins/OxyServices.features.js +0 -11
  51. package/dist/esm/mixins/OxyServices.fedcm.js +4 -3
  52. package/dist/esm/mixins/OxyServices.language.js +5 -3
  53. package/dist/esm/mixins/OxyServices.redirect.js +6 -2
  54. package/dist/esm/mixins/OxyServices.security.js +13 -2
  55. package/dist/esm/mixins/OxyServices.user.js +59 -38
  56. package/dist/esm/mixins/OxyServices.utility.js +416 -77
  57. package/dist/esm/mixins/index.js +11 -3
  58. package/dist/esm/utils/accountUtils.js +67 -1
  59. package/dist/esm/utils/deviceManager.js +5 -3
  60. package/dist/esm/utils/languageUtils.js +21 -0
  61. package/dist/esm/utils/platformCrypto.js +125 -0
  62. package/dist/esm/utils/platformCrypto.native.js +80 -0
  63. package/dist/types/.tsbuildinfo +1 -1
  64. package/dist/types/HttpService.d.ts +47 -3
  65. package/dist/types/OxyServices.base.d.ts +7 -0
  66. package/dist/types/OxyServices.d.ts +50 -7
  67. package/dist/types/crypto/index.d.ts +1 -1
  68. package/dist/types/crypto/keyManager.d.ts +110 -9
  69. package/dist/types/crypto/polyfill.d.ts +3 -1
  70. package/dist/types/crypto/recoveryPhrase.d.ts +31 -7
  71. package/dist/types/crypto/signatureService.d.ts +4 -0
  72. package/dist/types/index.d.ts +7 -5
  73. package/dist/types/mixins/OxyServices.analytics.d.ts +1 -0
  74. package/dist/types/mixins/OxyServices.assets.d.ts +6 -10
  75. package/dist/types/mixins/OxyServices.auth.d.ts +82 -5
  76. package/dist/types/mixins/OxyServices.contacts.d.ts +99 -0
  77. package/dist/types/mixins/OxyServices.developer.d.ts +1 -0
  78. package/dist/types/mixins/OxyServices.devices.d.ts +1 -0
  79. package/dist/types/mixins/OxyServices.features.d.ts +2 -7
  80. package/dist/types/mixins/OxyServices.fedcm.d.ts +1 -0
  81. package/dist/types/mixins/OxyServices.karma.d.ts +1 -0
  82. package/dist/types/mixins/OxyServices.language.d.ts +1 -0
  83. package/dist/types/mixins/OxyServices.location.d.ts +1 -0
  84. package/dist/types/mixins/OxyServices.managedAccounts.d.ts +1 -0
  85. package/dist/types/mixins/OxyServices.payment.d.ts +1 -0
  86. package/dist/types/mixins/OxyServices.popup.d.ts +1 -0
  87. package/dist/types/mixins/OxyServices.privacy.d.ts +1 -0
  88. package/dist/types/mixins/OxyServices.redirect.d.ts +1 -0
  89. package/dist/types/mixins/OxyServices.security.d.ts +1 -0
  90. package/dist/types/mixins/OxyServices.topics.d.ts +1 -0
  91. package/dist/types/mixins/OxyServices.user.d.ts +28 -11
  92. package/dist/types/mixins/OxyServices.utility.d.ts +145 -10
  93. package/dist/types/mixins/index.d.ts +52 -4
  94. package/dist/types/models/interfaces.d.ts +62 -3
  95. package/dist/types/utils/accountUtils.d.ts +41 -1
  96. package/dist/types/utils/languageUtils.d.ts +1 -0
  97. package/dist/types/utils/platformCrypto.d.ts +87 -0
  98. package/dist/types/utils/platformCrypto.native.d.ts +54 -0
  99. package/package.json +45 -2
  100. package/src/CrossDomainAuth.ts +12 -10
  101. package/src/HttpService.ts +251 -40
  102. package/src/OxyServices.base.ts +10 -0
  103. package/src/OxyServices.ts +26 -7
  104. package/src/crypto/__tests__/keyManager.test.ts +336 -0
  105. package/src/crypto/index.ts +6 -1
  106. package/src/crypto/keyManager.ts +529 -151
  107. package/src/crypto/polyfill.ts +14 -34
  108. package/src/crypto/recoveryPhrase.ts +56 -17
  109. package/src/crypto/signatureService.ts +25 -30
  110. package/src/i18n/locales/en-US.json +46 -1
  111. package/src/i18n/locales/es-ES.json +46 -1
  112. package/src/index.ts +19 -4
  113. package/src/mixins/OxyServices.assets.ts +15 -11
  114. package/src/mixins/OxyServices.auth.ts +175 -15
  115. package/src/mixins/OxyServices.contacts.ts +73 -0
  116. package/src/mixins/OxyServices.features.ts +2 -12
  117. package/src/mixins/OxyServices.fedcm.ts +4 -3
  118. package/src/mixins/OxyServices.language.ts +6 -4
  119. package/src/mixins/OxyServices.redirect.ts +6 -2
  120. package/src/mixins/OxyServices.security.ts +18 -8
  121. package/src/mixins/OxyServices.user.ts +72 -49
  122. package/src/mixins/OxyServices.utility.ts +562 -89
  123. package/src/mixins/__tests__/serviceAuth.test.ts +623 -0
  124. package/src/mixins/index.ts +58 -7
  125. package/src/models/interfaces.ts +65 -3
  126. package/src/utils/accountUtils.ts +82 -2
  127. package/src/utils/deviceManager.ts +7 -4
  128. package/src/utils/languageUtils.ts +23 -2
  129. package/src/utils/platformCrypto.native.ts +101 -0
  130. package/src/utils/platformCrypto.ts +145 -0
@@ -7,6 +7,8 @@
7
7
  import { jwtDecode } from 'jwt-decode';
8
8
  import type { ApiError, User } from '../models/interfaces';
9
9
  import type { OxyServicesBase } from '../OxyServices.base';
10
+ import { loadNodeCrypto } from '../utils/platformCrypto';
11
+ import { logger } from '../utils/loggerUtils';
10
12
  import { CACHE_TIMES } from './mixinHelpers';
11
13
 
12
14
  interface JwtPayload {
@@ -17,7 +19,10 @@ interface JwtPayload {
17
19
  type?: string;
18
20
  appId?: string;
19
21
  appName?: string;
20
- [key: string]: any;
22
+ scopes?: string[];
23
+ aud?: string | string[];
24
+ iss?: string;
25
+ [key: string]: unknown;
21
26
  }
22
27
 
23
28
  /**
@@ -30,11 +35,67 @@ interface ActingAsVerification {
30
35
  }
31
36
 
32
37
  /**
33
- * Service app metadata attached to requests authenticated with service tokens
38
+ * Result from the service-acting-as verification endpoint.
39
+ * Confirms that a given service app holds an active delegation grant for
40
+ * the supplied user, along with the explicit scope list the grant covers.
41
+ *
42
+ * The api side persists these via the `ServiceActingAs` model:
43
+ * { serviceAppId, userId, scopes: string[], grantedAt, expiresAt }
44
+ *
45
+ * The SDK never inspects the grant directly — it round-trips through
46
+ * `GET /internal/service-acting-as/verify?appId=...&userId=...` so the
47
+ * authoritative store stays server-side.
48
+ */
49
+ export interface ServiceActingAsVerification {
50
+ authorized: boolean;
51
+ scopes: string[];
52
+ }
53
+
54
+ /**
55
+ * Service app metadata attached to requests authenticated with service tokens.
56
+ * `scopes` reflects the scopes granted to the app at signup time (from the
57
+ * `DeveloperApp.scopes` field); route-level checks can require additional
58
+ * scope-narrowing via `requireScope()`.
34
59
  */
35
60
  export interface ServiceApp {
36
61
  appId: string;
37
62
  appName: string;
63
+ scopes: string[];
64
+ }
65
+
66
+ /**
67
+ * Expected JWT audience for tokens issued by the Oxy auth service.
68
+ */
69
+ const OXY_JWT_AUDIENCE = 'oxy-api';
70
+ /**
71
+ * Expected JWT issuer for tokens issued by the Oxy auth service.
72
+ */
73
+ const OXY_JWT_ISSUER = 'oxy-auth';
74
+
75
+ /**
76
+ * Sentinel error classes for service-token verification. Using classes (not
77
+ * message strings) makes the catch site below safe to extend: a new failure
78
+ * mode added later cannot silently fall through to the generic 500 branch.
79
+ */
80
+ class ServiceTokenStructureError extends Error {
81
+ constructor(message = 'Service token has malformed structure') {
82
+ super(message);
83
+ this.name = 'ServiceTokenStructureError';
84
+ }
85
+ }
86
+
87
+ class ServiceTokenSignatureError extends Error {
88
+ constructor(message = 'Service token signature is invalid') {
89
+ super(message);
90
+ this.name = 'ServiceTokenSignatureError';
91
+ }
92
+ }
93
+
94
+ class ServiceTokenClaimError extends Error {
95
+ constructor(message: string) {
96
+ super(message);
97
+ this.name = 'ServiceTokenClaimError';
98
+ }
38
99
  }
39
100
 
40
101
  /**
@@ -44,7 +105,7 @@ interface AuthMiddlewareOptions {
44
105
  /** Enable debug logging (default: false) */
45
106
  debug?: boolean;
46
107
  /** Custom error handler - receives error object, can return response */
47
- onError?: (error: ApiError) => any;
108
+ onError?: (error: ApiError) => unknown;
48
109
  /** Load full user profile from API (default: false for performance) */
49
110
  loadUser?: boolean;
50
111
  /** Optional auth - attach user if token present but don't block (default: false) */
@@ -53,8 +114,24 @@ interface AuthMiddlewareOptions {
53
114
  * JWT secret for verifying service token signatures locally.
54
115
  * When provided, service tokens will be cryptographically verified.
55
116
  * When omitted, service tokens will be rejected (secure default).
117
+ *
118
+ * **Migration note (>=1.11.14):** the Oxy API now signs service tokens
119
+ * with a dedicated `SERVICE_TOKEN_SECRET` distinct from `ACCESS_TOKEN_SECRET`.
120
+ * Pass that value here. If you keep passing the access-token secret you will
121
+ * still verify ALL signed-by-Oxy tokens (which is the whole class of bug
122
+ * H4 was supposed to prevent — DO NOT do that in production).
56
123
  */
57
124
  jwtSecret?: string;
125
+ /**
126
+ * Expected JWT issuer. Defaults to `'oxy-auth'`. Override only if you run
127
+ * a private fork of the Oxy auth server under a different `iss` claim.
128
+ */
129
+ expectedIssuer?: string;
130
+ /**
131
+ * Expected JWT audience. Defaults to `'oxy-api'`. Override only if your
132
+ * private fork mints tokens for a different audience.
133
+ */
134
+ expectedAudience?: string;
58
135
  }
59
136
 
60
137
  export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base: T) {
@@ -62,6 +139,19 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
62
139
  /** @internal In-memory cache for acting-as verification results (TTL: 5 min) */
63
140
  _actingAsCache = new Map<string, { result: ActingAsVerification | null; expiresAt: number }>();
64
141
 
142
+ /**
143
+ * In-memory cache for service-acting-as verification.
144
+ * Negative results are cached for 1min to avoid hammering the verify
145
+ * endpoint when a service is misconfigured; positive grants are cached
146
+ * for 5min to amortize the round-trip without holding stale grants too long.
147
+ * @internal
148
+ */
149
+ _serviceActingAsCache = new Map<string, { result: ServiceActingAsVerification | null; expiresAt: number }>();
150
+
151
+ // TypeScript's mixin pattern requires `(...args: any[])` here — the
152
+ // constructor signature is a structural shape check the compiler enforces.
153
+ // Matches every other mixin in this package; do not change without a
154
+ // monorepo-wide refactor of the mixin pipeline.
65
155
  constructor(...args: any[]) {
66
156
  super(...(args as [any]));
67
157
  }
@@ -98,7 +188,13 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
98
188
  });
99
189
 
100
190
  return result && result.authorized ? result : null;
101
- } catch {
191
+ } catch (error) {
192
+ logger.warn('[oxy.auth] verifyActingAs lookup failed — caching negative result', {
193
+ component: 'auth',
194
+ method: 'verifyActingAs',
195
+ userId,
196
+ accountId,
197
+ }, error);
102
198
  // Cache negative result for 1 minute to avoid hammering on transient errors
103
199
  this._actingAsCache.set(cacheKey, {
104
200
  result: null,
@@ -108,6 +204,66 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
108
204
  }
109
205
  }
110
206
 
207
+ /**
208
+ * Verify that a service app holds an active delegation grant authorising
209
+ * it to act on behalf of `userId`. Returns the grant (with allowed scopes)
210
+ * on success or `null` if no valid grant exists. Negative answers are
211
+ * cached briefly to protect the verify endpoint from misconfigured callers.
212
+ *
213
+ * Implemented as a per-instance Map keyed by `appId:userId`. Cached
214
+ * positive grants live for 5 minutes (acceptable staleness window for an
215
+ * impersonation grant); revocations propagate within that window.
216
+ *
217
+ * @internal Used by the auth() middleware — not part of the public API
218
+ */
219
+ async verifyServiceActingAs(
220
+ appId: string,
221
+ userId: string,
222
+ ): Promise<ServiceActingAsVerification | null> {
223
+ const cacheKey = `${appId}:${userId}`;
224
+ const now = Date.now();
225
+
226
+ const cached = this._serviceActingAsCache.get(cacheKey);
227
+ if (cached && cached.expiresAt > now) {
228
+ return cached.result;
229
+ }
230
+
231
+ try {
232
+ const result = await this.makeRequest<ServiceActingAsVerification>(
233
+ 'GET',
234
+ '/internal/service-acting-as/verify',
235
+ { appId, userId },
236
+ { cache: false, retry: false, timeout: 5000 },
237
+ );
238
+
239
+ const authorized = Boolean(result && result.authorized);
240
+ const verified: ServiceActingAsVerification | null = authorized
241
+ ? { authorized: true, scopes: Array.isArray(result.scopes) ? result.scopes : [] }
242
+ : null;
243
+
244
+ this._serviceActingAsCache.set(cacheKey, {
245
+ result: verified,
246
+ expiresAt: now + 5 * 60 * 1000,
247
+ });
248
+
249
+ return verified;
250
+ } catch (error) {
251
+ logger.warn('[oxy.auth] verifyServiceActingAs lookup failed — caching negative result', {
252
+ component: 'auth',
253
+ method: 'verifyServiceActingAs',
254
+ appId,
255
+ userId,
256
+ }, error);
257
+ // Negative cache prevents a runaway loop if the verify endpoint is
258
+ // down, while still letting a real grant become visible within 60s.
259
+ this._serviceActingAsCache.set(cacheKey, {
260
+ result: null,
261
+ expiresAt: now + 1 * 60 * 1000,
262
+ });
263
+ return null;
264
+ }
265
+ }
266
+
111
267
  /**
112
268
  * Fetch link metadata
113
269
  */
@@ -145,9 +301,17 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
145
301
  * - Security comes from API-based session validation (`validateSession()`)
146
302
  * which checks the session server-side on every request
147
303
  * - Service tokens (type: 'service') DO use cryptographic HMAC verification
148
- * via the `jwtSecret` option, since they are stateless
304
+ * via the `jwtSecret` option, since they are stateless. Service tokens
305
+ * are additionally checked for `aud`, `iss`, and `type` claims to prevent
306
+ * cross-token-type confusion attacks.
149
307
  * - The backend's own `authMiddleware` uses `jwt.verify()` because it has
150
- * direct access to `ACCESS_TOKEN_SECRET`
308
+ * direct access to `SERVICE_TOKEN_SECRET` / `ACCESS_TOKEN_SECRET`.
309
+ *
310
+ * **Service-token delegation (X-Oxy-User-Id):**
311
+ * When a service token is accompanied by `X-Oxy-User-Id`, the SDK calls
312
+ * `verifyServiceActingAs(appId, userId)` to confirm an explicit delegation
313
+ * grant exists before attaching `req.userId`. A missing/expired grant
314
+ * results in a 403 — there is no fail-open path.
151
315
  *
152
316
  * @example
153
317
  * ```typescript
@@ -156,7 +320,7 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
156
320
  * const oxy = new OxyServices({ baseURL: 'https://api.oxy.so' });
157
321
  *
158
322
  * // Protect all routes under /protected
159
- * app.use('/protected', oxy.auth());
323
+ * app.use('/protected', oxy.auth({ jwtSecret: process.env.SERVICE_TOKEN_SECRET }));
160
324
  *
161
325
  * // Access user in route handler
162
326
  * app.get('/protected/me', (req, res) => {
@@ -168,27 +332,41 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
168
332
  *
169
333
  * // Optional auth - attach user if present, don't block if absent
170
334
  * app.use('/public', oxy.auth({ optional: true }));
335
+ *
336
+ * // Require a specific scope on a service-token-protected route
337
+ * app.use('/internal/files', oxy.serviceAuth({ jwtSecret: process.env.SERVICE_TOKEN_SECRET }), oxy.requireScope('files:write'));
171
338
  * ```
172
339
  *
173
340
  * @param options Optional configuration
174
341
  * @returns Express middleware function
175
342
  */
176
343
  auth(options: AuthMiddlewareOptions = {}) {
177
- const { debug = false, onError, loadUser = false, optional = false, jwtSecret } = options;
178
- // Cast to any for cross-mixin method access (Auth mixin methods available at runtime)
179
- const oxyInstance = this as any;
344
+ const {
345
+ debug = false,
346
+ onError,
347
+ loadUser = false,
348
+ optional = false,
349
+ jwtSecret,
350
+ expectedIssuer = OXY_JWT_ISSUER,
351
+ expectedAudience = OXY_JWT_AUDIENCE,
352
+ } = options;
353
+ // Cross-mixin method access: typed as a structural subset of the
354
+ // composed OxyServices we know we have at runtime.
355
+ const oxyInstance = this as unknown as OxyAuthInstance;
180
356
 
181
357
  // Return an async middleware function
182
- return async (req: any, res: any, next: any) => {
358
+ return async (req: AuthReq, res: AuthRes, next: AuthNext) => {
183
359
  // Process X-Acting-As header for managed account identity delegation.
184
360
  // Called after successful authentication, before next(). If the header
185
361
  // is present, verifies authorization and swaps the request identity to
186
362
  // the managed account, preserving the original user for audit trails.
187
363
  const processActingAs = async (): Promise<boolean> => {
188
- const actingAsUserId = req.headers['x-acting-as'] as string | undefined;
189
- if (!actingAsUserId) return true; // No header, proceed normally
364
+ const actingAsUserId = req.headers['x-acting-as'];
365
+ if (!actingAsUserId || typeof actingAsUserId !== 'string') return true; // No header, proceed normally
366
+ const currentUserId = req.userId;
367
+ if (!currentUserId) return true; // No authenticated user yet — nothing to swap
190
368
 
191
- const verification = await oxyInstance.verifyActingAs(req.userId, actingAsUserId);
369
+ const verification = await oxyInstance.verifyActingAs(currentUserId, actingAsUserId);
192
370
  if (!verification) {
193
371
  const error = {
194
372
  error: 'ACTING_AS_UNAUTHORIZED',
@@ -205,27 +383,29 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
205
383
  }
206
384
 
207
385
  // Preserve original user for audit trails
208
- req.originalUser = { id: req.userId, ...req.user };
386
+ req.originalUser = { id: currentUserId, ...(req.user ?? {}) };
209
387
  req.actingAs = { userId: actingAsUserId, role: verification.role };
210
388
 
211
389
  // Swap user identity to the managed account
212
390
  req.userId = actingAsUserId;
213
- req.user = { id: actingAsUserId } as any;
214
- // Also set _id for routes that use Pattern B (req.user._id)
215
- if (req.user) {
216
- (req.user as any)._id = actingAsUserId;
217
- }
391
+ req.user = { id: actingAsUserId, _id: actingAsUserId } as unknown as User;
218
392
 
219
393
  if (debug) {
220
- console.log(`[oxy.auth] Acting as ${actingAsUserId} (role=${verification.role}) original=${req.originalUser.id}`);
394
+ logger.debug(`[oxy.auth] Acting as ${actingAsUserId} (role=${verification.role}) original=${currentUserId}`, {
395
+ component: 'auth',
396
+ method: 'auth.processActingAs',
397
+ });
221
398
  }
222
399
 
223
400
  return true;
224
401
  };
225
402
 
226
403
  try {
227
- // Extract token from Authorization header or query params
228
- const authHeader = req.headers['authorization'];
404
+ // Extract token from Authorization header or query params.
405
+ // Node/Express normalizes `Authorization` to a string; we guard
406
+ // against the (legal but unusual) string[] case anyway.
407
+ const rawAuthHeader = req.headers.authorization;
408
+ const authHeader = Array.isArray(rawAuthHeader) ? rawAuthHeader[0] : rawAuthHeader;
229
409
  let token = authHeader?.startsWith('Bearer ') ? authHeader.substring(7) : null;
230
410
 
231
411
  // Fallback to query params (useful for WebSocket upgrades)
@@ -236,7 +416,10 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
236
416
  }
237
417
 
238
418
  if (debug) {
239
- console.log(`[oxy.auth] ${req.method} ${req.path} | token: ${!!token}`);
419
+ logger.debug(`[oxy.auth] ${req.method} ${req.path} | token: ${!!token}`, {
420
+ component: 'auth',
421
+ method: 'auth',
422
+ });
240
423
  }
241
424
 
242
425
  if (!token) {
@@ -261,6 +444,12 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
261
444
  try {
262
445
  decoded = jwtDecode<JwtPayload>(token);
263
446
  } catch (decodeError) {
447
+ if (debug) {
448
+ logger.debug('[oxy.auth] Token decode failed', {
449
+ component: 'auth',
450
+ method: 'auth',
451
+ }, decodeError);
452
+ }
264
453
  if (optional) {
265
454
  req.userId = null;
266
455
  req.user = null;
@@ -297,50 +486,73 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
297
486
  return res.status(403).json(error);
298
487
  }
299
488
 
300
- // Verify JWT signature (not just decode)
489
+ // Verify JWT signature, then audience / issuer / type / appId claims.
490
+ //
491
+ // Signature verification uses a manual HMAC-SHA256 compare because
492
+ // this file ships into RN/web bundles where `jsonwebtoken` is
493
+ // unavailable. The middleware only ever runs on Node hosts (see
494
+ // platformCrypto's doc-comment), and `loadNodeCrypto` is per-
495
+ // platform: the RN variant throws so Metro never bundles a Node
496
+ // built-in reference.
301
497
  try {
302
- const { createHmac } = await import('crypto');
303
- const [headerB64, payloadB64, signatureB64] = token.split('.');
304
- if (!headerB64 || !payloadB64 || !signatureB64) {
305
- throw new Error('Invalid token structure');
306
- }
307
- const expectedSig = createHmac('sha256', jwtSecret)
308
- .update(`${headerB64}.${payloadB64}`)
309
- .digest('base64')
310
- .replace(/\+/g, '-')
311
- .replace(/\//g, '_')
312
- .replace(/=/g, '');
313
-
314
- // Timing-safe comparison
315
- const sigBuf = Buffer.from(signatureB64);
316
- const expectedBuf = Buffer.from(expectedSig);
317
- const { timingSafeEqual } = await import('crypto');
318
- if (sigBuf.length !== expectedBuf.length || !timingSafeEqual(sigBuf, expectedBuf)) {
319
- throw new Error('Invalid signature');
320
- }
498
+ await verifyServiceTokenSignature(token, jwtSecret);
499
+ verifyServiceTokenClaims(decoded, {
500
+ audience: expectedAudience,
501
+ issuer: expectedIssuer,
502
+ });
321
503
  } catch (verifyError) {
322
- const isSignatureError = verifyError instanceof Error &&
323
- (verifyError.message === 'Invalid signature' || verifyError.message === 'Invalid token structure');
324
-
325
- if (!isSignatureError) {
326
- console.error('[oxy.auth] Unexpected error during service token verification:', verifyError);
327
- const error = { error: 'AUTH_INTERNAL_ERROR', message: 'Internal authentication error', code: 'AUTH_INTERNAL_ERROR', status: 500 };
504
+ // Structure + signature + claim errors all map to 401. Anything
505
+ // else (e.g. Node crypto failing to load on a misconfigured host)
506
+ // genuinely IS a 500.
507
+ if (
508
+ verifyError instanceof ServiceTokenStructureError ||
509
+ verifyError instanceof ServiceTokenSignatureError ||
510
+ verifyError instanceof ServiceTokenClaimError
511
+ ) {
512
+ if (debug) {
513
+ logger.debug('[oxy.auth] Service token rejected', {
514
+ component: 'auth',
515
+ method: 'auth.serviceToken',
516
+ reason: verifyError.name,
517
+ detail: verifyError.message,
518
+ });
519
+ }
520
+ if (optional) {
521
+ req.userId = null;
522
+ req.user = null;
523
+ return next();
524
+ }
525
+ const code = verifyError instanceof ServiceTokenSignatureError
526
+ ? 'INVALID_SERVICE_TOKEN'
527
+ : verifyError instanceof ServiceTokenStructureError
528
+ ? 'INVALID_SERVICE_TOKEN'
529
+ : 'INVALID_SERVICE_TOKEN_CLAIMS';
530
+ const error = {
531
+ error: code,
532
+ message: verifyError.message,
533
+ code,
534
+ status: 401,
535
+ };
328
536
  if (onError) return onError(error);
329
- return res.status(500).json(error);
537
+ return res.status(401).json(error);
330
538
  }
331
539
 
332
- if (optional) {
333
- req.userId = null;
334
- req.user = null;
335
- return next();
336
- }
337
- const error = { error: 'INVALID_SERVICE_TOKEN', message: 'Invalid service token signature', code: 'INVALID_SERVICE_TOKEN', status: 401 };
540
+ logger.error('[oxy.auth] Unexpected error during service token verification', verifyError, {
541
+ component: 'auth',
542
+ method: 'auth.serviceToken',
543
+ });
544
+ const error = {
545
+ error: 'AUTH_INTERNAL_ERROR',
546
+ message: 'Internal authentication error',
547
+ code: 'AUTH_INTERNAL_ERROR',
548
+ status: 500,
549
+ };
338
550
  if (onError) return onError(error);
339
- return res.status(401).json(error);
551
+ return res.status(500).json(error);
340
552
  }
341
553
 
342
- // Check expiration
343
- if (decoded.exp && decoded.exp < Math.floor(Date.now() / 1000)) {
554
+ // Check expiration — reject tokens at exact expiry second (use <=)
555
+ if (decoded.exp && decoded.exp <= Math.floor(Date.now() / 1000)) {
344
556
  if (optional) {
345
557
  req.userId = null;
346
558
  req.user = null;
@@ -352,7 +564,8 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
352
564
  }
353
565
 
354
566
  // Validate required service token fields
355
- if (!decoded.appId) {
567
+ const appId = decoded.appId;
568
+ if (!appId) {
356
569
  if (optional) {
357
570
  req.userId = null;
358
571
  req.user = null;
@@ -364,18 +577,54 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
364
577
  }
365
578
 
366
579
  // Read delegated user ID from header
367
- const oxyUserId = req.headers['x-oxy-user-id'] as string;
580
+ const oxyUserIdRaw = req.headers['x-oxy-user-id'];
581
+ const oxyUserId = typeof oxyUserIdRaw === 'string' && oxyUserIdRaw.length > 0 ? oxyUserIdRaw : null;
582
+
583
+ // C3: a service may only act as a user when an explicit
584
+ // ServiceActingAs grant exists for that (appId, userId) pair.
585
+ // Without the grant we MUST refuse — silently attaching
586
+ // `req.userId = oxyUserId` would let any service impersonate
587
+ // any user simply by setting the header.
588
+ if (oxyUserId) {
589
+ const grant = await oxyInstance.verifyServiceActingAs(appId, oxyUserId);
590
+ if (!grant || !grant.authorized) {
591
+ logger.warn('[oxy.auth] Service token rejected — no delegation grant', {
592
+ component: 'auth',
593
+ method: 'auth.serviceToken',
594
+ appId,
595
+ attemptedUserId: oxyUserId,
596
+ });
597
+ const error = {
598
+ error: 'SERVICE_ACTING_AS_UNAUTHORIZED',
599
+ message: 'Service not authorized to act as this user',
600
+ code: 'SERVICE_ACTING_AS_UNAUTHORIZED',
601
+ status: 403,
602
+ };
603
+ if (onError) return onError(error);
604
+ return res.status(403).json(error);
605
+ }
606
+
607
+ req.userId = oxyUserId;
608
+ req.user = { id: oxyUserId } as User;
609
+ req.serviceActingAs = { userId: oxyUserId, scopes: grant.scopes };
610
+ } else {
611
+ // No X-Oxy-User-Id means the service is acting as itself.
612
+ req.userId = null;
613
+ req.user = null;
614
+ }
368
615
 
369
- req.userId = oxyUserId || null;
370
- req.user = oxyUserId ? ({ id: oxyUserId } as User) : null;
371
616
  req.accessToken = token;
372
617
  req.serviceApp = {
373
- appId: decoded.appId || '',
618
+ appId,
374
619
  appName: decoded.appName || 'unknown',
620
+ scopes: Array.isArray(decoded.scopes) ? decoded.scopes : [],
375
621
  };
376
622
 
377
623
  if (debug) {
378
- console.log(`[oxy.auth] Service token OK app=${decoded.appName} delegateUser=${oxyUserId || '(none)'}`);
624
+ logger.debug(`[oxy.auth] Service token OK app=${decoded.appName} delegateUser=${oxyUserId || '(none)'}`, {
625
+ component: 'auth',
626
+ method: 'auth.serviceToken',
627
+ });
379
628
  }
380
629
 
381
630
  return next();
@@ -400,7 +649,8 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
400
649
  }
401
650
 
402
651
  // Check token expiration locally first (fast path)
403
- if (decoded.exp && decoded.exp < Math.floor(Date.now() / 1000)) {
652
+ // Reject tokens at exact expiry second (use <=)
653
+ if (decoded.exp && decoded.exp <= Math.floor(Date.now() / 1000)) {
404
654
  if (optional) {
405
655
  req.userId = null;
406
656
  req.user = null;
@@ -455,7 +705,10 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
455
705
  }
456
706
 
457
707
  if (debug) {
458
- console.log(`[oxy.auth] OK user=${userId} session=${decoded.sessionId}`);
708
+ logger.debug(`[oxy.auth] OK user=${userId} session=${decoded.sessionId}`, {
709
+ component: 'auth',
710
+ method: 'auth',
711
+ });
459
712
  }
460
713
 
461
714
  // Process X-Acting-As header before proceeding
@@ -463,7 +716,10 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
463
716
  return;
464
717
  } catch (validationError) {
465
718
  if (debug) {
466
- console.log(`[oxy.auth] Session validation failed:`, validationError);
719
+ logger.debug('[oxy.auth] Session validation failed', {
720
+ component: 'auth',
721
+ method: 'auth',
722
+ }, validationError);
467
723
  }
468
724
 
469
725
  if (optional) {
@@ -505,26 +761,49 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
505
761
  if (fullUser) {
506
762
  req.user = fullUser;
507
763
  }
508
- } catch {
509
- // Failed to load user, continue with basic user object
764
+ } catch (loadUserError) {
765
+ // Loading the full user is best-effort here; the basic { id }
766
+ // object is already attached. Log so misconfigured deployments
767
+ // can be diagnosed instead of silently failing.
768
+ logger.warn('[oxy.auth] loadUser fallback — could not fetch full profile', {
769
+ component: 'auth',
770
+ method: 'auth.loadUser',
771
+ userId,
772
+ }, loadUserError);
510
773
  }
511
774
  }
512
775
 
513
776
  if (debug) {
514
- console.log(`[oxy.auth] OK user=${userId} (no session)`);
777
+ logger.debug(`[oxy.auth] OK user=${userId} (no session)`, {
778
+ component: 'auth',
779
+ method: 'auth',
780
+ });
515
781
  }
516
782
 
517
783
  // Process X-Acting-As header before proceeding
518
784
  if (await processActingAs()) next();
519
785
  } catch (error) {
520
- const apiError = oxyInstance.handleError(error) as any;
786
+ const handled = oxyInstance.handleError(error) as Error & {
787
+ code?: string;
788
+ status?: number;
789
+ details?: Record<string, unknown>;
790
+ };
791
+ const apiError: ApiError = {
792
+ message: handled.message || 'Authentication error',
793
+ code: handled.code ?? 'AUTH_ERROR',
794
+ status: handled.status ?? 500,
795
+ details: handled.details,
796
+ };
521
797
 
522
798
  if (debug) {
523
- console.log(`[oxy.auth] Error:`, apiError);
799
+ logger.debug('[oxy.auth] Error', {
800
+ component: 'auth',
801
+ method: 'auth',
802
+ }, apiError);
524
803
  }
525
804
 
526
805
  if (onError) return onError(apiError);
527
- return res.status((apiError && apiError.status) || 500).json(apiError);
806
+ return res.status(apiError.status).json(apiError);
528
807
  }
529
808
  };
530
809
  }
@@ -555,10 +834,10 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
555
834
  */
556
835
  authSocket(options: { debug?: boolean } = {}) {
557
836
  const { debug = false } = options;
558
- // Cast to any for cross-mixin method access (Auth mixin methods available at runtime)
559
- const oxyInstance = this as any;
837
+ // Cross-mixin method access typed via the same structural subset.
838
+ const oxyInstance = this as unknown as OxyAuthInstance;
560
839
 
561
- return async (socket: any, next: (err?: Error) => void) => {
840
+ return async (socket: SocketLike, next: (err?: Error) => void) => {
562
841
  try {
563
842
  const token = socket.handshake?.auth?.token;
564
843
 
@@ -569,7 +848,13 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
569
848
  let decoded: JwtPayload;
570
849
  try {
571
850
  decoded = jwtDecode<JwtPayload>(token);
572
- } catch {
851
+ } catch (decodeError) {
852
+ if (debug) {
853
+ logger.debug('[oxy.authSocket] Token decode failed', {
854
+ component: 'auth',
855
+ method: 'authSocket',
856
+ }, decodeError);
857
+ }
573
858
  return next(new Error('Invalid token'));
574
859
  }
575
860
 
@@ -578,8 +863,8 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
578
863
  return next(new Error('Invalid token payload'));
579
864
  }
580
865
 
581
- // Check expiration
582
- if (decoded.exp && decoded.exp < Math.floor(Date.now() / 1000)) {
866
+ // Check expiration — reject tokens at exact expiry second (use <=)
867
+ if (decoded.exp && decoded.exp <= Math.floor(Date.now() / 1000)) {
583
868
  return next(new Error('Token expired'));
584
869
  }
585
870
 
@@ -592,28 +877,42 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
592
877
  if (!result || !result.valid) {
593
878
  return next(new Error('Session invalid'));
594
879
  }
595
- } catch {
880
+ } catch (validateErr) {
881
+ if (debug) {
882
+ logger.debug('[oxy.authSocket] Session validation failed', {
883
+ component: 'auth',
884
+ method: 'authSocket',
885
+ }, validateErr);
886
+ }
596
887
  return next(new Error('Session validation failed'));
597
888
  }
598
889
  }
599
890
 
600
- // Attach user data to socket
891
+ // Attach user data to socket. We expose BOTH `socket.data.userId`
892
+ // (the official Socket.IO data slot) and `socket.user` because
893
+ // every consumer in this ecosystem (Mention, Allo, api/server.ts)
894
+ // reads from `socket.user.id`.
601
895
  socket.data = socket.data || {};
602
896
  socket.data.userId = userId;
603
897
  socket.data.sessionId = decoded.sessionId || null;
604
898
  socket.data.token = token;
605
899
 
606
- // Also set on socket.user for backward compatibility
607
900
  socket.user = { id: userId, userId, sessionId: decoded.sessionId };
608
901
 
609
902
  if (debug) {
610
- console.log(`[oxy.authSocket] OK user=${userId}`);
903
+ logger.debug(`[oxy.authSocket] OK user=${userId}`, {
904
+ component: 'auth',
905
+ method: 'authSocket',
906
+ });
611
907
  }
612
908
 
613
909
  next();
614
910
  } catch (err) {
615
911
  if (debug) {
616
- console.log(`[oxy.authSocket] Error:`, err);
912
+ logger.debug('[oxy.authSocket] Error', {
913
+ component: 'auth',
914
+ method: 'authSocket',
915
+ }, err);
617
916
  }
618
917
  next(new Error('Authentication error'));
619
918
  }
@@ -627,7 +926,7 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
627
926
  * @example
628
927
  * ```typescript
629
928
  * // Protect internal endpoints
630
- * app.use('/internal', oxy.serviceAuth());
929
+ * app.use('/internal', oxy.serviceAuth({ jwtSecret: process.env.SERVICE_TOKEN_SECRET }));
631
930
  *
632
931
  * app.post('/internal/trigger', (req, res) => {
633
932
  * console.log('Service app:', req.serviceApp);
@@ -635,10 +934,10 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
635
934
  * });
636
935
  * ```
637
936
  */
638
- serviceAuth(options: { debug?: boolean; jwtSecret?: string } = {}) {
937
+ serviceAuth(options: { debug?: boolean; jwtSecret?: string; expectedIssuer?: string; expectedAudience?: string } = {}) {
639
938
  const innerAuth = this.auth({ ...options });
640
939
 
641
- return async (req: any, res: any, next: any) => {
940
+ return async (req: AuthReq, res: AuthRes, next: AuthNext) => {
642
941
  await innerAuth(req, res, () => {
643
942
  if (!req.serviceApp) {
644
943
  return res.status(403).json({
@@ -651,6 +950,180 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
651
950
  });
652
951
  };
653
952
  }
953
+
954
+ /**
955
+ * Express.js middleware that enforces a specific service-token scope.
956
+ *
957
+ * Mount AFTER `auth()` / `serviceAuth()` — relies on `req.serviceApp` and
958
+ * (when delegation is in effect) `req.serviceActingAs.scopes`. The scope
959
+ * is granted if EITHER list contains it, mirroring the OAuth2 model where
960
+ * the app's app-level scopes and the per-user delegated scopes both count.
961
+ *
962
+ * Requests authenticated as a regular user (no service token) are rejected
963
+ * with 403 — scope-protected endpoints are service-to-service by design.
964
+ *
965
+ * @example
966
+ * ```typescript
967
+ * app.use(
968
+ * '/internal/files',
969
+ * oxy.serviceAuth({ jwtSecret: process.env.SERVICE_TOKEN_SECRET }),
970
+ * oxy.requireScope('files:write'),
971
+ * );
972
+ * ```
973
+ */
974
+ requireScope(scope: string) {
975
+ if (typeof scope !== 'string' || scope.length === 0) {
976
+ throw new Error('requireScope: scope must be a non-empty string');
977
+ }
978
+
979
+ return (req: AuthReq, res: AuthRes, next: AuthNext): void => {
980
+ const appScopes = req.serviceApp?.scopes ?? [];
981
+ const delegatedScopes = req.serviceActingAs?.scopes ?? [];
982
+
983
+ if (!req.serviceApp) {
984
+ res.status(403).json({
985
+ error: 'SERVICE_TOKEN_REQUIRED',
986
+ message: 'Scope-protected endpoint requires a service token',
987
+ code: 'SERVICE_TOKEN_REQUIRED',
988
+ status: 403,
989
+ });
990
+ return;
991
+ }
992
+
993
+ if (appScopes.includes(scope) || delegatedScopes.includes(scope)) {
994
+ next();
995
+ return;
996
+ }
997
+
998
+ logger.warn('[oxy.auth] Service token missing required scope', {
999
+ component: 'auth',
1000
+ method: 'requireScope',
1001
+ appId: req.serviceApp.appId,
1002
+ required: scope,
1003
+ });
1004
+ res.status(403).json({
1005
+ error: 'INSUFFICIENT_SCOPE',
1006
+ message: `Required scope '${scope}' not granted`,
1007
+ code: 'INSUFFICIENT_SCOPE',
1008
+ status: 403,
1009
+ });
1010
+ };
1011
+ }
654
1012
  };
655
1013
  }
656
1014
 
1015
+ // ---------------------------------------------------------------------------
1016
+ // Service token verification helpers
1017
+ // ---------------------------------------------------------------------------
1018
+
1019
+ /**
1020
+ * Verify a service JWT's HMAC-SHA256 signature using a constant-time compare.
1021
+ * Throws `ServiceTokenStructureError` on malformed tokens and
1022
+ * `ServiceTokenSignatureError` on signature mismatch — both map to 401.
1023
+ */
1024
+ async function verifyServiceTokenSignature(token: string, secret: string): Promise<void> {
1025
+ const nodeCrypto = await loadNodeCrypto();
1026
+ const { createHmac, timingSafeEqual } = nodeCrypto;
1027
+ const parts = token.split('.');
1028
+ if (parts.length !== 3) {
1029
+ throw new ServiceTokenStructureError(`Service token must have 3 parts, got ${parts.length}`);
1030
+ }
1031
+ const [headerB64, payloadB64, signatureB64] = parts;
1032
+ if (!headerB64 || !payloadB64 || !signatureB64) {
1033
+ throw new ServiceTokenStructureError('Service token has empty segment');
1034
+ }
1035
+ const expectedSig = createHmac('sha256', secret)
1036
+ .update(`${headerB64}.${payloadB64}`)
1037
+ .digest('base64')
1038
+ .replace(/\+/g, '-')
1039
+ .replace(/\//g, '_')
1040
+ .replace(/=/g, '');
1041
+
1042
+ const sigBuf = Buffer.from(signatureB64);
1043
+ const expectedBuf = Buffer.from(expectedSig);
1044
+ if (sigBuf.length !== expectedBuf.length || !timingSafeEqual(sigBuf, expectedBuf)) {
1045
+ throw new ServiceTokenSignatureError();
1046
+ }
1047
+ }
1048
+
1049
+ /**
1050
+ * Verify that a decoded service-token payload carries the expected `aud`,
1051
+ * `iss`, and `type` claims. Throws `ServiceTokenClaimError` on mismatch.
1052
+ * This is the defence against the H4 vulnerability where a recovery / 2FA /
1053
+ * access token signed by the same shared secret could be replayed as a
1054
+ * service token because no claim binding existed.
1055
+ */
1056
+ function verifyServiceTokenClaims(
1057
+ decoded: JwtPayload,
1058
+ expected: { audience: string; issuer: string },
1059
+ ): void {
1060
+ if (decoded.type !== 'service') {
1061
+ throw new ServiceTokenClaimError(`Service token has unexpected type '${String(decoded.type)}'`);
1062
+ }
1063
+ if (decoded.iss !== expected.issuer) {
1064
+ throw new ServiceTokenClaimError(`Service token issuer mismatch: expected '${expected.issuer}', got '${String(decoded.iss)}'`);
1065
+ }
1066
+ const aud = decoded.aud;
1067
+ if (Array.isArray(aud)) {
1068
+ if (!aud.includes(expected.audience)) {
1069
+ throw new ServiceTokenClaimError(`Service token audience does not include '${expected.audience}'`);
1070
+ }
1071
+ } else if (aud !== expected.audience) {
1072
+ throw new ServiceTokenClaimError(`Service token audience mismatch: expected '${expected.audience}', got '${String(aud)}'`);
1073
+ }
1074
+ }
1075
+
1076
+ // ---------------------------------------------------------------------------
1077
+ // Local request/response/socket typing
1078
+ //
1079
+ // Express's types are an optional peer (we don't want to take a hard dep on
1080
+ // `@types/express` from a platform-agnostic SDK). The structural subset below
1081
+ // captures everything this middleware actually touches, so consumers get type
1082
+ // checking without us coupling to Express's full surface.
1083
+ // ---------------------------------------------------------------------------
1084
+
1085
+ interface AuthReq {
1086
+ method?: string;
1087
+ path?: string;
1088
+ headers: Record<string, string | string[] | undefined>;
1089
+ query?: Record<string, unknown>;
1090
+ userId?: string | null;
1091
+ user?: User | null;
1092
+ accessToken?: string;
1093
+ sessionId?: string | null;
1094
+ serviceApp?: ServiceApp;
1095
+ serviceActingAs?: { userId: string; scopes: string[] };
1096
+ actingAs?: { userId: string; role: string };
1097
+ originalUser?: { id: string } & Partial<User>;
1098
+ }
1099
+
1100
+ interface AuthRes {
1101
+ status(code: number): AuthRes;
1102
+ json(body: unknown): unknown;
1103
+ }
1104
+
1105
+ type AuthNext = (err?: unknown) => void;
1106
+
1107
+ interface SocketLike {
1108
+ handshake?: { auth?: { token?: string } };
1109
+ data?: Record<string, unknown>;
1110
+ user?: { id: string; userId: string; sessionId?: string | null };
1111
+ }
1112
+
1113
+ interface OxyAuthInstance {
1114
+ verifyActingAs(userId: string, accountId: string): Promise<ActingAsVerification | null>;
1115
+ verifyServiceActingAs(appId: string, userId: string): Promise<ServiceActingAsVerification | null>;
1116
+ validateSession(
1117
+ sessionId: string,
1118
+ options?: { deviceFingerprint?: string; useHeaderValidation?: boolean },
1119
+ ): Promise<{
1120
+ valid: boolean;
1121
+ user?: User;
1122
+ [key: string]: unknown;
1123
+ } | null>;
1124
+ getAccessToken(): string | null;
1125
+ setTokens(accessToken: string, refreshToken?: string): void;
1126
+ clearTokens(): void;
1127
+ getCurrentUser(): Promise<User | null>;
1128
+ handleError(error: unknown): Error;
1129
+ }