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