@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
@@ -20,6 +20,7 @@ const STORAGE_KEYS = {
20
20
  SESSION: 'oxy_session',
21
21
  USER: 'oxy_user',
22
22
  AUTH_METHOD: 'oxy_auth_method',
23
+ FEDCM_LOGIN_HINT: 'oxy_fedcm_login_hint',
23
24
  };
24
25
  /**
25
26
  * Default in-memory storage for non-browser environments.
@@ -300,6 +301,19 @@ class AuthManager {
300
301
  clearTimeout(this.refreshTimer);
301
302
  this.refreshTimer = null;
302
303
  }
304
+ // Invalidate current session on the server (best-effort)
305
+ try {
306
+ const sessionJson = await this.storage.getItem(STORAGE_KEYS.SESSION);
307
+ if (sessionJson) {
308
+ const session = JSON.parse(sessionJson);
309
+ if (session.sessionId && typeof this.oxyServices.logoutSession === 'function') {
310
+ await this.oxyServices.logoutSession(session.sessionId);
311
+ }
312
+ }
313
+ }
314
+ catch {
315
+ // Best-effort: don't block local signout on network failure
316
+ }
303
317
  // Revoke FedCM credential if supported
304
318
  try {
305
319
  const services = this.oxyServices;
@@ -327,6 +341,7 @@ class AuthManager {
327
341
  await this.storage.removeItem(STORAGE_KEYS.SESSION);
328
342
  await this.storage.removeItem(STORAGE_KEYS.USER);
329
343
  await this.storage.removeItem(STORAGE_KEYS.AUTH_METHOD);
344
+ await this.storage.removeItem(STORAGE_KEYS.FEDCM_LOGIN_HINT);
330
345
  }
331
346
  /**
332
347
  * Get current user.
package/dist/cjs/index.js CHANGED
@@ -30,7 +30,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
30
30
  };
31
31
  Object.defineProperty(exports, "__esModule", { value: true });
32
32
  exports.calculateBackoffInterval = exports.createCircuitBreakerState = exports.DEFAULT_CIRCUIT_BREAKER_CONFIG = exports.isRetryableError = exports.isNetworkError = exports.isServerError = exports.isRateLimitError = exports.isNotFoundError = exports.isForbiddenError = exports.isUnauthorizedError = exports.isAlreadyRegisteredError = exports.getErrorMessage = exports.getErrorStatus = exports.HttpStatus = exports.getSystemColorScheme = exports.systemPrefersDarkMode = exports.getOppositeTheme = exports.normalizeColorScheme = exports.normalizeTheme = exports.getContrastTextColor = exports.isLightColor = exports.withOpacity = exports.rgbToHex = exports.hexToRgb = exports.lightenColor = exports.darkenColor = exports.isAndroid = exports.isIOS = exports.isNative = exports.isWeb = exports.setPlatformOS = exports.getPlatformOS = exports.normalizeLanguageCode = exports.getNativeLanguageName = exports.getLanguageName = exports.getLanguageMetadata = exports.SUPPORTED_LANGUAGES = exports.DeviceManager = exports.RecoveryPhraseService = exports.SignatureService = exports.KeyManager = exports.createCrossDomainAuth = exports.CrossDomainAuth = exports.createAuthManager = exports.AuthManager = exports.oxyClient = exports.OXY_CLOUD_URL = exports.OxyAuthenticationTimeoutError = exports.OxyAuthenticationError = exports.OxyServices = void 0;
33
- exports.logPerformance = exports.logPayment = exports.logDevice = exports.logUser = exports.logSession = exports.logApi = exports.logAuth = exports.LogLevel = exports.logger = exports.retryAsync = exports.validateRequiredFields = exports.handleHttpError = exports.createApiError = exports.ErrorCodes = exports.packageInfo = exports.sessionsArraysEqual = exports.normalizeAndSortSessions = exports.mergeSessions = exports.translate = exports.createDebugLogger = exports.debugError = exports.debugWarn = exports.debugLog = exports.isDev = exports.withRetry = exports.delay = exports.shouldAllowRequest = exports.recordSuccess = exports.recordFailure = void 0;
33
+ exports.logPerformance = exports.logPayment = exports.logDevice = exports.logUser = exports.logSession = exports.logApi = exports.logAuth = exports.LogLevel = exports.logger = exports.retryAsync = exports.validateRequiredFields = exports.handleHttpError = exports.createApiError = exports.ErrorCodes = exports.packageInfo = exports.sessionsArraysEqual = exports.normalizeAndSortSessions = exports.mergeSessions = exports.authenticatedApiCall = exports.withAuthErrorHandling = exports.isAuthenticationError = exports.ensureValidToken = exports.AuthenticationFailedError = exports.SessionSyncRequiredError = exports.translate = exports.createDebugLogger = exports.debugError = exports.debugWarn = exports.debugLog = exports.isDev = exports.withRetry = exports.delay = exports.shouldAllowRequest = exports.recordSuccess = exports.recordFailure = void 0;
34
34
  // Ensure crypto polyfills are loaded before anything else
35
35
  require("./crypto/polyfill");
36
36
  // --- Core API Client ---
@@ -119,6 +119,14 @@ Object.defineProperty(exports, "createDebugLogger", { enumerable: true, get: fun
119
119
  // --- i18n ---
120
120
  var i18n_1 = require("./i18n");
121
121
  Object.defineProperty(exports, "translate", { enumerable: true, get: function () { return i18n_1.translate; } });
122
+ // --- Auth Helpers ---
123
+ var authHelpers_1 = require("./utils/authHelpers");
124
+ Object.defineProperty(exports, "SessionSyncRequiredError", { enumerable: true, get: function () { return authHelpers_1.SessionSyncRequiredError; } });
125
+ Object.defineProperty(exports, "AuthenticationFailedError", { enumerable: true, get: function () { return authHelpers_1.AuthenticationFailedError; } });
126
+ Object.defineProperty(exports, "ensureValidToken", { enumerable: true, get: function () { return authHelpers_1.ensureValidToken; } });
127
+ Object.defineProperty(exports, "isAuthenticationError", { enumerable: true, get: function () { return authHelpers_1.isAuthenticationError; } });
128
+ Object.defineProperty(exports, "withAuthErrorHandling", { enumerable: true, get: function () { return authHelpers_1.withAuthErrorHandling; } });
129
+ Object.defineProperty(exports, "authenticatedApiCall", { enumerable: true, get: function () { return authHelpers_1.authenticatedApiCall; } });
122
130
  // --- Session Utilities ---
123
131
  var sessionUtils_1 = require("./utils/sessionUtils");
124
132
  Object.defineProperty(exports, "mergeSessions", { enumerable: true, get: function () { return sessionUtils_1.mergeSessions; } });
@@ -5,6 +5,7 @@ exports.FedCMMixin = OxyServicesFedCMMixin;
5
5
  const OxyServices_errors_1 = require("../OxyServices.errors");
6
6
  const debugUtils_1 = require("../shared/utils/debugUtils");
7
7
  const debug = (0, debugUtils_1.createDebugLogger)('FedCM');
8
+ const FEDCM_LOGIN_HINT_KEY = 'oxy_fedcm_login_hint';
8
9
  // Global lock to prevent concurrent FedCM requests
9
10
  // FedCM only allows one navigator.credentials.get request at a time
10
11
  let fedCMRequestInProgress = false;
@@ -82,13 +83,16 @@ function OxyServicesFedCMMixin(Base) {
82
83
  try {
83
84
  const nonce = options.nonce || this.generateNonce();
84
85
  const clientId = this.getClientId();
85
- debug.log('Interactive sign-in: Requesting credential for', clientId);
86
+ // Use provided loginHint, or fall back to stored last-used account ID
87
+ const loginHint = options.loginHint || this.getStoredLoginHint();
88
+ debug.log('Interactive sign-in: Requesting credential for', clientId, loginHint ? `(hint: ${loginHint})` : '');
86
89
  // Request credential from browser's native identity flow
87
90
  const credential = await this.requestIdentityCredential({
88
91
  configURL: this.constructor.DEFAULT_CONFIG_URL,
89
92
  clientId,
90
93
  nonce,
91
94
  context: options.context,
95
+ loginHint,
92
96
  });
93
97
  if (!credential || !credential.token) {
94
98
  throw new OxyServices_errors_1.OxyAuthenticationError('No credential received from browser');
@@ -100,6 +104,10 @@ function OxyServicesFedCMMixin(Base) {
100
104
  if (session && session.accessToken) {
101
105
  this.httpService.setTokens(session.accessToken);
102
106
  }
107
+ // Store the user ID as loginHint for future FedCM requests
108
+ if (session?.user?.id) {
109
+ this.storeLoginHint(session.user.id);
110
+ }
103
111
  debug.log('Interactive sign-in: Success!', { userId: session?.user?.id });
104
112
  return session;
105
113
  }
@@ -164,13 +172,15 @@ function OxyServicesFedCMMixin(Base) {
164
172
  // this runs on app startup — showing browser UI without user action is bad UX.
165
173
  // Optional/interactive mediation should only happen when the user clicks "Sign In".
166
174
  let credential = null;
175
+ const loginHint = this.getStoredLoginHint();
167
176
  try {
168
177
  const nonce = this.generateNonce();
169
- debug.log('Silent SSO: Attempting silent mediation...');
178
+ debug.log('Silent SSO: Attempting silent mediation...', loginHint ? `(hint: ${loginHint})` : '');
170
179
  credential = await this.requestIdentityCredential({
171
180
  configURL: this.constructor.DEFAULT_CONFIG_URL,
172
181
  clientId,
173
182
  nonce,
183
+ loginHint,
174
184
  mediation: 'silent',
175
185
  });
176
186
  debug.log('Silent SSO: Silent mediation result:', { hasCredential: !!credential, hasToken: !!credential?.token });
@@ -225,6 +235,10 @@ function OxyServicesFedCMMixin(Base) {
225
235
  else {
226
236
  debug.warn('Silent SSO: No accessToken in session response');
227
237
  }
238
+ // Store the user ID as loginHint for future FedCM requests
239
+ if (session.user?.id) {
240
+ this.storeLoginHint(session.user.id);
241
+ }
228
242
  debug.log('Silent SSO: Success!', {
229
243
  sessionId: session.sessionId?.substring(0, 8) + '...',
230
244
  userId: session.user?.id
@@ -299,7 +313,7 @@ function OxyServicesFedCMMixin(Base) {
299
313
  params: {
300
314
  nonce: options.nonce, // For Chrome 145+
301
315
  },
302
- ...(options.context && { loginHint: options.context }),
316
+ ...(options.loginHint && { loginHint: options.loginHint }),
303
317
  },
304
318
  ],
305
319
  },
@@ -419,6 +433,28 @@ function OxyServicesFedCMMixin(Base) {
419
433
  }
420
434
  return window.location.origin;
421
435
  }
436
+ /** @internal */
437
+ getStoredLoginHint() {
438
+ if (typeof window === 'undefined')
439
+ return undefined;
440
+ try {
441
+ return localStorage.getItem(FEDCM_LOGIN_HINT_KEY) || undefined;
442
+ }
443
+ catch {
444
+ return undefined;
445
+ }
446
+ }
447
+ /** @internal */
448
+ storeLoginHint(userId) {
449
+ if (typeof window === 'undefined')
450
+ return;
451
+ try {
452
+ localStorage.setItem(FEDCM_LOGIN_HINT_KEY, userId);
453
+ }
454
+ catch {
455
+ // Storage full or blocked
456
+ }
457
+ }
422
458
  },
423
459
  _a.DEFAULT_CONFIG_URL = 'https://auth.oxy.so/fedcm.json',
424
460
  _a.FEDCM_TIMEOUT = 15000 // 15 seconds for interactive
@@ -67,6 +67,17 @@ function OxyServicesUtilityMixin(Base) {
67
67
  * Validates JWT tokens against the Oxy API and attaches user data to requests.
68
68
  * Uses server-side session validation for security (not just JWT decode).
69
69
  *
70
+ * **Design note — jwtDecode vs jwt.verify:**
71
+ * This middleware intentionally uses `jwtDecode()` (decode-only, no signature
72
+ * verification) for user tokens. This is by design, NOT a security gap:
73
+ * - Third-party apps using `oxy.auth()` don't have the Oxy JWT secret
74
+ * - Security comes from API-based session validation (`validateSession()`)
75
+ * which checks the session server-side on every request
76
+ * - Service tokens (type: 'service') DO use cryptographic HMAC verification
77
+ * via the `jwtSecret` option, since they are stateless
78
+ * - The backend's own `authMiddleware` uses `jwt.verify()` because it has
79
+ * direct access to `ACCESS_TOKEN_SECRET`
80
+ *
70
81
  * @example
71
82
  * ```typescript
72
83
  * import { OxyServices } from '@oxyhq/core';
@@ -119,6 +130,7 @@ function OxyServicesUtilityMixin(Base) {
119
130
  return next();
120
131
  }
121
132
  const error = {
133
+ error: 'MISSING_TOKEN',
122
134
  message: 'Access token required',
123
135
  code: 'MISSING_TOKEN',
124
136
  status: 401
@@ -139,6 +151,7 @@ function OxyServicesUtilityMixin(Base) {
139
151
  return next();
140
152
  }
141
153
  const error = {
154
+ error: 'INVALID_TOKEN_FORMAT',
142
155
  message: 'Invalid token format',
143
156
  code: 'INVALID_TOKEN_FORMAT',
144
157
  status: 401
@@ -158,6 +171,7 @@ function OxyServicesUtilityMixin(Base) {
158
171
  return next();
159
172
  }
160
173
  const error = {
174
+ error: 'SERVICE_TOKEN_NOT_CONFIGURED',
161
175
  message: 'Service token verification not configured',
162
176
  code: 'SERVICE_TOKEN_NOT_CONFIGURED',
163
177
  status: 403
@@ -192,7 +206,7 @@ function OxyServicesUtilityMixin(Base) {
192
206
  (verifyError.message === 'Invalid signature' || verifyError.message === 'Invalid token structure');
193
207
  if (!isSignatureError) {
194
208
  console.error('[oxy.auth] Unexpected error during service token verification:', verifyError);
195
- const error = { message: 'Internal authentication error', code: 'AUTH_INTERNAL_ERROR', status: 500 };
209
+ const error = { error: 'AUTH_INTERNAL_ERROR', message: 'Internal authentication error', code: 'AUTH_INTERNAL_ERROR', status: 500 };
196
210
  if (onError)
197
211
  return onError(error);
198
212
  return res.status(500).json(error);
@@ -202,7 +216,7 @@ function OxyServicesUtilityMixin(Base) {
202
216
  req.user = null;
203
217
  return next();
204
218
  }
205
- const error = { message: 'Invalid service token signature', code: 'INVALID_SERVICE_TOKEN', status: 401 };
219
+ const error = { error: 'INVALID_SERVICE_TOKEN', message: 'Invalid service token signature', code: 'INVALID_SERVICE_TOKEN', status: 401 };
206
220
  if (onError)
207
221
  return onError(error);
208
222
  return res.status(401).json(error);
@@ -214,7 +228,7 @@ function OxyServicesUtilityMixin(Base) {
214
228
  req.user = null;
215
229
  return next();
216
230
  }
217
- const error = { message: 'Service token expired', code: 'TOKEN_EXPIRED', status: 401 };
231
+ const error = { error: 'TOKEN_EXPIRED', message: 'Service token expired', code: 'TOKEN_EXPIRED', status: 401 };
218
232
  if (onError)
219
233
  return onError(error);
220
234
  return res.status(401).json(error);
@@ -226,7 +240,7 @@ function OxyServicesUtilityMixin(Base) {
226
240
  req.user = null;
227
241
  return next();
228
242
  }
229
- const error = { message: 'Invalid service token: missing appId', code: 'INVALID_SERVICE_TOKEN', status: 401 };
243
+ const error = { error: 'INVALID_SERVICE_TOKEN', message: 'Invalid service token: missing appId', code: 'INVALID_SERVICE_TOKEN', status: 401 };
230
244
  if (onError)
231
245
  return onError(error);
232
246
  return res.status(401).json(error);
@@ -253,6 +267,7 @@ function OxyServicesUtilityMixin(Base) {
253
267
  return next();
254
268
  }
255
269
  const error = {
270
+ error: 'INVALID_TOKEN_PAYLOAD',
256
271
  message: 'Token missing user ID',
257
272
  code: 'INVALID_TOKEN_PAYLOAD',
258
273
  status: 401
@@ -269,6 +284,7 @@ function OxyServicesUtilityMixin(Base) {
269
284
  return next();
270
285
  }
271
286
  const error = {
287
+ error: 'TOKEN_EXPIRED',
272
288
  message: 'Token expired',
273
289
  code: 'TOKEN_EXPIRED',
274
290
  status: 401
@@ -291,6 +307,7 @@ function OxyServicesUtilityMixin(Base) {
291
307
  return next();
292
308
  }
293
309
  const error = {
310
+ error: 'INVALID_SESSION',
294
311
  message: 'Session invalid or expired',
295
312
  code: 'INVALID_SESSION',
296
313
  status: 401
@@ -325,6 +342,7 @@ function OxyServicesUtilityMixin(Base) {
325
342
  return next();
326
343
  }
327
344
  const error = {
345
+ error: 'SESSION_VALIDATION_ERROR',
328
346
  message: 'Session validation failed',
329
347
  code: 'SESSION_VALIDATION_ERROR',
330
348
  status: 401
@@ -382,6 +400,8 @@ function OxyServicesUtilityMixin(Base) {
382
400
  * Returns a middleware function for Socket.IO that validates JWT tokens
383
401
  * from the handshake auth object and attaches user data to the socket.
384
402
  *
403
+ * Uses `jwtDecode()` + API session validation (same rationale as `auth()`).
404
+ *
385
405
  * @example
386
406
  * ```typescript
387
407
  * import { OxyServices } from '@oxyhq/core';
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ /**
3
+ * Authentication helper utilities for common token validation
4
+ * and authentication error handling patterns.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.AuthenticationFailedError = exports.SessionSyncRequiredError = void 0;
8
+ exports.ensureValidToken = ensureValidToken;
9
+ exports.isAuthenticationError = isAuthenticationError;
10
+ exports.withAuthErrorHandling = withAuthErrorHandling;
11
+ exports.authenticatedApiCall = authenticatedApiCall;
12
+ /**
13
+ * Error thrown when session sync is required
14
+ */
15
+ class SessionSyncRequiredError extends Error {
16
+ constructor(message = 'Session needs to be synced. Please try again.') {
17
+ super(message);
18
+ this.name = 'SessionSyncRequiredError';
19
+ }
20
+ }
21
+ exports.SessionSyncRequiredError = SessionSyncRequiredError;
22
+ /**
23
+ * Error thrown when authentication fails
24
+ */
25
+ class AuthenticationFailedError extends Error {
26
+ constructor(message = 'Authentication failed. Please sign in again.') {
27
+ super(message);
28
+ this.name = 'AuthenticationFailedError';
29
+ }
30
+ }
31
+ exports.AuthenticationFailedError = AuthenticationFailedError;
32
+ /**
33
+ * Ensures a valid token exists before making authenticated API calls.
34
+ * If no valid token exists and an active session ID is available,
35
+ * attempts to refresh the token using the session.
36
+ *
37
+ * @throws {SessionSyncRequiredError} If the session needs to be synced (offline session)
38
+ */
39
+ async function ensureValidToken(oxyServices, activeSessionId) {
40
+ if (oxyServices.hasValidToken() || !activeSessionId) {
41
+ return;
42
+ }
43
+ try {
44
+ await oxyServices.getTokenBySession(activeSessionId);
45
+ }
46
+ catch (tokenError) {
47
+ const errorMessage = tokenError instanceof Error ? tokenError.message : String(tokenError);
48
+ if (errorMessage.includes('AUTH_REQUIRED_OFFLINE_SESSION') || errorMessage.includes('offline')) {
49
+ throw new SessionSyncRequiredError();
50
+ }
51
+ throw tokenError;
52
+ }
53
+ }
54
+ /**
55
+ * Checks if an error is an authentication error (401 or auth-related message)
56
+ */
57
+ function isAuthenticationError(error) {
58
+ if (!error || typeof error !== 'object') {
59
+ return false;
60
+ }
61
+ const errorObj = error;
62
+ const errorMessage = errorObj.message || '';
63
+ const status = errorObj.status || errorObj.response?.status;
64
+ return (status === 401 ||
65
+ errorMessage.includes('Authentication required') ||
66
+ errorMessage.includes('Invalid or missing authorization header'));
67
+ }
68
+ /**
69
+ * Wraps an API call with authentication error handling.
70
+ * On auth failure, optionally attempts to sync the session and retry.
71
+ *
72
+ * @throws {AuthenticationFailedError} If authentication fails and cannot be recovered
73
+ */
74
+ async function withAuthErrorHandling(apiCall, options) {
75
+ try {
76
+ return await apiCall();
77
+ }
78
+ catch (error) {
79
+ if (!isAuthenticationError(error)) {
80
+ throw error;
81
+ }
82
+ if (options?.syncSession && options?.activeSessionId && options?.oxyServices) {
83
+ try {
84
+ await options.syncSession();
85
+ await options.oxyServices.getTokenBySession(options.activeSessionId);
86
+ return await apiCall();
87
+ }
88
+ catch {
89
+ throw new AuthenticationFailedError();
90
+ }
91
+ }
92
+ throw new AuthenticationFailedError();
93
+ }
94
+ }
95
+ /**
96
+ * Combines token validation and auth error handling for a complete authenticated API call.
97
+ *
98
+ * @example
99
+ * ```ts
100
+ * return await authenticatedApiCall(
101
+ * oxyServices,
102
+ * activeSessionId,
103
+ * () => oxyServices.updateProfile(updates)
104
+ * );
105
+ * ```
106
+ */
107
+ async function authenticatedApiCall(oxyServices, activeSessionId, apiCall, syncSession) {
108
+ await ensureValidToken(oxyServices, activeSessionId);
109
+ return withAuthErrorHandling(apiCall, {
110
+ syncSession,
111
+ activeSessionId,
112
+ oxyServices,
113
+ });
114
+ }
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * @module core/AuthManager
8
8
  */
9
- import { retryAsync } from './utils/asyncUtils';
9
+ import { retryAsync } from './utils/asyncUtils.js';
10
10
  /**
11
11
  * Storage keys used by AuthManager.
12
12
  */
@@ -16,6 +16,7 @@ const STORAGE_KEYS = {
16
16
  SESSION: 'oxy_session',
17
17
  USER: 'oxy_user',
18
18
  AUTH_METHOD: 'oxy_auth_method',
19
+ FEDCM_LOGIN_HINT: 'oxy_fedcm_login_hint',
19
20
  };
20
21
  /**
21
22
  * Default in-memory storage for non-browser environments.
@@ -296,6 +297,19 @@ export class AuthManager {
296
297
  clearTimeout(this.refreshTimer);
297
298
  this.refreshTimer = null;
298
299
  }
300
+ // Invalidate current session on the server (best-effort)
301
+ try {
302
+ const sessionJson = await this.storage.getItem(STORAGE_KEYS.SESSION);
303
+ if (sessionJson) {
304
+ const session = JSON.parse(sessionJson);
305
+ if (session.sessionId && typeof this.oxyServices.logoutSession === 'function') {
306
+ await this.oxyServices.logoutSession(session.sessionId);
307
+ }
308
+ }
309
+ }
310
+ catch {
311
+ // Best-effort: don't block local signout on network failure
312
+ }
299
313
  // Revoke FedCM credential if supported
300
314
  try {
301
315
  const services = this.oxyServices;
@@ -323,6 +337,7 @@ export class AuthManager {
323
337
  await this.storage.removeItem(STORAGE_KEYS.SESSION);
324
338
  await this.storage.removeItem(STORAGE_KEYS.USER);
325
339
  await this.storage.removeItem(STORAGE_KEYS.AUTH_METHOD);
340
+ await this.storage.removeItem(STORAGE_KEYS.FEDCM_LOGIN_HINT);
326
341
  }
327
342
  /**
328
343
  * Get current user.
@@ -12,13 +12,13 @@
12
12
  * - Error handling
13
13
  * - Request queuing
14
14
  */
15
- import { TTLCache, registerCacheForCleanup } from './utils/cache';
16
- import { RequestDeduplicator, RequestQueue, SimpleLogger } from './utils/requestUtils';
17
- import { retryAsync } from './utils/asyncUtils';
18
- import { handleHttpError } from './utils/errorUtils';
19
- import { isDev } from './shared/utils/debugUtils';
15
+ import { TTLCache, registerCacheForCleanup } from './utils/cache.js';
16
+ import { RequestDeduplicator, RequestQueue, SimpleLogger } from './utils/requestUtils.js';
17
+ import { retryAsync } from './utils/asyncUtils.js';
18
+ import { handleHttpError } from './utils/errorUtils.js';
19
+ import { isDev } from './shared/utils/debugUtils.js';
20
20
  import { jwtDecode } from 'jwt-decode';
21
- import { isNative, getPlatformOS } from './utils/platform';
21
+ import { isNative, getPlatformOS } from './utils/platform.js';
22
22
  /**
23
23
  * Check if we're running in a native app environment (React Native, not web)
24
24
  * This is used to determine CSRF handling mode
@@ -4,9 +4,9 @@
4
4
  * Contains core infrastructure, HTTP client, request management, and error handling
5
5
  */
6
6
  import { jwtDecode } from 'jwt-decode';
7
- import { handleHttpError } from './utils/errorUtils';
8
- import { HttpService } from './HttpService';
9
- import { OxyAuthenticationError, OxyAuthenticationTimeoutError } from './OxyServices.errors';
7
+ import { handleHttpError } from './utils/errorUtils.js';
8
+ import { HttpService } from './HttpService.js';
9
+ import { OxyAuthenticationError, OxyAuthenticationTimeoutError } from './OxyServices.errors.js';
10
10
  /**
11
11
  * Base class for OxyServices with core infrastructure
12
12
  */
@@ -1,6 +1,6 @@
1
- import { OxyAuthenticationError, OxyAuthenticationTimeoutError } from './OxyServices.errors';
1
+ import { OxyAuthenticationError, OxyAuthenticationTimeoutError } from './OxyServices.errors.js';
2
2
  // Import mixin composition helper
3
- import { composeOxyServices } from './mixins';
3
+ import { composeOxyServices } from './mixins.js';
4
4
  /**
5
5
  * OxyServices - Unified client library for interacting with the Oxy API
6
6
  *
@@ -5,9 +5,9 @@
5
5
  * Handles key generation, secure storage, digital signatures, and recovery phrases.
6
6
  */
7
7
  // Import polyfills first - this ensures Buffer is available for bip39 and other libraries
8
- import './polyfill';
9
- export { KeyManager } from './keyManager';
10
- export { SignatureService } from './signatureService';
11
- export { RecoveryPhraseService } from './recoveryPhrase';
8
+ import './polyfill.js';
9
+ export { KeyManager } from './keyManager.js';
10
+ export { SignatureService } from './signatureService.js';
11
+ export { RecoveryPhraseService } from './recoveryPhrase.js';
12
12
  // Re-export for convenience
13
- export { KeyManager as default } from './keyManager';
13
+ export { KeyManager as default } from './keyManager.js';
@@ -5,9 +5,9 @@
5
5
  * Private keys are stored securely using expo-secure-store and never leave the device.
6
6
  */
7
7
  import { ec as EC } from 'elliptic';
8
- import { isWeb, isIOS, isAndroid, isReactNative, isNodeJS } from '../utils/platform';
9
- import { logger } from '../utils/loggerUtils';
10
- import { isDev } from '../shared/utils/debugUtils';
8
+ import { isWeb, isIOS, isAndroid, isReactNative, isNodeJS } from '../utils/platform.js';
9
+ import { logger } from '../utils/loggerUtils.js';
10
+ import { isDev } from '../shared/utils/debugUtils.js';
11
11
  // Lazy imports for React Native specific modules
12
12
  let SecureStore = null;
13
13
  let ExpoCrypto = null;
@@ -7,7 +7,7 @@
7
7
  * Note: This module requires the polyfill to be loaded first (done via crypto/index.ts)
8
8
  */
9
9
  import * as bip39 from 'bip39';
10
- import { KeyManager } from './keyManager';
10
+ import { KeyManager } from './keyManager.js';
11
11
  /**
12
12
  * Convert Uint8Array or array-like to hexadecimal string
13
13
  * Works in both Node.js and React Native without depending on Buffer
@@ -5,8 +5,8 @@
5
5
  * Used for authenticating requests and proving identity ownership.
6
6
  */
7
7
  import { ec as EC } from 'elliptic';
8
- import { KeyManager } from './keyManager';
9
- import { isReactNative, isNodeJS } from '../utils/platform';
8
+ import { KeyManager } from './keyManager.js';
9
+ import { isReactNative, isNodeJS } from '../utils/platform.js';
10
10
  // Lazy import for expo-crypto
11
11
  let ExpoCrypto = null;
12
12
  const ec = new EC('secp256k1');
@@ -1,14 +1,14 @@
1
- import enUS from './locales/en-US.json';
2
- import esES from './locales/es-ES.json';
3
- import caES from './locales/ca-ES.json';
4
- import frFR from './locales/fr-FR.json';
5
- import deDE from './locales/de-DE.json';
6
- import itIT from './locales/it-IT.json';
7
- import ptPT from './locales/pt-PT.json';
8
- import jaJP from './locales/ja-JP.json';
9
- import koKR from './locales/ko-KR.json';
10
- import zhCN from './locales/zh-CN.json';
11
- import arSA from './locales/ar-SA.json';
1
+ import enUS from './locales/en-US.json.js';
2
+ import esES from './locales/es-ES.json.js';
3
+ import caES from './locales/ca-ES.json.js';
4
+ import frFR from './locales/fr-FR.json.js';
5
+ import deDE from './locales/de-DE.json.js';
6
+ import itIT from './locales/it-IT.json.js';
7
+ import ptPT from './locales/pt-PT.json.js';
8
+ import jaJP from './locales/ja-JP.json.js';
9
+ import koKR from './locales/ko-KR.json.js';
10
+ import zhCN from './locales/zh-CN.json.js';
11
+ import arSA from './locales/ar-SA.json.js';
12
12
  const DICTS = {
13
13
  'en': enUS,
14
14
  'en-US': enUS,
package/dist/esm/index.js CHANGED
@@ -14,42 +14,44 @@
14
14
  * ```
15
15
  */
16
16
  // Ensure crypto polyfills are loaded before anything else
17
- import './crypto/polyfill';
17
+ import './crypto/polyfill.js';
18
18
  // --- Core API Client ---
19
- export { OxyServices, OxyAuthenticationError, OxyAuthenticationTimeoutError } from './OxyServices';
20
- export { OXY_CLOUD_URL, oxyClient } from './OxyServices';
19
+ export { OxyServices, OxyAuthenticationError, OxyAuthenticationTimeoutError } from './OxyServices.js';
20
+ export { OXY_CLOUD_URL, oxyClient } from './OxyServices.js';
21
21
  // --- Authentication ---
22
- export { AuthManager, createAuthManager } from './AuthManager';
23
- export { CrossDomainAuth, createCrossDomainAuth } from './CrossDomainAuth';
22
+ export { AuthManager, createAuthManager } from './AuthManager.js';
23
+ export { CrossDomainAuth, createCrossDomainAuth } from './CrossDomainAuth.js';
24
24
  // --- Crypto / Identity ---
25
- export { KeyManager, SignatureService, RecoveryPhraseService } from './crypto';
25
+ export { KeyManager, SignatureService, RecoveryPhraseService } from './crypto.js';
26
26
  // --- Models & Types ---
27
- export * from './models/interfaces';
28
- export * from './models/session';
27
+ export * from './models/interfaces.js';
28
+ export * from './models/session.js';
29
29
  // --- Device Management ---
30
- export { DeviceManager } from './utils/deviceManager';
30
+ export { DeviceManager } from './utils/deviceManager.js';
31
31
  // --- Language Utilities ---
32
- export { SUPPORTED_LANGUAGES, getLanguageMetadata, getLanguageName, getNativeLanguageName, normalizeLanguageCode, } from './utils/languageUtils';
32
+ export { SUPPORTED_LANGUAGES, getLanguageMetadata, getLanguageName, getNativeLanguageName, normalizeLanguageCode, } from './utils/languageUtils.js';
33
33
  // --- Platform Detection ---
34
- export { getPlatformOS, setPlatformOS, isWeb, isNative, isIOS, isAndroid, } from './utils/platform';
34
+ export { getPlatformOS, setPlatformOS, isWeb, isNative, isIOS, isAndroid, } from './utils/platform.js';
35
35
  // --- Shared Utilities ---
36
- export { darkenColor, lightenColor, hexToRgb, rgbToHex, withOpacity, isLightColor, getContrastTextColor, } from './shared/utils/colorUtils';
37
- export { normalizeTheme, normalizeColorScheme, getOppositeTheme, systemPrefersDarkMode, getSystemColorScheme, } from './shared/utils/themeUtils';
38
- export { HttpStatus, getErrorStatus, getErrorMessage, isAlreadyRegisteredError, isUnauthorizedError, isForbiddenError, isNotFoundError, isRateLimitError, isServerError, isNetworkError, isRetryableError, } from './shared/utils/errorUtils';
39
- export { DEFAULT_CIRCUIT_BREAKER_CONFIG, createCircuitBreakerState, calculateBackoffInterval, recordFailure, recordSuccess, shouldAllowRequest, delay, withRetry, } from './shared/utils/networkUtils';
40
- export { isDev, debugLog, debugWarn, debugError, createDebugLogger, } from './shared/utils/debugUtils';
36
+ export { darkenColor, lightenColor, hexToRgb, rgbToHex, withOpacity, isLightColor, getContrastTextColor, } from './shared/utils/colorUtils.js';
37
+ export { normalizeTheme, normalizeColorScheme, getOppositeTheme, systemPrefersDarkMode, getSystemColorScheme, } from './shared/utils/themeUtils.js';
38
+ export { HttpStatus, getErrorStatus, getErrorMessage, isAlreadyRegisteredError, isUnauthorizedError, isForbiddenError, isNotFoundError, isRateLimitError, isServerError, isNetworkError, isRetryableError, } from './shared/utils/errorUtils.js';
39
+ export { DEFAULT_CIRCUIT_BREAKER_CONFIG, createCircuitBreakerState, calculateBackoffInterval, recordFailure, recordSuccess, shouldAllowRequest, delay, withRetry, } from './shared/utils/networkUtils.js';
40
+ export { isDev, debugLog, debugWarn, debugError, createDebugLogger, } from './shared/utils/debugUtils.js';
41
41
  // --- i18n ---
42
- export { translate } from './i18n';
42
+ export { translate } from './i18n.js';
43
+ // --- Auth Helpers ---
44
+ export { SessionSyncRequiredError, AuthenticationFailedError, ensureValidToken, isAuthenticationError, withAuthErrorHandling, authenticatedApiCall, } from './utils/authHelpers.js';
43
45
  // --- Session Utilities ---
44
- export { mergeSessions, normalizeAndSortSessions, sessionsArraysEqual } from './utils/sessionUtils';
46
+ export { mergeSessions, normalizeAndSortSessions, sessionsArraysEqual } from './utils/sessionUtils.js';
45
47
  // --- Constants ---
46
- export { packageInfo } from './constants/version';
48
+ export { packageInfo } from './constants/version.js';
47
49
  // --- API & Error Utilities ---
48
- export * from './utils/apiUtils';
49
- export { ErrorCodes, createApiError, handleHttpError, validateRequiredFields, } from './utils/errorUtils';
50
- export { retryAsync } from './utils/asyncUtils';
51
- export * from './utils/validationUtils';
52
- export { logger, LogLevel, logAuth, logApi, logSession, logUser, logDevice, logPayment, logPerformance, } from './utils/loggerUtils';
50
+ export * from './utils/apiUtils.js';
51
+ export { ErrorCodes, createApiError, handleHttpError, validateRequiredFields, } from './utils/errorUtils.js';
52
+ export { retryAsync } from './utils/asyncUtils.js';
53
+ export * from './utils/validationUtils.js';
54
+ export { logger, LogLevel, logAuth, logApi, logSession, logUser, logDevice, logPayment, logPerformance, } from './utils/loggerUtils.js';
53
55
  // Default export
54
- import { OxyServices } from './OxyServices';
56
+ import { OxyServices } from './OxyServices.js';
55
57
  export default OxyServices;
@@ -1,4 +1,4 @@
1
- import { CACHE_TIMES } from './mixinHelpers';
1
+ import { CACHE_TIMES } from './mixinHelpers.js';
2
2
  export function OxyServicesAnalyticsMixin(Base) {
3
3
  return class extends Base {
4
4
  constructor(...args) {