@oxyhq/core 1.6.0 → 1.6.2

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 (46) hide show
  1. package/dist/cjs/AuthManager.js +15 -0
  2. package/dist/cjs/index.js +9 -1
  3. package/dist/cjs/mixins/OxyServices.fedcm.js +39 -3
  4. package/dist/cjs/mixins/OxyServices.utility.js +24 -4
  5. package/dist/cjs/utils/authHelpers.js +114 -0
  6. package/dist/esm/AuthManager.js +16 -1
  7. package/dist/esm/HttpService.js +6 -6
  8. package/dist/esm/OxyServices.base.js +3 -3
  9. package/dist/esm/OxyServices.js +2 -2
  10. package/dist/esm/crypto/index.js +5 -5
  11. package/dist/esm/crypto/keyManager.js +3 -3
  12. package/dist/esm/crypto/recoveryPhrase.js +1 -1
  13. package/dist/esm/crypto/signatureService.js +2 -2
  14. package/dist/esm/i18n/index.js +11 -11
  15. package/dist/esm/index.js +27 -25
  16. package/dist/esm/mixins/OxyServices.analytics.js +1 -1
  17. package/dist/esm/mixins/OxyServices.auth.js +1 -1
  18. package/dist/esm/mixins/OxyServices.developer.js +1 -1
  19. package/dist/esm/mixins/OxyServices.features.js +1 -1
  20. package/dist/esm/mixins/OxyServices.fedcm.js +41 -5
  21. package/dist/esm/mixins/OxyServices.karma.js +1 -1
  22. package/dist/esm/mixins/OxyServices.language.js +2 -2
  23. package/dist/esm/mixins/OxyServices.payment.js +1 -1
  24. package/dist/esm/mixins/OxyServices.popup.js +2 -2
  25. package/dist/esm/mixins/OxyServices.privacy.js +1 -1
  26. package/dist/esm/mixins/OxyServices.redirect.js +1 -1
  27. package/dist/esm/mixins/OxyServices.security.js +1 -1
  28. package/dist/esm/mixins/OxyServices.user.js +1 -1
  29. package/dist/esm/mixins/OxyServices.utility.js +25 -5
  30. package/dist/esm/mixins/index.js +18 -18
  31. package/dist/esm/shared/index.js +5 -5
  32. package/dist/esm/shared/utils/index.js +4 -4
  33. package/dist/esm/utils/asyncUtils.js +1 -1
  34. package/dist/esm/utils/authHelpers.js +105 -0
  35. package/dist/esm/utils/errorUtils.js +1 -1
  36. package/dist/esm/utils/index.js +4 -4
  37. package/dist/types/index.d.ts +2 -0
  38. package/dist/types/mixins/OxyServices.fedcm.d.ts +6 -0
  39. package/dist/types/mixins/OxyServices.utility.d.ts +13 -0
  40. package/dist/types/utils/authHelpers.d.ts +57 -0
  41. package/package.json +2 -2
  42. package/src/AuthManager.ts +15 -0
  43. package/src/index.ts +11 -0
  44. package/src/mixins/OxyServices.fedcm.ts +44 -3
  45. package/src/mixins/OxyServices.utility.ts +24 -4
  46. package/src/utils/authHelpers.ts +140 -0
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Authentication helper utilities for common token validation
3
+ * and authentication error handling patterns.
4
+ */
5
+ import type { OxyServices } from '../OxyServices';
6
+ /**
7
+ * Error thrown when session sync is required
8
+ */
9
+ export declare class SessionSyncRequiredError extends Error {
10
+ constructor(message?: string);
11
+ }
12
+ /**
13
+ * Error thrown when authentication fails
14
+ */
15
+ export declare class AuthenticationFailedError extends Error {
16
+ constructor(message?: string);
17
+ }
18
+ /**
19
+ * Ensures a valid token exists before making authenticated API calls.
20
+ * If no valid token exists and an active session ID is available,
21
+ * attempts to refresh the token using the session.
22
+ *
23
+ * @throws {SessionSyncRequiredError} If the session needs to be synced (offline session)
24
+ */
25
+ export declare function ensureValidToken(oxyServices: OxyServices, activeSessionId: string | null | undefined): Promise<void>;
26
+ /**
27
+ * Options for handling API authentication errors
28
+ */
29
+ export interface HandleApiErrorOptions {
30
+ syncSession?: () => Promise<unknown>;
31
+ activeSessionId?: string | null;
32
+ oxyServices?: OxyServices;
33
+ }
34
+ /**
35
+ * Checks if an error is an authentication error (401 or auth-related message)
36
+ */
37
+ export declare function isAuthenticationError(error: unknown): boolean;
38
+ /**
39
+ * Wraps an API call with authentication error handling.
40
+ * On auth failure, optionally attempts to sync the session and retry.
41
+ *
42
+ * @throws {AuthenticationFailedError} If authentication fails and cannot be recovered
43
+ */
44
+ export declare function withAuthErrorHandling<T>(apiCall: () => Promise<T>, options?: HandleApiErrorOptions): Promise<T>;
45
+ /**
46
+ * Combines token validation and auth error handling for a complete authenticated API call.
47
+ *
48
+ * @example
49
+ * ```ts
50
+ * return await authenticatedApiCall(
51
+ * oxyServices,
52
+ * activeSessionId,
53
+ * () => oxyServices.updateProfile(updates)
54
+ * );
55
+ * ```
56
+ */
57
+ export declare function authenticatedApiCall<T>(oxyServices: OxyServices, activeSessionId: string | null | undefined, apiCall: () => Promise<T>, syncSession?: () => Promise<unknown>): Promise<T>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/core",
3
- "version": "1.6.0",
3
+ "version": "1.6.2",
4
4
  "description": "OxyHQ SDK Foundation — API client, authentication, cryptographic identity, and shared utilities",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -58,7 +58,7 @@
58
58
  "build": "npm run build:cjs && npm run build:esm && npm run build:types && npm run copy-assets",
59
59
  "copy-assets": "cp -r src/i18n/locales dist/cjs/i18n/locales && cp -r src/i18n/locales dist/esm/i18n/locales",
60
60
  "build:cjs": "tsc -p tsconfig.cjs.json",
61
- "build:esm": "tsc -p tsconfig.esm.json",
61
+ "build:esm": "tsc -p tsconfig.esm.json && node scripts/fix-esm-imports.mjs",
62
62
  "build:types": "tsc -p tsconfig.types.json",
63
63
  "clean": "rm -rf dist",
64
64
  "typescript": "tsc --noEmit",
@@ -58,6 +58,7 @@ const STORAGE_KEYS = {
58
58
  SESSION: 'oxy_session',
59
59
  USER: 'oxy_user',
60
60
  AUTH_METHOD: 'oxy_auth_method',
61
+ FEDCM_LOGIN_HINT: 'oxy_fedcm_login_hint',
61
62
  } as const;
62
63
 
63
64
  /**
@@ -366,6 +367,19 @@ export class AuthManager {
366
367
  this.refreshTimer = null;
367
368
  }
368
369
 
370
+ // Invalidate current session on the server (best-effort)
371
+ try {
372
+ const sessionJson = await this.storage.getItem(STORAGE_KEYS.SESSION);
373
+ if (sessionJson) {
374
+ const session = JSON.parse(sessionJson);
375
+ if (session.sessionId && typeof (this.oxyServices as any).logoutSession === 'function') {
376
+ await (this.oxyServices as any).logoutSession(session.sessionId);
377
+ }
378
+ }
379
+ } catch {
380
+ // Best-effort: don't block local signout on network failure
381
+ }
382
+
369
383
  // Revoke FedCM credential if supported
370
384
  try {
371
385
  const services = this.oxyServices as OxyServicesWithFedCM;
@@ -396,6 +410,7 @@ export class AuthManager {
396
410
  await this.storage.removeItem(STORAGE_KEYS.SESSION);
397
411
  await this.storage.removeItem(STORAGE_KEYS.USER);
398
412
  await this.storage.removeItem(STORAGE_KEYS.AUTH_METHOD);
413
+ await this.storage.removeItem(STORAGE_KEYS.FEDCM_LOGIN_HINT);
399
414
  }
400
415
 
401
416
  /**
package/src/index.ts CHANGED
@@ -123,6 +123,17 @@ export {
123
123
  // --- i18n ---
124
124
  export { translate } from './i18n';
125
125
 
126
+ // --- Auth Helpers ---
127
+ export {
128
+ SessionSyncRequiredError,
129
+ AuthenticationFailedError,
130
+ ensureValidToken,
131
+ isAuthenticationError,
132
+ withAuthErrorHandling,
133
+ authenticatedApiCall,
134
+ } from './utils/authHelpers';
135
+ export type { HandleApiErrorOptions } from './utils/authHelpers';
136
+
126
137
  // --- Session Utilities ---
127
138
  export { mergeSessions, normalizeAndSortSessions, sessionsArraysEqual } from './utils/sessionUtils';
128
139
 
@@ -8,6 +8,7 @@ const debug = createDebugLogger('FedCM');
8
8
  export interface FedCMAuthOptions {
9
9
  nonce?: string;
10
10
  context?: 'signin' | 'signup' | 'continue' | 'use';
11
+ loginHint?: string;
11
12
  }
12
13
 
13
14
  export interface FedCMConfig {
@@ -16,6 +17,8 @@ export interface FedCMConfig {
16
17
  clientId?: string;
17
18
  }
18
19
 
20
+ const FEDCM_LOGIN_HINT_KEY = 'oxy_fedcm_login_hint';
21
+
19
22
  // Global lock to prevent concurrent FedCM requests
20
23
  // FedCM only allows one navigator.credentials.get request at a time
21
24
  let fedCMRequestInProgress = false;
@@ -102,7 +105,10 @@ export function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(Base: T)
102
105
  const nonce = options.nonce || this.generateNonce();
103
106
  const clientId = this.getClientId();
104
107
 
105
- debug.log('Interactive sign-in: Requesting credential for', clientId);
108
+ // Use provided loginHint, or fall back to stored last-used account ID
109
+ const loginHint = options.loginHint || this.getStoredLoginHint();
110
+
111
+ debug.log('Interactive sign-in: Requesting credential for', clientId, loginHint ? `(hint: ${loginHint})` : '');
106
112
 
107
113
  // Request credential from browser's native identity flow
108
114
  const credential = await this.requestIdentityCredential({
@@ -110,6 +116,7 @@ export function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(Base: T)
110
116
  clientId,
111
117
  nonce,
112
118
  context: options.context,
119
+ loginHint,
113
120
  });
114
121
 
115
122
  if (!credential || !credential.token) {
@@ -126,6 +133,11 @@ export function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(Base: T)
126
133
  this.httpService.setTokens((session as any).accessToken);
127
134
  }
128
135
 
136
+ // Store the user ID as loginHint for future FedCM requests
137
+ if (session?.user?.id) {
138
+ this.storeLoginHint(session.user.id);
139
+ }
140
+
129
141
  debug.log('Interactive sign-in: Success!', { userId: (session as any)?.user?.id });
130
142
 
131
143
  return session;
@@ -195,14 +207,17 @@ export function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(Base: T)
195
207
  // Optional/interactive mediation should only happen when the user clicks "Sign In".
196
208
  let credential: { token: string } | null = null;
197
209
 
210
+ const loginHint = this.getStoredLoginHint();
211
+
198
212
  try {
199
213
  const nonce = this.generateNonce();
200
- debug.log('Silent SSO: Attempting silent mediation...');
214
+ debug.log('Silent SSO: Attempting silent mediation...', loginHint ? `(hint: ${loginHint})` : '');
201
215
 
202
216
  credential = await this.requestIdentityCredential({
203
217
  configURL: (this.constructor as any).DEFAULT_CONFIG_URL,
204
218
  clientId,
205
219
  nonce,
220
+ loginHint,
206
221
  mediation: 'silent',
207
222
  });
208
223
 
@@ -263,6 +278,11 @@ export function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(Base: T)
263
278
  debug.warn('Silent SSO: No accessToken in session response');
264
279
  }
265
280
 
281
+ // Store the user ID as loginHint for future FedCM requests
282
+ if (session.user?.id) {
283
+ this.storeLoginHint(session.user.id);
284
+ }
285
+
266
286
  debug.log('Silent SSO: Success!', {
267
287
  sessionId: session.sessionId?.substring(0, 8) + '...',
268
288
  userId: session.user?.id
@@ -286,6 +306,7 @@ export function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(Base: T)
286
306
  clientId: string;
287
307
  nonce: string;
288
308
  context?: string;
309
+ loginHint?: string;
289
310
  mediation?: 'silent' | 'optional' | 'required';
290
311
  }): Promise<{ token: string } | null> {
291
312
  const requestedMediation = options.mediation || 'optional';
@@ -346,7 +367,7 @@ export function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(Base: T)
346
367
  params: {
347
368
  nonce: options.nonce, // For Chrome 145+
348
369
  },
349
- ...(options.context && { loginHint: options.context }),
370
+ ...(options.loginHint && { loginHint: options.loginHint }),
350
371
  },
351
372
  ],
352
373
  },
@@ -480,6 +501,26 @@ export function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(Base: T)
480
501
  }
481
502
  return window.location.origin;
482
503
  }
504
+
505
+ /** @internal */
506
+ public getStoredLoginHint(): string | undefined {
507
+ if (typeof window === 'undefined') return undefined;
508
+ try {
509
+ return localStorage.getItem(FEDCM_LOGIN_HINT_KEY) || undefined;
510
+ } catch {
511
+ return undefined;
512
+ }
513
+ }
514
+
515
+ /** @internal */
516
+ public storeLoginHint(userId: string): void {
517
+ if (typeof window === 'undefined') return;
518
+ try {
519
+ localStorage.setItem(FEDCM_LOGIN_HINT_KEY, userId);
520
+ } catch {
521
+ // Storage full or blocked
522
+ }
523
+ }
483
524
  };
484
525
  }
485
526
 
@@ -83,6 +83,17 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
83
83
  * Validates JWT tokens against the Oxy API and attaches user data to requests.
84
84
  * Uses server-side session validation for security (not just JWT decode).
85
85
  *
86
+ * **Design note — jwtDecode vs jwt.verify:**
87
+ * This middleware intentionally uses `jwtDecode()` (decode-only, no signature
88
+ * verification) for user tokens. This is by design, NOT a security gap:
89
+ * - Third-party apps using `oxy.auth()` don't have the Oxy JWT secret
90
+ * - Security comes from API-based session validation (`validateSession()`)
91
+ * which checks the session server-side on every request
92
+ * - Service tokens (type: 'service') DO use cryptographic HMAC verification
93
+ * via the `jwtSecret` option, since they are stateless
94
+ * - The backend's own `authMiddleware` uses `jwt.verify()` because it has
95
+ * direct access to `ACCESS_TOKEN_SECRET`
96
+ *
86
97
  * @example
87
98
  * ```typescript
88
99
  * import { OxyServices } from '@oxyhq/core';
@@ -138,6 +149,7 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
138
149
  }
139
150
 
140
151
  const error = {
152
+ error: 'MISSING_TOKEN',
141
153
  message: 'Access token required',
142
154
  code: 'MISSING_TOKEN',
143
155
  status: 401
@@ -158,6 +170,7 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
158
170
  }
159
171
 
160
172
  const error = {
173
+ error: 'INVALID_TOKEN_FORMAT',
161
174
  message: 'Invalid token format',
162
175
  code: 'INVALID_TOKEN_FORMAT',
163
176
  status: 401
@@ -177,6 +190,7 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
177
190
  return next();
178
191
  }
179
192
  const error = {
193
+ error: 'SERVICE_TOKEN_NOT_CONFIGURED',
180
194
  message: 'Service token verification not configured',
181
195
  code: 'SERVICE_TOKEN_NOT_CONFIGURED',
182
196
  status: 403
@@ -212,7 +226,7 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
212
226
 
213
227
  if (!isSignatureError) {
214
228
  console.error('[oxy.auth] Unexpected error during service token verification:', verifyError);
215
- const error = { message: 'Internal authentication error', code: 'AUTH_INTERNAL_ERROR', status: 500 };
229
+ const error = { error: 'AUTH_INTERNAL_ERROR', message: 'Internal authentication error', code: 'AUTH_INTERNAL_ERROR', status: 500 };
216
230
  if (onError) return onError(error);
217
231
  return res.status(500).json(error);
218
232
  }
@@ -222,7 +236,7 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
222
236
  req.user = null;
223
237
  return next();
224
238
  }
225
- const error = { message: 'Invalid service token signature', code: 'INVALID_SERVICE_TOKEN', status: 401 };
239
+ const error = { error: 'INVALID_SERVICE_TOKEN', message: 'Invalid service token signature', code: 'INVALID_SERVICE_TOKEN', status: 401 };
226
240
  if (onError) return onError(error);
227
241
  return res.status(401).json(error);
228
242
  }
@@ -234,7 +248,7 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
234
248
  req.user = null;
235
249
  return next();
236
250
  }
237
- const error = { message: 'Service token expired', code: 'TOKEN_EXPIRED', status: 401 };
251
+ const error = { error: 'TOKEN_EXPIRED', message: 'Service token expired', code: 'TOKEN_EXPIRED', status: 401 };
238
252
  if (onError) return onError(error);
239
253
  return res.status(401).json(error);
240
254
  }
@@ -246,7 +260,7 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
246
260
  req.user = null;
247
261
  return next();
248
262
  }
249
- const error = { message: 'Invalid service token: missing appId', code: 'INVALID_SERVICE_TOKEN', status: 401 };
263
+ const error = { error: 'INVALID_SERVICE_TOKEN', message: 'Invalid service token: missing appId', code: 'INVALID_SERVICE_TOKEN', status: 401 };
250
264
  if (onError) return onError(error);
251
265
  return res.status(401).json(error);
252
266
  }
@@ -278,6 +292,7 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
278
292
  }
279
293
 
280
294
  const error = {
295
+ error: 'INVALID_TOKEN_PAYLOAD',
281
296
  message: 'Token missing user ID',
282
297
  code: 'INVALID_TOKEN_PAYLOAD',
283
298
  status: 401
@@ -295,6 +310,7 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
295
310
  }
296
311
 
297
312
  const error = {
313
+ error: 'TOKEN_EXPIRED',
298
314
  message: 'Token expired',
299
315
  code: 'TOKEN_EXPIRED',
300
316
  status: 401
@@ -319,6 +335,7 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
319
335
  }
320
336
 
321
337
  const error = {
338
+ error: 'INVALID_SESSION',
322
339
  message: 'Session invalid or expired',
323
340
  code: 'INVALID_SESSION',
324
341
  status: 401
@@ -356,6 +373,7 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
356
373
  }
357
374
 
358
375
  const error = {
376
+ error: 'SESSION_VALIDATION_ERROR',
359
377
  message: 'Session validation failed',
360
378
  code: 'SESSION_VALIDATION_ERROR',
361
379
  status: 401
@@ -416,6 +434,8 @@ export function OxyServicesUtilityMixin<T extends typeof OxyServicesBase>(Base:
416
434
  * Returns a middleware function for Socket.IO that validates JWT tokens
417
435
  * from the handshake auth object and attaches user data to the socket.
418
436
  *
437
+ * Uses `jwtDecode()` + API session validation (same rationale as `auth()`).
438
+ *
419
439
  * @example
420
440
  * ```typescript
421
441
  * import { OxyServices } from '@oxyhq/core';
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Authentication helper utilities for common token validation
3
+ * and authentication error handling patterns.
4
+ */
5
+
6
+ import type { OxyServices } from '../OxyServices';
7
+
8
+ /**
9
+ * Error thrown when session sync is required
10
+ */
11
+ export class SessionSyncRequiredError extends Error {
12
+ constructor(message = 'Session needs to be synced. Please try again.') {
13
+ super(message);
14
+ this.name = 'SessionSyncRequiredError';
15
+ }
16
+ }
17
+
18
+ /**
19
+ * Error thrown when authentication fails
20
+ */
21
+ export class AuthenticationFailedError extends Error {
22
+ constructor(message = 'Authentication failed. Please sign in again.') {
23
+ super(message);
24
+ this.name = 'AuthenticationFailedError';
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Ensures a valid token exists before making authenticated API calls.
30
+ * If no valid token exists and an active session ID is available,
31
+ * attempts to refresh the token using the session.
32
+ *
33
+ * @throws {SessionSyncRequiredError} If the session needs to be synced (offline session)
34
+ */
35
+ export async function ensureValidToken(
36
+ oxyServices: OxyServices,
37
+ activeSessionId: string | null | undefined
38
+ ): Promise<void> {
39
+ if (oxyServices.hasValidToken() || !activeSessionId) {
40
+ return;
41
+ }
42
+
43
+ try {
44
+ await oxyServices.getTokenBySession(activeSessionId);
45
+ } catch (tokenError) {
46
+ const errorMessage = tokenError instanceof Error ? tokenError.message : String(tokenError);
47
+
48
+ if (errorMessage.includes('AUTH_REQUIRED_OFFLINE_SESSION') || errorMessage.includes('offline')) {
49
+ throw new SessionSyncRequiredError();
50
+ }
51
+
52
+ throw tokenError;
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Options for handling API authentication errors
58
+ */
59
+ export interface HandleApiErrorOptions {
60
+ syncSession?: () => Promise<unknown>;
61
+ activeSessionId?: string | null;
62
+ oxyServices?: OxyServices;
63
+ }
64
+
65
+ /**
66
+ * Checks if an error is an authentication error (401 or auth-related message)
67
+ */
68
+ export function isAuthenticationError(error: unknown): boolean {
69
+ if (!error || typeof error !== 'object') {
70
+ return false;
71
+ }
72
+
73
+ const errorObj = error as { message?: string; status?: number; response?: { status?: number } };
74
+ const errorMessage = errorObj.message || '';
75
+ const status = errorObj.status || errorObj.response?.status;
76
+
77
+ return (
78
+ status === 401 ||
79
+ errorMessage.includes('Authentication required') ||
80
+ errorMessage.includes('Invalid or missing authorization header')
81
+ );
82
+ }
83
+
84
+ /**
85
+ * Wraps an API call with authentication error handling.
86
+ * On auth failure, optionally attempts to sync the session and retry.
87
+ *
88
+ * @throws {AuthenticationFailedError} If authentication fails and cannot be recovered
89
+ */
90
+ export async function withAuthErrorHandling<T>(
91
+ apiCall: () => Promise<T>,
92
+ options?: HandleApiErrorOptions
93
+ ): Promise<T> {
94
+ try {
95
+ return await apiCall();
96
+ } catch (error) {
97
+ if (!isAuthenticationError(error)) {
98
+ throw error;
99
+ }
100
+
101
+ if (options?.syncSession && options?.activeSessionId && options?.oxyServices) {
102
+ try {
103
+ await options.syncSession();
104
+ await options.oxyServices.getTokenBySession(options.activeSessionId);
105
+ return await apiCall();
106
+ } catch {
107
+ throw new AuthenticationFailedError();
108
+ }
109
+ }
110
+
111
+ throw new AuthenticationFailedError();
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Combines token validation and auth error handling for a complete authenticated API call.
117
+ *
118
+ * @example
119
+ * ```ts
120
+ * return await authenticatedApiCall(
121
+ * oxyServices,
122
+ * activeSessionId,
123
+ * () => oxyServices.updateProfile(updates)
124
+ * );
125
+ * ```
126
+ */
127
+ export async function authenticatedApiCall<T>(
128
+ oxyServices: OxyServices,
129
+ activeSessionId: string | null | undefined,
130
+ apiCall: () => Promise<T>,
131
+ syncSession?: () => Promise<unknown>
132
+ ): Promise<T> {
133
+ await ensureValidToken(oxyServices, activeSessionId);
134
+
135
+ return withAuthErrorHandling(apiCall, {
136
+ syncSession,
137
+ activeSessionId,
138
+ oxyServices,
139
+ });
140
+ }