@oxyhq/core 1.2.2 → 1.2.4
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 +43 -6
- package/dist/cjs/HttpService.js +24 -1
- package/dist/cjs/mixins/OxyServices.utility.js +225 -51
- package/dist/esm/AuthManager.js +43 -6
- package/dist/esm/HttpService.js +24 -1
- package/dist/esm/mixins/OxyServices.utility.js +225 -51
- package/dist/types/HttpService.d.ts +4 -0
- package/dist/types/OxyServices.d.ts +9 -0
- package/dist/types/mixins/OxyServices.utility.d.ts +56 -21
- package/package.json +1 -1
- package/src/AuthManager.ts +47 -7
- package/src/HttpService.ts +31 -6
- package/src/OxyServices.ts +13 -0
- package/src/mixins/OxyServices.utility.ts +274 -82
package/dist/cjs/AuthManager.js
CHANGED
|
@@ -111,6 +111,10 @@ class AuthManager {
|
|
|
111
111
|
refreshBuffer: config.refreshBuffer ?? 5 * 60 * 1000, // 5 minutes
|
|
112
112
|
};
|
|
113
113
|
this.storage = this.config.storage;
|
|
114
|
+
// Persist tokens to storage when HttpService refreshes them automatically
|
|
115
|
+
this.oxyServices.httpService.onTokenRefreshed = (accessToken) => {
|
|
116
|
+
this.storage.setItem(STORAGE_KEYS.ACCESS_TOKEN, accessToken);
|
|
117
|
+
};
|
|
114
118
|
}
|
|
115
119
|
/**
|
|
116
120
|
* Get default storage based on environment.
|
|
@@ -221,20 +225,53 @@ class AuthManager {
|
|
|
221
225
|
}
|
|
222
226
|
}
|
|
223
227
|
async _doRefreshToken() {
|
|
224
|
-
|
|
225
|
-
|
|
228
|
+
// Get session info to find sessionId for token refresh
|
|
229
|
+
const sessionJson = await this.storage.getItem(STORAGE_KEYS.SESSION);
|
|
230
|
+
if (!sessionJson) {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
let sessionId;
|
|
234
|
+
try {
|
|
235
|
+
const session = JSON.parse(sessionJson);
|
|
236
|
+
sessionId = session.sessionId;
|
|
237
|
+
if (!sessionId)
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
catch (err) {
|
|
241
|
+
console.error('AuthManager: Failed to parse session from storage.', err);
|
|
226
242
|
return false;
|
|
227
243
|
}
|
|
228
244
|
try {
|
|
229
245
|
await (0, asyncUtils_1.retryAsync)(async () => {
|
|
230
246
|
const httpService = this.oxyServices.httpService;
|
|
247
|
+
// Use session-based token endpoint which handles auto-refresh server-side
|
|
231
248
|
const response = await httpService.request({
|
|
232
|
-
method: '
|
|
233
|
-
url:
|
|
234
|
-
data: { refreshToken },
|
|
249
|
+
method: 'GET',
|
|
250
|
+
url: `/api/session/token/${sessionId}`,
|
|
235
251
|
cache: false,
|
|
252
|
+
retry: false,
|
|
236
253
|
});
|
|
237
|
-
|
|
254
|
+
if (!response.accessToken) {
|
|
255
|
+
throw new Error('No access token in refresh response');
|
|
256
|
+
}
|
|
257
|
+
// Update access token in storage and HTTP client
|
|
258
|
+
await this.storage.setItem(STORAGE_KEYS.ACCESS_TOKEN, response.accessToken);
|
|
259
|
+
this.oxyServices.httpService.setTokens(response.accessToken);
|
|
260
|
+
// Update session expiry and schedule next refresh
|
|
261
|
+
if (response.expiresAt) {
|
|
262
|
+
try {
|
|
263
|
+
const session = JSON.parse(sessionJson);
|
|
264
|
+
session.expiresAt = response.expiresAt;
|
|
265
|
+
await this.storage.setItem(STORAGE_KEYS.SESSION, JSON.stringify(session));
|
|
266
|
+
}
|
|
267
|
+
catch (err) {
|
|
268
|
+
// Ignore parse errors for session update, but log for debugging.
|
|
269
|
+
console.error('AuthManager: Failed to re-save session after token refresh.', err);
|
|
270
|
+
}
|
|
271
|
+
if (this.config.autoRefresh) {
|
|
272
|
+
this.setupTokenRefresh(response.expiresAt);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
238
275
|
}, 2, // 2 retries = 3 total attempts
|
|
239
276
|
1000, // 1s base delay with exponential backoff + jitter
|
|
240
277
|
(error) => {
|
package/dist/cjs/HttpService.js
CHANGED
|
@@ -86,6 +86,7 @@ class TokenStore {
|
|
|
86
86
|
class HttpService {
|
|
87
87
|
constructor(config) {
|
|
88
88
|
this.tokenRefreshPromise = null;
|
|
89
|
+
this._onTokenRefreshed = null;
|
|
89
90
|
// Performance monitoring
|
|
90
91
|
this.requestMetrics = {
|
|
91
92
|
totalRequests: 0,
|
|
@@ -228,7 +229,25 @@ class HttpService {
|
|
|
228
229
|
clearTimeout(timeoutId);
|
|
229
230
|
// Handle response
|
|
230
231
|
if (!response.ok) {
|
|
231
|
-
|
|
232
|
+
// On 401, attempt token refresh and retry once before giving up
|
|
233
|
+
if (response.status === 401 && !config._isAuthRetry) {
|
|
234
|
+
const currentToken = this.tokenStore.getAccessToken();
|
|
235
|
+
if (currentToken) {
|
|
236
|
+
try {
|
|
237
|
+
const decoded = (0, jwt_decode_1.jwtDecode)(currentToken);
|
|
238
|
+
if (decoded.sessionId) {
|
|
239
|
+
const refreshResult = await this._refreshTokenFromSession(decoded.sessionId);
|
|
240
|
+
if (refreshResult) {
|
|
241
|
+
// Retry the request with the new token
|
|
242
|
+
return this.request({ ...config, _isAuthRetry: true, retry: false });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
// Token decode failed, fall through to clear
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// Refresh failed or no token — clear tokens
|
|
232
251
|
this.tokenStore.clearTokens();
|
|
233
252
|
}
|
|
234
253
|
// Try to parse error response (handle empty/malformed JSON)
|
|
@@ -481,6 +500,7 @@ class HttpService {
|
|
|
481
500
|
if (response.ok) {
|
|
482
501
|
const { accessToken: newToken } = await response.json();
|
|
483
502
|
this.tokenStore.setTokens(newToken);
|
|
503
|
+
this._onTokenRefreshed?.(newToken);
|
|
484
504
|
this.logger.debug('Token refreshed');
|
|
485
505
|
return `Bearer ${newToken}`;
|
|
486
506
|
}
|
|
@@ -587,6 +607,9 @@ class HttpService {
|
|
|
587
607
|
setTokens(accessToken, refreshToken = '') {
|
|
588
608
|
this.tokenStore.setTokens(accessToken, refreshToken);
|
|
589
609
|
}
|
|
610
|
+
set onTokenRefreshed(callback) {
|
|
611
|
+
this._onTokenRefreshed = callback;
|
|
612
|
+
}
|
|
590
613
|
clearTokens() {
|
|
591
614
|
this.tokenStore.clearTokens();
|
|
592
615
|
this.tokenStore.clearCsrfToken();
|
|
@@ -29,122 +29,215 @@ function OxyServicesUtilityMixin(Base) {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
/**
|
|
32
|
-
*
|
|
32
|
+
* Express.js authentication middleware
|
|
33
33
|
*
|
|
34
|
-
*
|
|
34
|
+
* Validates JWT tokens against the Oxy API and attaches user data to requests.
|
|
35
|
+
* Uses server-side session validation for security (not just JWT decode).
|
|
35
36
|
*
|
|
36
37
|
* @example
|
|
37
38
|
* ```typescript
|
|
38
|
-
*
|
|
39
|
-
* app.use('/api/protected', oxyServices.auth());
|
|
39
|
+
* import { OxyServices } from '@oxyhq/core';
|
|
40
40
|
*
|
|
41
|
-
*
|
|
42
|
-
* app.use('/api/protected', oxyServices.auth({ debug: true }));
|
|
41
|
+
* const oxy = new OxyServices({ baseURL: 'https://api.oxy.so' });
|
|
43
42
|
*
|
|
44
|
-
* //
|
|
45
|
-
* app.use('/api/protected',
|
|
46
|
-
* onError: (error) => console.error('Auth failed:', error)
|
|
47
|
-
* }));
|
|
43
|
+
* // Protect all routes under /api/protected
|
|
44
|
+
* app.use('/api/protected', oxy.auth());
|
|
48
45
|
*
|
|
49
|
-
* //
|
|
50
|
-
* app.
|
|
46
|
+
* // Access user in route handler
|
|
47
|
+
* app.get('/api/protected/me', (req, res) => {
|
|
48
|
+
* res.json({ userId: req.userId, user: req.user });
|
|
49
|
+
* });
|
|
50
|
+
*
|
|
51
|
+
* // Load full user profile from API
|
|
52
|
+
* app.use('/api/admin', oxy.auth({ loadUser: true }));
|
|
53
|
+
*
|
|
54
|
+
* // Optional auth - attach user if present, don't block if absent
|
|
55
|
+
* app.use('/api/public', oxy.auth({ optional: true }));
|
|
51
56
|
* ```
|
|
52
57
|
*
|
|
53
58
|
* @param options Optional configuration
|
|
54
|
-
* @param options.debug Enable debug logging (default: false)
|
|
55
|
-
* @param options.onError Custom error handler
|
|
56
|
-
* @param options.loadUser Load full user data (default: false for performance)
|
|
57
|
-
* @param options.session Use session-based validation (default: false)
|
|
58
59
|
* @returns Express middleware function
|
|
59
60
|
*/
|
|
60
61
|
auth(options = {}) {
|
|
61
|
-
const { debug = false, onError, loadUser = false,
|
|
62
|
-
//
|
|
63
|
-
|
|
62
|
+
const { debug = false, onError, loadUser = false, optional = false } = options;
|
|
63
|
+
// Cast to any for cross-mixin method access (Auth mixin methods available at runtime)
|
|
64
|
+
const oxyInstance = this;
|
|
65
|
+
// Return an async middleware function
|
|
66
|
+
return async (req, res, next) => {
|
|
64
67
|
try {
|
|
65
|
-
// Extract token from Authorization header
|
|
68
|
+
// Extract token from Authorization header or query params
|
|
66
69
|
const authHeader = req.headers['authorization'];
|
|
67
|
-
|
|
70
|
+
let token = authHeader?.startsWith('Bearer ') ? authHeader.substring(7) : null;
|
|
71
|
+
// Fallback to query params (useful for WebSocket upgrades)
|
|
72
|
+
if (!token) {
|
|
73
|
+
const q = req.query || {};
|
|
74
|
+
if (typeof q.token === 'string' && q.token)
|
|
75
|
+
token = q.token;
|
|
76
|
+
else if (typeof q.access_token === 'string' && q.access_token)
|
|
77
|
+
token = q.access_token;
|
|
78
|
+
}
|
|
68
79
|
if (debug) {
|
|
69
|
-
console.log(
|
|
70
|
-
console.log(`🔐 Auth: Token present: ${!!token}`);
|
|
80
|
+
console.log(`[oxy.auth] ${req.method} ${req.path} | token: ${!!token}`);
|
|
71
81
|
}
|
|
72
82
|
if (!token) {
|
|
83
|
+
if (optional) {
|
|
84
|
+
req.userId = null;
|
|
85
|
+
req.user = null;
|
|
86
|
+
return next();
|
|
87
|
+
}
|
|
73
88
|
const error = {
|
|
74
89
|
message: 'Access token required',
|
|
75
90
|
code: 'MISSING_TOKEN',
|
|
76
91
|
status: 401
|
|
77
92
|
};
|
|
78
|
-
if (debug)
|
|
79
|
-
console.log(`❌ Auth: Missing token`);
|
|
80
93
|
if (onError)
|
|
81
94
|
return onError(error);
|
|
82
95
|
return res.status(401).json(error);
|
|
83
96
|
}
|
|
84
|
-
// Decode
|
|
97
|
+
// Decode token to extract claims
|
|
85
98
|
let decoded;
|
|
86
99
|
try {
|
|
87
100
|
decoded = (0, jwt_decode_1.jwtDecode)(token);
|
|
88
|
-
if (debug) {
|
|
89
|
-
console.log(`🔐 Auth: Token decoded, User ID: ${decoded.userId || decoded.id}`);
|
|
90
|
-
}
|
|
91
101
|
}
|
|
92
102
|
catch (decodeError) {
|
|
103
|
+
if (optional) {
|
|
104
|
+
req.userId = null;
|
|
105
|
+
req.user = null;
|
|
106
|
+
return next();
|
|
107
|
+
}
|
|
93
108
|
const error = {
|
|
94
109
|
message: 'Invalid token format',
|
|
95
110
|
code: 'INVALID_TOKEN_FORMAT',
|
|
96
|
-
status:
|
|
111
|
+
status: 401
|
|
97
112
|
};
|
|
98
|
-
if (debug)
|
|
99
|
-
console.log(`❌ Auth: Token decode failed`);
|
|
100
113
|
if (onError)
|
|
101
114
|
return onError(error);
|
|
102
|
-
return res.status(
|
|
115
|
+
return res.status(401).json(error);
|
|
103
116
|
}
|
|
104
117
|
const userId = decoded.userId || decoded.id;
|
|
105
118
|
if (!userId) {
|
|
119
|
+
if (optional) {
|
|
120
|
+
req.userId = null;
|
|
121
|
+
req.user = null;
|
|
122
|
+
return next();
|
|
123
|
+
}
|
|
106
124
|
const error = {
|
|
107
125
|
message: 'Token missing user ID',
|
|
108
126
|
code: 'INVALID_TOKEN_PAYLOAD',
|
|
109
|
-
status:
|
|
127
|
+
status: 401
|
|
110
128
|
};
|
|
111
|
-
if (debug)
|
|
112
|
-
console.log(`❌ Auth: Token missing user ID`);
|
|
113
129
|
if (onError)
|
|
114
130
|
return onError(error);
|
|
115
|
-
return res.status(
|
|
131
|
+
return res.status(401).json(error);
|
|
116
132
|
}
|
|
117
|
-
// Check token expiration
|
|
133
|
+
// Check token expiration locally first (fast path)
|
|
118
134
|
if (decoded.exp && decoded.exp < Math.floor(Date.now() / 1000)) {
|
|
135
|
+
if (optional) {
|
|
136
|
+
req.userId = null;
|
|
137
|
+
req.user = null;
|
|
138
|
+
return next();
|
|
139
|
+
}
|
|
119
140
|
const error = {
|
|
120
141
|
message: 'Token expired',
|
|
121
142
|
code: 'TOKEN_EXPIRED',
|
|
122
|
-
status:
|
|
143
|
+
status: 401
|
|
123
144
|
};
|
|
124
|
-
if (debug)
|
|
125
|
-
console.log(`❌ Auth: Token expired`);
|
|
126
145
|
if (onError)
|
|
127
146
|
return onError(error);
|
|
128
|
-
return res.status(
|
|
147
|
+
return res.status(401).json(error);
|
|
148
|
+
}
|
|
149
|
+
// Validate token against the Oxy API for session-based verification
|
|
150
|
+
// This ensures the session hasn't been revoked server-side
|
|
151
|
+
if (decoded.sessionId) {
|
|
152
|
+
try {
|
|
153
|
+
const validationResult = await oxyInstance.validateSession(decoded.sessionId, {
|
|
154
|
+
useHeaderValidation: true,
|
|
155
|
+
});
|
|
156
|
+
if (!validationResult || !validationResult.valid) {
|
|
157
|
+
if (optional) {
|
|
158
|
+
req.userId = null;
|
|
159
|
+
req.user = null;
|
|
160
|
+
return next();
|
|
161
|
+
}
|
|
162
|
+
const error = {
|
|
163
|
+
message: 'Session invalid or expired',
|
|
164
|
+
code: 'INVALID_SESSION',
|
|
165
|
+
status: 401
|
|
166
|
+
};
|
|
167
|
+
if (onError)
|
|
168
|
+
return onError(error);
|
|
169
|
+
return res.status(401).json(error);
|
|
170
|
+
}
|
|
171
|
+
// Use validated user data from session validation (already has full user)
|
|
172
|
+
req.userId = userId;
|
|
173
|
+
req.accessToken = token;
|
|
174
|
+
req.sessionId = decoded.sessionId;
|
|
175
|
+
if (loadUser && validationResult.user) {
|
|
176
|
+
// Session validation already returns full user data
|
|
177
|
+
req.user = validationResult.user;
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
req.user = { id: userId };
|
|
181
|
+
}
|
|
182
|
+
if (debug) {
|
|
183
|
+
console.log(`[oxy.auth] OK user=${userId} session=${decoded.sessionId}`);
|
|
184
|
+
}
|
|
185
|
+
return next();
|
|
186
|
+
}
|
|
187
|
+
catch (validationError) {
|
|
188
|
+
if (debug) {
|
|
189
|
+
console.log(`[oxy.auth] Session validation failed:`, validationError);
|
|
190
|
+
}
|
|
191
|
+
if (optional) {
|
|
192
|
+
req.userId = null;
|
|
193
|
+
req.user = null;
|
|
194
|
+
return next();
|
|
195
|
+
}
|
|
196
|
+
const error = {
|
|
197
|
+
message: 'Session validation failed',
|
|
198
|
+
code: 'SESSION_VALIDATION_ERROR',
|
|
199
|
+
status: 401
|
|
200
|
+
};
|
|
201
|
+
if (onError)
|
|
202
|
+
return onError(error);
|
|
203
|
+
return res.status(401).json(error);
|
|
204
|
+
}
|
|
129
205
|
}
|
|
130
|
-
//
|
|
131
|
-
// Session validation can be added later if needed
|
|
132
|
-
// Set request properties immediately
|
|
206
|
+
// Non-session token: use local validation only (userId from JWT)
|
|
133
207
|
req.userId = userId;
|
|
134
208
|
req.accessToken = token;
|
|
135
209
|
req.user = { id: userId };
|
|
136
|
-
//
|
|
137
|
-
|
|
210
|
+
// If loadUser requested with non-session token, fetch from API
|
|
211
|
+
if (loadUser) {
|
|
212
|
+
try {
|
|
213
|
+
// Temporarily set token to make the API call
|
|
214
|
+
const prevToken = oxyInstance.getAccessToken();
|
|
215
|
+
oxyInstance.setTokens(token);
|
|
216
|
+
const fullUser = await oxyInstance.getCurrentUser();
|
|
217
|
+
// Restore previous token
|
|
218
|
+
if (prevToken) {
|
|
219
|
+
oxyInstance.setTokens(prevToken);
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
oxyInstance.clearTokens();
|
|
223
|
+
}
|
|
224
|
+
if (fullUser) {
|
|
225
|
+
req.user = fullUser;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
// Failed to load user, continue with basic user object
|
|
230
|
+
}
|
|
231
|
+
}
|
|
138
232
|
if (debug) {
|
|
139
|
-
console.log(
|
|
233
|
+
console.log(`[oxy.auth] OK user=${userId} (no session)`);
|
|
140
234
|
}
|
|
141
235
|
next();
|
|
142
236
|
}
|
|
143
237
|
catch (error) {
|
|
144
|
-
const apiError =
|
|
238
|
+
const apiError = oxyInstance.handleError(error);
|
|
145
239
|
if (debug) {
|
|
146
|
-
|
|
147
|
-
console.log(`❌ Auth: Unexpected error:`, apiError);
|
|
240
|
+
console.log(`[oxy.auth] Error:`, apiError);
|
|
148
241
|
}
|
|
149
242
|
if (onError)
|
|
150
243
|
return onError(apiError);
|
|
@@ -152,5 +245,86 @@ function OxyServicesUtilityMixin(Base) {
|
|
|
152
245
|
}
|
|
153
246
|
};
|
|
154
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* Socket.IO authentication middleware factory
|
|
250
|
+
*
|
|
251
|
+
* Returns a middleware function for Socket.IO that validates JWT tokens
|
|
252
|
+
* from the handshake auth object and attaches user data to the socket.
|
|
253
|
+
*
|
|
254
|
+
* @example
|
|
255
|
+
* ```typescript
|
|
256
|
+
* import { OxyServices } from '@oxyhq/core';
|
|
257
|
+
* import { Server } from 'socket.io';
|
|
258
|
+
*
|
|
259
|
+
* const oxy = new OxyServices({ baseURL: 'https://api.oxy.so' });
|
|
260
|
+
* const io = new Server(server);
|
|
261
|
+
*
|
|
262
|
+
* // Authenticate all socket connections
|
|
263
|
+
* io.use(oxy.authSocket());
|
|
264
|
+
*
|
|
265
|
+
* io.on('connection', (socket) => {
|
|
266
|
+
* console.log('Authenticated user:', socket.data.userId);
|
|
267
|
+
* });
|
|
268
|
+
* ```
|
|
269
|
+
*/
|
|
270
|
+
authSocket(options = {}) {
|
|
271
|
+
const { debug = false } = options;
|
|
272
|
+
// Cast to any for cross-mixin method access (Auth mixin methods available at runtime)
|
|
273
|
+
const oxyInstance = this;
|
|
274
|
+
return async (socket, next) => {
|
|
275
|
+
try {
|
|
276
|
+
const token = socket.handshake?.auth?.token;
|
|
277
|
+
if (!token) {
|
|
278
|
+
return next(new Error('Authentication required'));
|
|
279
|
+
}
|
|
280
|
+
let decoded;
|
|
281
|
+
try {
|
|
282
|
+
decoded = (0, jwt_decode_1.jwtDecode)(token);
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
return next(new Error('Invalid token'));
|
|
286
|
+
}
|
|
287
|
+
const userId = decoded.userId || decoded.id;
|
|
288
|
+
if (!userId) {
|
|
289
|
+
return next(new Error('Invalid token payload'));
|
|
290
|
+
}
|
|
291
|
+
// Check expiration
|
|
292
|
+
if (decoded.exp && decoded.exp < Math.floor(Date.now() / 1000)) {
|
|
293
|
+
return next(new Error('Token expired'));
|
|
294
|
+
}
|
|
295
|
+
// Validate session if available
|
|
296
|
+
if (decoded.sessionId) {
|
|
297
|
+
try {
|
|
298
|
+
const result = await oxyInstance.validateSession(decoded.sessionId, {
|
|
299
|
+
useHeaderValidation: true,
|
|
300
|
+
});
|
|
301
|
+
if (!result || !result.valid) {
|
|
302
|
+
return next(new Error('Session invalid'));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
return next(new Error('Session validation failed'));
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// Attach user data to socket
|
|
310
|
+
socket.data = socket.data || {};
|
|
311
|
+
socket.data.userId = userId;
|
|
312
|
+
socket.data.sessionId = decoded.sessionId || null;
|
|
313
|
+
socket.data.token = token;
|
|
314
|
+
// Also set on socket.user for backward compatibility
|
|
315
|
+
socket.user = { id: userId, userId, sessionId: decoded.sessionId };
|
|
316
|
+
if (debug) {
|
|
317
|
+
console.log(`[oxy.authSocket] OK user=${userId}`);
|
|
318
|
+
}
|
|
319
|
+
next();
|
|
320
|
+
}
|
|
321
|
+
catch (err) {
|
|
322
|
+
if (debug) {
|
|
323
|
+
console.log(`[oxy.authSocket] Error:`, err);
|
|
324
|
+
}
|
|
325
|
+
next(new Error('Authentication error'));
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
}
|
|
155
329
|
};
|
|
156
330
|
}
|
package/dist/esm/AuthManager.js
CHANGED
|
@@ -107,6 +107,10 @@ export class AuthManager {
|
|
|
107
107
|
refreshBuffer: config.refreshBuffer ?? 5 * 60 * 1000, // 5 minutes
|
|
108
108
|
};
|
|
109
109
|
this.storage = this.config.storage;
|
|
110
|
+
// Persist tokens to storage when HttpService refreshes them automatically
|
|
111
|
+
this.oxyServices.httpService.onTokenRefreshed = (accessToken) => {
|
|
112
|
+
this.storage.setItem(STORAGE_KEYS.ACCESS_TOKEN, accessToken);
|
|
113
|
+
};
|
|
110
114
|
}
|
|
111
115
|
/**
|
|
112
116
|
* Get default storage based on environment.
|
|
@@ -217,20 +221,53 @@ export class AuthManager {
|
|
|
217
221
|
}
|
|
218
222
|
}
|
|
219
223
|
async _doRefreshToken() {
|
|
220
|
-
|
|
221
|
-
|
|
224
|
+
// Get session info to find sessionId for token refresh
|
|
225
|
+
const sessionJson = await this.storage.getItem(STORAGE_KEYS.SESSION);
|
|
226
|
+
if (!sessionJson) {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
let sessionId;
|
|
230
|
+
try {
|
|
231
|
+
const session = JSON.parse(sessionJson);
|
|
232
|
+
sessionId = session.sessionId;
|
|
233
|
+
if (!sessionId)
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
console.error('AuthManager: Failed to parse session from storage.', err);
|
|
222
238
|
return false;
|
|
223
239
|
}
|
|
224
240
|
try {
|
|
225
241
|
await retryAsync(async () => {
|
|
226
242
|
const httpService = this.oxyServices.httpService;
|
|
243
|
+
// Use session-based token endpoint which handles auto-refresh server-side
|
|
227
244
|
const response = await httpService.request({
|
|
228
|
-
method: '
|
|
229
|
-
url:
|
|
230
|
-
data: { refreshToken },
|
|
245
|
+
method: 'GET',
|
|
246
|
+
url: `/api/session/token/${sessionId}`,
|
|
231
247
|
cache: false,
|
|
248
|
+
retry: false,
|
|
232
249
|
});
|
|
233
|
-
|
|
250
|
+
if (!response.accessToken) {
|
|
251
|
+
throw new Error('No access token in refresh response');
|
|
252
|
+
}
|
|
253
|
+
// Update access token in storage and HTTP client
|
|
254
|
+
await this.storage.setItem(STORAGE_KEYS.ACCESS_TOKEN, response.accessToken);
|
|
255
|
+
this.oxyServices.httpService.setTokens(response.accessToken);
|
|
256
|
+
// Update session expiry and schedule next refresh
|
|
257
|
+
if (response.expiresAt) {
|
|
258
|
+
try {
|
|
259
|
+
const session = JSON.parse(sessionJson);
|
|
260
|
+
session.expiresAt = response.expiresAt;
|
|
261
|
+
await this.storage.setItem(STORAGE_KEYS.SESSION, JSON.stringify(session));
|
|
262
|
+
}
|
|
263
|
+
catch (err) {
|
|
264
|
+
// Ignore parse errors for session update, but log for debugging.
|
|
265
|
+
console.error('AuthManager: Failed to re-save session after token refresh.', err);
|
|
266
|
+
}
|
|
267
|
+
if (this.config.autoRefresh) {
|
|
268
|
+
this.setupTokenRefresh(response.expiresAt);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
234
271
|
}, 2, // 2 retries = 3 total attempts
|
|
235
272
|
1000, // 1s base delay with exponential backoff + jitter
|
|
236
273
|
(error) => {
|
package/dist/esm/HttpService.js
CHANGED
|
@@ -83,6 +83,7 @@ class TokenStore {
|
|
|
83
83
|
export class HttpService {
|
|
84
84
|
constructor(config) {
|
|
85
85
|
this.tokenRefreshPromise = null;
|
|
86
|
+
this._onTokenRefreshed = null;
|
|
86
87
|
// Performance monitoring
|
|
87
88
|
this.requestMetrics = {
|
|
88
89
|
totalRequests: 0,
|
|
@@ -225,7 +226,25 @@ export class HttpService {
|
|
|
225
226
|
clearTimeout(timeoutId);
|
|
226
227
|
// Handle response
|
|
227
228
|
if (!response.ok) {
|
|
228
|
-
|
|
229
|
+
// On 401, attempt token refresh and retry once before giving up
|
|
230
|
+
if (response.status === 401 && !config._isAuthRetry) {
|
|
231
|
+
const currentToken = this.tokenStore.getAccessToken();
|
|
232
|
+
if (currentToken) {
|
|
233
|
+
try {
|
|
234
|
+
const decoded = jwtDecode(currentToken);
|
|
235
|
+
if (decoded.sessionId) {
|
|
236
|
+
const refreshResult = await this._refreshTokenFromSession(decoded.sessionId);
|
|
237
|
+
if (refreshResult) {
|
|
238
|
+
// Retry the request with the new token
|
|
239
|
+
return this.request({ ...config, _isAuthRetry: true, retry: false });
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
// Token decode failed, fall through to clear
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// Refresh failed or no token — clear tokens
|
|
229
248
|
this.tokenStore.clearTokens();
|
|
230
249
|
}
|
|
231
250
|
// Try to parse error response (handle empty/malformed JSON)
|
|
@@ -478,6 +497,7 @@ export class HttpService {
|
|
|
478
497
|
if (response.ok) {
|
|
479
498
|
const { accessToken: newToken } = await response.json();
|
|
480
499
|
this.tokenStore.setTokens(newToken);
|
|
500
|
+
this._onTokenRefreshed?.(newToken);
|
|
481
501
|
this.logger.debug('Token refreshed');
|
|
482
502
|
return `Bearer ${newToken}`;
|
|
483
503
|
}
|
|
@@ -584,6 +604,9 @@ export class HttpService {
|
|
|
584
604
|
setTokens(accessToken, refreshToken = '') {
|
|
585
605
|
this.tokenStore.setTokens(accessToken, refreshToken);
|
|
586
606
|
}
|
|
607
|
+
set onTokenRefreshed(callback) {
|
|
608
|
+
this._onTokenRefreshed = callback;
|
|
609
|
+
}
|
|
587
610
|
clearTokens() {
|
|
588
611
|
this.tokenStore.clearTokens();
|
|
589
612
|
this.tokenStore.clearCsrfToken();
|