@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
@@ -1,4 +1,4 @@
1
- import { OxyAuthenticationError } from '../OxyServices.errors';
1
+ import { OxyAuthenticationError } from '../OxyServices.errors.js';
2
2
  export function OxyServicesAuthMixin(Base) {
3
3
  return class extends Base {
4
4
  constructor(...args) {
@@ -1,4 +1,4 @@
1
- import { CACHE_TIMES } from './mixinHelpers';
1
+ import { CACHE_TIMES } from './mixinHelpers.js';
2
2
  export function OxyServicesDeveloperMixin(Base) {
3
3
  return class extends Base {
4
4
  constructor(...args) {
@@ -1,4 +1,4 @@
1
- import { CACHE_TIMES } from './mixinHelpers';
1
+ import { CACHE_TIMES } from './mixinHelpers.js';
2
2
  export function OxyServicesFeaturesMixin(Base) {
3
3
  return class extends Base {
4
4
  constructor(...args) {
@@ -1,6 +1,7 @@
1
- import { OxyAuthenticationError } from '../OxyServices.errors';
2
- import { createDebugLogger } from '../shared/utils/debugUtils';
1
+ import { OxyAuthenticationError } from '../OxyServices.errors.js';
2
+ import { createDebugLogger } from '../shared/utils/debugUtils.js';
3
3
  const debug = createDebugLogger('FedCM');
4
+ const FEDCM_LOGIN_HINT_KEY = 'oxy_fedcm_login_hint';
4
5
  // Global lock to prevent concurrent FedCM requests
5
6
  // FedCM only allows one navigator.credentials.get request at a time
6
7
  let fedCMRequestInProgress = false;
@@ -78,13 +79,16 @@ export function OxyServicesFedCMMixin(Base) {
78
79
  try {
79
80
  const nonce = options.nonce || this.generateNonce();
80
81
  const clientId = this.getClientId();
81
- debug.log('Interactive sign-in: Requesting credential for', clientId);
82
+ // Use provided loginHint, or fall back to stored last-used account ID
83
+ const loginHint = options.loginHint || this.getStoredLoginHint();
84
+ debug.log('Interactive sign-in: Requesting credential for', clientId, loginHint ? `(hint: ${loginHint})` : '');
82
85
  // Request credential from browser's native identity flow
83
86
  const credential = await this.requestIdentityCredential({
84
87
  configURL: this.constructor.DEFAULT_CONFIG_URL,
85
88
  clientId,
86
89
  nonce,
87
90
  context: options.context,
91
+ loginHint,
88
92
  });
89
93
  if (!credential || !credential.token) {
90
94
  throw new OxyAuthenticationError('No credential received from browser');
@@ -96,6 +100,10 @@ export function OxyServicesFedCMMixin(Base) {
96
100
  if (session && session.accessToken) {
97
101
  this.httpService.setTokens(session.accessToken);
98
102
  }
103
+ // Store the user ID as loginHint for future FedCM requests
104
+ if (session?.user?.id) {
105
+ this.storeLoginHint(session.user.id);
106
+ }
99
107
  debug.log('Interactive sign-in: Success!', { userId: session?.user?.id });
100
108
  return session;
101
109
  }
@@ -160,13 +168,15 @@ export function OxyServicesFedCMMixin(Base) {
160
168
  // this runs on app startup — showing browser UI without user action is bad UX.
161
169
  // Optional/interactive mediation should only happen when the user clicks "Sign In".
162
170
  let credential = null;
171
+ const loginHint = this.getStoredLoginHint();
163
172
  try {
164
173
  const nonce = this.generateNonce();
165
- debug.log('Silent SSO: Attempting silent mediation...');
174
+ debug.log('Silent SSO: Attempting silent mediation...', loginHint ? `(hint: ${loginHint})` : '');
166
175
  credential = await this.requestIdentityCredential({
167
176
  configURL: this.constructor.DEFAULT_CONFIG_URL,
168
177
  clientId,
169
178
  nonce,
179
+ loginHint,
170
180
  mediation: 'silent',
171
181
  });
172
182
  debug.log('Silent SSO: Silent mediation result:', { hasCredential: !!credential, hasToken: !!credential?.token });
@@ -221,6 +231,10 @@ export function OxyServicesFedCMMixin(Base) {
221
231
  else {
222
232
  debug.warn('Silent SSO: No accessToken in session response');
223
233
  }
234
+ // Store the user ID as loginHint for future FedCM requests
235
+ if (session.user?.id) {
236
+ this.storeLoginHint(session.user.id);
237
+ }
224
238
  debug.log('Silent SSO: Success!', {
225
239
  sessionId: session.sessionId?.substring(0, 8) + '...',
226
240
  userId: session.user?.id
@@ -295,7 +309,7 @@ export function OxyServicesFedCMMixin(Base) {
295
309
  params: {
296
310
  nonce: options.nonce, // For Chrome 145+
297
311
  },
298
- ...(options.context && { loginHint: options.context }),
312
+ ...(options.loginHint && { loginHint: options.loginHint }),
299
313
  },
300
314
  ],
301
315
  },
@@ -415,6 +429,28 @@ export function OxyServicesFedCMMixin(Base) {
415
429
  }
416
430
  return window.location.origin;
417
431
  }
432
+ /** @internal */
433
+ getStoredLoginHint() {
434
+ if (typeof window === 'undefined')
435
+ return undefined;
436
+ try {
437
+ return localStorage.getItem(FEDCM_LOGIN_HINT_KEY) || undefined;
438
+ }
439
+ catch {
440
+ return undefined;
441
+ }
442
+ }
443
+ /** @internal */
444
+ storeLoginHint(userId) {
445
+ if (typeof window === 'undefined')
446
+ return;
447
+ try {
448
+ localStorage.setItem(FEDCM_LOGIN_HINT_KEY, userId);
449
+ }
450
+ catch {
451
+ // Storage full or blocked
452
+ }
453
+ }
418
454
  },
419
455
  _a.DEFAULT_CONFIG_URL = 'https://auth.oxy.so/fedcm.json',
420
456
  _a.FEDCM_TIMEOUT = 15000 // 15 seconds for interactive
@@ -1,4 +1,4 @@
1
- import { CACHE_TIMES } from './mixinHelpers';
1
+ import { CACHE_TIMES } from './mixinHelpers.js';
2
2
  export function OxyServicesKarmaMixin(Base) {
3
3
  return class extends Base {
4
4
  constructor(...args) {
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Language Methods Mixin
3
3
  */
4
- import { normalizeLanguageCode, getLanguageMetadata, getLanguageName, getNativeLanguageName } from '../utils/languageUtils';
5
- import { isDev } from '../shared/utils/debugUtils';
4
+ import { normalizeLanguageCode, getLanguageMetadata, getLanguageName, getNativeLanguageName } from '../utils/languageUtils.js';
5
+ import { isDev } from '../shared/utils/debugUtils.js';
6
6
  export function OxyServicesLanguageMixin(Base) {
7
7
  return class extends Base {
8
8
  constructor(...args) {
@@ -1,4 +1,4 @@
1
- import { CACHE_TIMES } from './mixinHelpers';
1
+ import { CACHE_TIMES } from './mixinHelpers.js';
2
2
  export function OxyServicesPaymentMixin(Base) {
3
3
  return class extends Base {
4
4
  constructor(...args) {
@@ -1,5 +1,5 @@
1
- import { OxyAuthenticationError } from '../OxyServices.errors';
2
- import { createDebugLogger } from '../shared/utils/debugUtils';
1
+ import { OxyAuthenticationError } from '../OxyServices.errors.js';
2
+ import { createDebugLogger } from '../shared/utils/debugUtils.js';
3
3
  const debug = createDebugLogger('PopupAuth');
4
4
  /**
5
5
  * Popup-based Cross-Domain Authentication Mixin
@@ -1,4 +1,4 @@
1
- import { isDev } from '../shared/utils/debugUtils';
1
+ import { isDev } from '../shared/utils/debugUtils.js';
2
2
  export function OxyServicesPrivacyMixin(Base) {
3
3
  return class extends Base {
4
4
  constructor(...args) {
@@ -1,4 +1,4 @@
1
- import { OxyAuthenticationError } from '../OxyServices.errors';
1
+ import { OxyAuthenticationError } from '../OxyServices.errors.js';
2
2
  /**
3
3
  * Redirect-based Cross-Domain Authentication Mixin
4
4
  *
@@ -1,4 +1,4 @@
1
- import { isDev } from '../shared/utils/debugUtils';
1
+ import { isDev } from '../shared/utils/debugUtils.js';
2
2
  export function OxyServicesSecurityMixin(Base) {
3
3
  return class extends Base {
4
4
  constructor(...args) {
@@ -1,4 +1,4 @@
1
- import { buildSearchParams, buildPaginationParams } from '../utils/apiUtils';
1
+ import { buildSearchParams, buildPaginationParams } from '../utils/apiUtils.js';
2
2
  export function OxyServicesUserMixin(Base) {
3
3
  return class extends Base {
4
4
  constructor(...args) {
@@ -5,7 +5,7 @@
5
5
  * and Express.js authentication middleware
6
6
  */
7
7
  import { jwtDecode } from 'jwt-decode';
8
- import { CACHE_TIMES } from './mixinHelpers';
8
+ import { CACHE_TIMES } from './mixinHelpers.js';
9
9
  export function OxyServicesUtilityMixin(Base) {
10
10
  return class extends Base {
11
11
  constructor(...args) {
@@ -31,6 +31,17 @@ export function OxyServicesUtilityMixin(Base) {
31
31
  * Validates JWT tokens against the Oxy API and attaches user data to requests.
32
32
  * Uses server-side session validation for security (not just JWT decode).
33
33
  *
34
+ * **Design note — jwtDecode vs jwt.verify:**
35
+ * This middleware intentionally uses `jwtDecode()` (decode-only, no signature
36
+ * verification) for user tokens. This is by design, NOT a security gap:
37
+ * - Third-party apps using `oxy.auth()` don't have the Oxy JWT secret
38
+ * - Security comes from API-based session validation (`validateSession()`)
39
+ * which checks the session server-side on every request
40
+ * - Service tokens (type: 'service') DO use cryptographic HMAC verification
41
+ * via the `jwtSecret` option, since they are stateless
42
+ * - The backend's own `authMiddleware` uses `jwt.verify()` because it has
43
+ * direct access to `ACCESS_TOKEN_SECRET`
44
+ *
34
45
  * @example
35
46
  * ```typescript
36
47
  * import { OxyServices } from '@oxyhq/core';
@@ -83,6 +94,7 @@ export function OxyServicesUtilityMixin(Base) {
83
94
  return next();
84
95
  }
85
96
  const error = {
97
+ error: 'MISSING_TOKEN',
86
98
  message: 'Access token required',
87
99
  code: 'MISSING_TOKEN',
88
100
  status: 401
@@ -103,6 +115,7 @@ export function OxyServicesUtilityMixin(Base) {
103
115
  return next();
104
116
  }
105
117
  const error = {
118
+ error: 'INVALID_TOKEN_FORMAT',
106
119
  message: 'Invalid token format',
107
120
  code: 'INVALID_TOKEN_FORMAT',
108
121
  status: 401
@@ -122,6 +135,7 @@ export function OxyServicesUtilityMixin(Base) {
122
135
  return next();
123
136
  }
124
137
  const error = {
138
+ error: 'SERVICE_TOKEN_NOT_CONFIGURED',
125
139
  message: 'Service token verification not configured',
126
140
  code: 'SERVICE_TOKEN_NOT_CONFIGURED',
127
141
  status: 403
@@ -156,7 +170,7 @@ export function OxyServicesUtilityMixin(Base) {
156
170
  (verifyError.message === 'Invalid signature' || verifyError.message === 'Invalid token structure');
157
171
  if (!isSignatureError) {
158
172
  console.error('[oxy.auth] Unexpected error during service token verification:', verifyError);
159
- const error = { message: 'Internal authentication error', code: 'AUTH_INTERNAL_ERROR', status: 500 };
173
+ const error = { error: 'AUTH_INTERNAL_ERROR', message: 'Internal authentication error', code: 'AUTH_INTERNAL_ERROR', status: 500 };
160
174
  if (onError)
161
175
  return onError(error);
162
176
  return res.status(500).json(error);
@@ -166,7 +180,7 @@ export function OxyServicesUtilityMixin(Base) {
166
180
  req.user = null;
167
181
  return next();
168
182
  }
169
- const error = { message: 'Invalid service token signature', code: 'INVALID_SERVICE_TOKEN', status: 401 };
183
+ const error = { error: 'INVALID_SERVICE_TOKEN', message: 'Invalid service token signature', code: 'INVALID_SERVICE_TOKEN', status: 401 };
170
184
  if (onError)
171
185
  return onError(error);
172
186
  return res.status(401).json(error);
@@ -178,7 +192,7 @@ export function OxyServicesUtilityMixin(Base) {
178
192
  req.user = null;
179
193
  return next();
180
194
  }
181
- const error = { message: 'Service token expired', code: 'TOKEN_EXPIRED', status: 401 };
195
+ const error = { error: 'TOKEN_EXPIRED', message: 'Service token expired', code: 'TOKEN_EXPIRED', status: 401 };
182
196
  if (onError)
183
197
  return onError(error);
184
198
  return res.status(401).json(error);
@@ -190,7 +204,7 @@ export function OxyServicesUtilityMixin(Base) {
190
204
  req.user = null;
191
205
  return next();
192
206
  }
193
- const error = { message: 'Invalid service token: missing appId', code: 'INVALID_SERVICE_TOKEN', status: 401 };
207
+ const error = { error: 'INVALID_SERVICE_TOKEN', message: 'Invalid service token: missing appId', code: 'INVALID_SERVICE_TOKEN', status: 401 };
194
208
  if (onError)
195
209
  return onError(error);
196
210
  return res.status(401).json(error);
@@ -217,6 +231,7 @@ export function OxyServicesUtilityMixin(Base) {
217
231
  return next();
218
232
  }
219
233
  const error = {
234
+ error: 'INVALID_TOKEN_PAYLOAD',
220
235
  message: 'Token missing user ID',
221
236
  code: 'INVALID_TOKEN_PAYLOAD',
222
237
  status: 401
@@ -233,6 +248,7 @@ export function OxyServicesUtilityMixin(Base) {
233
248
  return next();
234
249
  }
235
250
  const error = {
251
+ error: 'TOKEN_EXPIRED',
236
252
  message: 'Token expired',
237
253
  code: 'TOKEN_EXPIRED',
238
254
  status: 401
@@ -255,6 +271,7 @@ export function OxyServicesUtilityMixin(Base) {
255
271
  return next();
256
272
  }
257
273
  const error = {
274
+ error: 'INVALID_SESSION',
258
275
  message: 'Session invalid or expired',
259
276
  code: 'INVALID_SESSION',
260
277
  status: 401
@@ -289,6 +306,7 @@ export function OxyServicesUtilityMixin(Base) {
289
306
  return next();
290
307
  }
291
308
  const error = {
309
+ error: 'SESSION_VALIDATION_ERROR',
292
310
  message: 'Session validation failed',
293
311
  code: 'SESSION_VALIDATION_ERROR',
294
312
  status: 401
@@ -346,6 +364,8 @@ export function OxyServicesUtilityMixin(Base) {
346
364
  * Returns a middleware function for Socket.IO that validates JWT tokens
347
365
  * from the handshake auth object and attaches user data to the socket.
348
366
  *
367
+ * Uses `jwtDecode()` + API session validation (same rationale as `auth()`).
368
+ *
349
369
  * @example
350
370
  * ```typescript
351
371
  * import { OxyServices } from '@oxyhq/core';
@@ -4,24 +4,24 @@
4
4
  * This module provides a clean way to compose all mixins
5
5
  * and ensures consistent ordering for better maintainability
6
6
  */
7
- import { OxyServicesBase } from '../OxyServices.base';
8
- import { OxyServicesAuthMixin } from './OxyServices.auth';
9
- import { OxyServicesFedCMMixin } from './OxyServices.fedcm';
10
- import { OxyServicesPopupAuthMixin } from './OxyServices.popup';
11
- import { OxyServicesRedirectAuthMixin } from './OxyServices.redirect';
12
- import { OxyServicesUserMixin } from './OxyServices.user';
13
- import { OxyServicesPrivacyMixin } from './OxyServices.privacy';
14
- import { OxyServicesLanguageMixin } from './OxyServices.language';
15
- import { OxyServicesPaymentMixin } from './OxyServices.payment';
16
- import { OxyServicesKarmaMixin } from './OxyServices.karma';
17
- import { OxyServicesAssetsMixin } from './OxyServices.assets';
18
- import { OxyServicesDeveloperMixin } from './OxyServices.developer';
19
- import { OxyServicesLocationMixin } from './OxyServices.location';
20
- import { OxyServicesAnalyticsMixin } from './OxyServices.analytics';
21
- import { OxyServicesDevicesMixin } from './OxyServices.devices';
22
- import { OxyServicesSecurityMixin } from './OxyServices.security';
23
- import { OxyServicesUtilityMixin } from './OxyServices.utility';
24
- import { OxyServicesFeaturesMixin } from './OxyServices.features';
7
+ import { OxyServicesBase } from '../OxyServices.base.js';
8
+ import { OxyServicesAuthMixin } from './OxyServices.auth.js';
9
+ import { OxyServicesFedCMMixin } from './OxyServices.fedcm.js';
10
+ import { OxyServicesPopupAuthMixin } from './OxyServices.popup.js';
11
+ import { OxyServicesRedirectAuthMixin } from './OxyServices.redirect.js';
12
+ import { OxyServicesUserMixin } from './OxyServices.user.js';
13
+ import { OxyServicesPrivacyMixin } from './OxyServices.privacy.js';
14
+ import { OxyServicesLanguageMixin } from './OxyServices.language.js';
15
+ import { OxyServicesPaymentMixin } from './OxyServices.payment.js';
16
+ import { OxyServicesKarmaMixin } from './OxyServices.karma.js';
17
+ import { OxyServicesAssetsMixin } from './OxyServices.assets.js';
18
+ import { OxyServicesDeveloperMixin } from './OxyServices.developer.js';
19
+ import { OxyServicesLocationMixin } from './OxyServices.location.js';
20
+ import { OxyServicesAnalyticsMixin } from './OxyServices.analytics.js';
21
+ import { OxyServicesDevicesMixin } from './OxyServices.devices.js';
22
+ import { OxyServicesSecurityMixin } from './OxyServices.security.js';
23
+ import { OxyServicesUtilityMixin } from './OxyServices.utility.js';
24
+ import { OxyServicesFeaturesMixin } from './OxyServices.features.js';
25
25
  /**
26
26
  * Mixin pipeline - applied in order from first to last.
27
27
  *
@@ -20,12 +20,12 @@
20
20
  * ```
21
21
  */
22
22
  // Color utilities
23
- export { darkenColor, lightenColor, hexToRgb, rgbToHex, withOpacity, isLightColor, getContrastTextColor, } from './utils/colorUtils';
23
+ export { darkenColor, lightenColor, hexToRgb, rgbToHex, withOpacity, isLightColor, getContrastTextColor, } from './utils/colorUtils.js';
24
24
  // Theme utilities
25
- export { normalizeTheme, normalizeColorScheme, getOppositeTheme, systemPrefersDarkMode, getSystemColorScheme, } from './utils/themeUtils';
25
+ export { normalizeTheme, normalizeColorScheme, getOppositeTheme, systemPrefersDarkMode, getSystemColorScheme, } from './utils/themeUtils.js';
26
26
  // Error utilities
27
- export { HttpStatus, getErrorStatus, getErrorMessage, isAlreadyRegisteredError, isUnauthorizedError, isForbiddenError, isNotFoundError, isRateLimitError, isServerError, isNetworkError, isRetryableError, } from './utils/errorUtils';
27
+ export { HttpStatus, getErrorStatus, getErrorMessage, isAlreadyRegisteredError, isUnauthorizedError, isForbiddenError, isNotFoundError, isRateLimitError, isServerError, isNetworkError, isRetryableError, } from './utils/errorUtils.js';
28
28
  // Network utilities
29
- export { DEFAULT_CIRCUIT_BREAKER_CONFIG, createCircuitBreakerState, calculateBackoffInterval, recordFailure, recordSuccess, shouldAllowRequest, delay, withRetry, } from './utils/networkUtils';
29
+ export { DEFAULT_CIRCUIT_BREAKER_CONFIG, createCircuitBreakerState, calculateBackoffInterval, recordFailure, recordSuccess, shouldAllowRequest, delay, withRetry, } from './utils/networkUtils.js';
30
30
  // Debug utilities
31
- export { isDev, debugLog, debugWarn, debugError, createDebugLogger, } from './utils/debugUtils';
31
+ export { isDev, debugLog, debugWarn, debugError, createDebugLogger, } from './utils/debugUtils.js';
@@ -6,10 +6,10 @@
6
6
  * @module shared/utils
7
7
  */
8
8
  // Color utilities
9
- export { darkenColor, lightenColor, hexToRgb, rgbToHex, withOpacity, isLightColor, getContrastTextColor, } from './colorUtils';
9
+ export { darkenColor, lightenColor, hexToRgb, rgbToHex, withOpacity, isLightColor, getContrastTextColor, } from './colorUtils.js';
10
10
  // Theme utilities
11
- export { normalizeTheme, normalizeColorScheme, getOppositeTheme, systemPrefersDarkMode, getSystemColorScheme, } from './themeUtils';
11
+ export { normalizeTheme, normalizeColorScheme, getOppositeTheme, systemPrefersDarkMode, getSystemColorScheme, } from './themeUtils.js';
12
12
  // Error utilities
13
- export { HttpStatus, getErrorStatus, getErrorMessage, isAlreadyRegisteredError, isUnauthorizedError, isForbiddenError, isNotFoundError, isRateLimitError, isServerError, isNetworkError, isRetryableError, } from './errorUtils';
13
+ export { HttpStatus, getErrorStatus, getErrorMessage, isAlreadyRegisteredError, isUnauthorizedError, isForbiddenError, isNotFoundError, isRateLimitError, isServerError, isNetworkError, isRetryableError, } from './errorUtils.js';
14
14
  // Network utilities
15
- export { DEFAULT_CIRCUIT_BREAKER_CONFIG, createCircuitBreakerState, calculateBackoffInterval, recordFailure, recordSuccess, shouldAllowRequest, delay, withRetry, } from './networkUtils';
15
+ export { DEFAULT_CIRCUIT_BREAKER_CONFIG, createCircuitBreakerState, calculateBackoffInterval, recordFailure, recordSuccess, shouldAllowRequest, delay, withRetry, } from './networkUtils.js';
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Async utilities for common asynchronous patterns and error handling
3
3
  */
4
- import { logger } from './loggerUtils';
4
+ import { logger } from './loggerUtils.js';
5
5
  /**
6
6
  * Wrapper for async operations with automatic error handling
7
7
  * Returns null on error instead of throwing
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Authentication helper utilities for common token validation
3
+ * and authentication error handling patterns.
4
+ */
5
+ /**
6
+ * Error thrown when session sync is required
7
+ */
8
+ export class SessionSyncRequiredError extends Error {
9
+ constructor(message = 'Session needs to be synced. Please try again.') {
10
+ super(message);
11
+ this.name = 'SessionSyncRequiredError';
12
+ }
13
+ }
14
+ /**
15
+ * Error thrown when authentication fails
16
+ */
17
+ export class AuthenticationFailedError extends Error {
18
+ constructor(message = 'Authentication failed. Please sign in again.') {
19
+ super(message);
20
+ this.name = 'AuthenticationFailedError';
21
+ }
22
+ }
23
+ /**
24
+ * Ensures a valid token exists before making authenticated API calls.
25
+ * If no valid token exists and an active session ID is available,
26
+ * attempts to refresh the token using the session.
27
+ *
28
+ * @throws {SessionSyncRequiredError} If the session needs to be synced (offline session)
29
+ */
30
+ export async function ensureValidToken(oxyServices, activeSessionId) {
31
+ if (oxyServices.hasValidToken() || !activeSessionId) {
32
+ return;
33
+ }
34
+ try {
35
+ await oxyServices.getTokenBySession(activeSessionId);
36
+ }
37
+ catch (tokenError) {
38
+ const errorMessage = tokenError instanceof Error ? tokenError.message : String(tokenError);
39
+ if (errorMessage.includes('AUTH_REQUIRED_OFFLINE_SESSION') || errorMessage.includes('offline')) {
40
+ throw new SessionSyncRequiredError();
41
+ }
42
+ throw tokenError;
43
+ }
44
+ }
45
+ /**
46
+ * Checks if an error is an authentication error (401 or auth-related message)
47
+ */
48
+ export function isAuthenticationError(error) {
49
+ if (!error || typeof error !== 'object') {
50
+ return false;
51
+ }
52
+ const errorObj = error;
53
+ const errorMessage = errorObj.message || '';
54
+ const status = errorObj.status || errorObj.response?.status;
55
+ return (status === 401 ||
56
+ errorMessage.includes('Authentication required') ||
57
+ errorMessage.includes('Invalid or missing authorization header'));
58
+ }
59
+ /**
60
+ * Wraps an API call with authentication error handling.
61
+ * On auth failure, optionally attempts to sync the session and retry.
62
+ *
63
+ * @throws {AuthenticationFailedError} If authentication fails and cannot be recovered
64
+ */
65
+ export async function withAuthErrorHandling(apiCall, options) {
66
+ try {
67
+ return await apiCall();
68
+ }
69
+ catch (error) {
70
+ if (!isAuthenticationError(error)) {
71
+ throw error;
72
+ }
73
+ if (options?.syncSession && options?.activeSessionId && options?.oxyServices) {
74
+ try {
75
+ await options.syncSession();
76
+ await options.oxyServices.getTokenBySession(options.activeSessionId);
77
+ return await apiCall();
78
+ }
79
+ catch {
80
+ throw new AuthenticationFailedError();
81
+ }
82
+ }
83
+ throw new AuthenticationFailedError();
84
+ }
85
+ }
86
+ /**
87
+ * Combines token validation and auth error handling for a complete authenticated API call.
88
+ *
89
+ * @example
90
+ * ```ts
91
+ * return await authenticatedApiCall(
92
+ * oxyServices,
93
+ * activeSessionId,
94
+ * () => oxyServices.updateProfile(updates)
95
+ * );
96
+ * ```
97
+ */
98
+ export async function authenticatedApiCall(oxyServices, activeSessionId, apiCall, syncSession) {
99
+ await ensureValidToken(oxyServices, activeSessionId);
100
+ return withAuthErrorHandling(apiCall, {
101
+ syncSession,
102
+ activeSessionId,
103
+ oxyServices,
104
+ });
105
+ }
@@ -1,4 +1,4 @@
1
- import { logger } from './loggerUtils';
1
+ import { logger } from './loggerUtils.js';
2
2
  /**
3
3
  * Error handling utilities for consistent error processing
4
4
  */
@@ -1,7 +1,7 @@
1
- export { DeviceManager } from './deviceManager';
1
+ export { DeviceManager } from './deviceManager.js';
2
2
  // Request utilities
3
- export { RequestDeduplicator, RequestQueue, SimpleLogger } from './requestUtils';
3
+ export { RequestDeduplicator, RequestQueue, SimpleLogger } from './requestUtils.js';
4
4
  // Cache utilities
5
- export { TTLCache, createCache, registerCacheForCleanup, unregisterCacheFromCleanup } from './cache';
5
+ export { TTLCache, createCache, registerCacheForCleanup, unregisterCacheFromCleanup } from './cache.js';
6
6
  // Session utilities
7
- export { normalizeSession, sortSessions, deduplicateSessions, deduplicateSessionsByUserId, normalizeAndSortSessions, mergeSessions, sessionsEqual, sessionsArraysEqual } from './sessionUtils';
7
+ export { normalizeSession, sortSessions, deduplicateSessions, deduplicateSessionsByUserId, normalizeAndSortSessions, mergeSessions, sessionsEqual, sessionsArraysEqual } from './sessionUtils.js';
@@ -43,6 +43,8 @@ export { DEFAULT_CIRCUIT_BREAKER_CONFIG, createCircuitBreakerState, calculateBac
43
43
  export type { CircuitBreakerState, CircuitBreakerConfig } from './shared/utils/networkUtils';
44
44
  export { isDev, debugLog, debugWarn, debugError, createDebugLogger, } from './shared/utils/debugUtils';
45
45
  export { translate } from './i18n';
46
+ export { SessionSyncRequiredError, AuthenticationFailedError, ensureValidToken, isAuthenticationError, withAuthErrorHandling, authenticatedApiCall, } from './utils/authHelpers';
47
+ export type { HandleApiErrorOptions } from './utils/authHelpers';
46
48
  export { mergeSessions, normalizeAndSortSessions, sessionsArraysEqual } from './utils/sessionUtils';
47
49
  export { packageInfo } from './constants/version';
48
50
  export * from './utils/apiUtils';
@@ -3,6 +3,7 @@ import type { SessionLoginResponse } from '../models/session';
3
3
  export interface FedCMAuthOptions {
4
4
  nonce?: string;
5
5
  context?: 'signin' | 'signup' | 'continue' | 'use';
6
+ loginHint?: string;
6
7
  }
7
8
  export interface FedCMConfig {
8
9
  enabled: boolean;
@@ -107,6 +108,7 @@ export declare function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(
107
108
  clientId: string;
108
109
  nonce: string;
109
110
  context?: string;
111
+ loginHint?: string;
110
112
  mediation?: "silent" | "optional" | "required";
111
113
  }): Promise<{
112
114
  token: string;
@@ -145,6 +147,10 @@ export declare function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(
145
147
  * @private
146
148
  */
147
149
  getClientId(): string;
150
+ /** @internal */
151
+ getStoredLoginHint(): string | undefined;
152
+ /** @internal */
153
+ storeLoginHint(userId: string): void;
148
154
  httpService: import("../HttpService").HttpService;
149
155
  cloudURL: string;
150
156
  config: import("../OxyServices.base").OxyConfig;
@@ -43,6 +43,17 @@ export declare function OxyServicesUtilityMixin<T extends typeof OxyServicesBase
43
43
  * Validates JWT tokens against the Oxy API and attaches user data to requests.
44
44
  * Uses server-side session validation for security (not just JWT decode).
45
45
  *
46
+ * **Design note — jwtDecode vs jwt.verify:**
47
+ * This middleware intentionally uses `jwtDecode()` (decode-only, no signature
48
+ * verification) for user tokens. This is by design, NOT a security gap:
49
+ * - Third-party apps using `oxy.auth()` don't have the Oxy JWT secret
50
+ * - Security comes from API-based session validation (`validateSession()`)
51
+ * which checks the session server-side on every request
52
+ * - Service tokens (type: 'service') DO use cryptographic HMAC verification
53
+ * via the `jwtSecret` option, since they are stateless
54
+ * - The backend's own `authMiddleware` uses `jwt.verify()` because it has
55
+ * direct access to `ACCESS_TOKEN_SECRET`
56
+ *
46
57
  * @example
47
58
  * ```typescript
48
59
  * import { OxyServices } from '@oxyhq/core';
@@ -74,6 +85,8 @@ export declare function OxyServicesUtilityMixin<T extends typeof OxyServicesBase
74
85
  * Returns a middleware function for Socket.IO that validates JWT tokens
75
86
  * from the handshake auth object and attaches user data to the socket.
76
87
  *
88
+ * Uses `jwtDecode()` + API session validation (same rationale as `auth()`).
89
+ *
77
90
  * @example
78
91
  * ```typescript
79
92
  * import { OxyServices } from '@oxyhq/core';