@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.
- package/dist/cjs/AuthManager.js +15 -0
- package/dist/cjs/index.js +9 -1
- package/dist/cjs/mixins/OxyServices.fedcm.js +39 -3
- package/dist/cjs/mixins/OxyServices.utility.js +24 -4
- package/dist/cjs/utils/authHelpers.js +114 -0
- package/dist/esm/AuthManager.js +16 -1
- package/dist/esm/HttpService.js +6 -6
- package/dist/esm/OxyServices.base.js +3 -3
- package/dist/esm/OxyServices.js +2 -2
- package/dist/esm/crypto/index.js +5 -5
- package/dist/esm/crypto/keyManager.js +3 -3
- package/dist/esm/crypto/recoveryPhrase.js +1 -1
- package/dist/esm/crypto/signatureService.js +2 -2
- package/dist/esm/i18n/index.js +11 -11
- package/dist/esm/index.js +27 -25
- package/dist/esm/mixins/OxyServices.analytics.js +1 -1
- package/dist/esm/mixins/OxyServices.auth.js +1 -1
- package/dist/esm/mixins/OxyServices.developer.js +1 -1
- package/dist/esm/mixins/OxyServices.features.js +1 -1
- package/dist/esm/mixins/OxyServices.fedcm.js +41 -5
- package/dist/esm/mixins/OxyServices.karma.js +1 -1
- package/dist/esm/mixins/OxyServices.language.js +2 -2
- package/dist/esm/mixins/OxyServices.payment.js +1 -1
- package/dist/esm/mixins/OxyServices.popup.js +2 -2
- package/dist/esm/mixins/OxyServices.privacy.js +1 -1
- package/dist/esm/mixins/OxyServices.redirect.js +1 -1
- package/dist/esm/mixins/OxyServices.security.js +1 -1
- package/dist/esm/mixins/OxyServices.user.js +1 -1
- package/dist/esm/mixins/OxyServices.utility.js +25 -5
- package/dist/esm/mixins/index.js +18 -18
- package/dist/esm/shared/index.js +5 -5
- package/dist/esm/shared/utils/index.js +4 -4
- package/dist/esm/utils/asyncUtils.js +1 -1
- package/dist/esm/utils/authHelpers.js +105 -0
- package/dist/esm/utils/errorUtils.js +1 -1
- package/dist/esm/utils/index.js +4 -4
- package/dist/types/index.d.ts +2 -0
- package/dist/types/mixins/OxyServices.fedcm.d.ts +6 -0
- package/dist/types/mixins/OxyServices.utility.d.ts +13 -0
- package/dist/types/utils/authHelpers.d.ts +57 -0
- package/package.json +2 -2
- package/src/AuthManager.ts +15 -0
- package/src/index.ts +11 -0
- package/src/mixins/OxyServices.fedcm.ts +44 -3
- package/src/mixins/OxyServices.utility.ts +24 -4
- package/src/utils/authHelpers.ts +140 -0
|
@@ -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
|
-
|
|
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.
|
|
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,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,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
|
|
@@ -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';
|
package/dist/esm/mixins/index.js
CHANGED
|
@@ -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
|
*
|
package/dist/esm/shared/index.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/esm/utils/index.js
CHANGED
|
@@ -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';
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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';
|