@oxyhq/core 1.1.0 → 1.2.0
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 +16 -1
- package/dist/cjs/HttpService.js +31 -16
- package/dist/cjs/mixins/OxyServices.fedcm.js +8 -4
- package/dist/cjs/mixins/OxyServices.popup.js +16 -6
- package/dist/cjs/mixins/OxyServices.redirect.js +16 -6
- package/dist/cjs/utils/deviceManager.js +1 -5
- package/dist/esm/AuthManager.js +16 -1
- package/dist/esm/HttpService.js +31 -16
- package/dist/esm/mixins/OxyServices.fedcm.js +8 -4
- package/dist/esm/mixins/OxyServices.popup.js +16 -6
- package/dist/esm/mixins/OxyServices.redirect.js +16 -6
- package/dist/esm/utils/deviceManager.js +1 -5
- package/dist/types/AuthManager.d.ts +4 -1
- package/dist/types/HttpService.d.ts +2 -0
- package/package.json +1 -1
- package/src/AuthManager.ts +17 -1
- package/src/HttpService.ts +31 -18
- package/src/mixins/OxyServices.fedcm.ts +8 -4
- package/src/mixins/OxyServices.popup.ts +16 -6
- package/src/mixins/OxyServices.redirect.ts +16 -6
- package/src/utils/deviceManager.ts +1 -4
package/dist/cjs/AuthManager.js
CHANGED
|
@@ -103,6 +103,7 @@ class AuthManager {
|
|
|
103
103
|
this.listeners = new Set();
|
|
104
104
|
this.currentUser = null;
|
|
105
105
|
this.refreshTimer = null;
|
|
106
|
+
this.refreshPromise = null;
|
|
106
107
|
this.oxyServices = oxyServices;
|
|
107
108
|
this.config = {
|
|
108
109
|
storage: config.storage ?? this.getDefaultStorage(),
|
|
@@ -203,9 +204,23 @@ class AuthManager {
|
|
|
203
204
|
}
|
|
204
205
|
}
|
|
205
206
|
/**
|
|
206
|
-
* Refresh the access token.
|
|
207
|
+
* Refresh the access token. Deduplicates concurrent calls so only one
|
|
208
|
+
* refresh request is in-flight at a time.
|
|
207
209
|
*/
|
|
208
210
|
async refreshToken() {
|
|
211
|
+
// If a refresh is already in-flight, return the same promise
|
|
212
|
+
if (this.refreshPromise) {
|
|
213
|
+
return this.refreshPromise;
|
|
214
|
+
}
|
|
215
|
+
this.refreshPromise = this._doRefreshToken();
|
|
216
|
+
try {
|
|
217
|
+
return await this.refreshPromise;
|
|
218
|
+
}
|
|
219
|
+
finally {
|
|
220
|
+
this.refreshPromise = null;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
async _doRefreshToken() {
|
|
209
224
|
const refreshToken = await this.storage.getItem(STORAGE_KEYS.REFRESH_TOKEN);
|
|
210
225
|
if (!refreshToken) {
|
|
211
226
|
return false;
|
package/dist/cjs/HttpService.js
CHANGED
|
@@ -85,6 +85,7 @@ class TokenStore {
|
|
|
85
85
|
*/
|
|
86
86
|
class HttpService {
|
|
87
87
|
constructor(config) {
|
|
88
|
+
this.tokenRefreshPromise = null;
|
|
88
89
|
// Performance monitoring
|
|
89
90
|
this.requestMetrics = {
|
|
90
91
|
totalRequests: 0,
|
|
@@ -448,24 +449,17 @@ class HttpService {
|
|
|
448
449
|
const currentTime = Math.floor(Date.now() / 1000);
|
|
449
450
|
// If token expires in less than 60 seconds, refresh it
|
|
450
451
|
if (decoded.exp && decoded.exp - currentTime < 60 && decoded.sessionId) {
|
|
452
|
+
// Deduplicate concurrent refresh attempts
|
|
453
|
+
if (!this.tokenRefreshPromise) {
|
|
454
|
+
this.tokenRefreshPromise = this._refreshTokenFromSession(decoded.sessionId);
|
|
455
|
+
}
|
|
451
456
|
try {
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
method: 'GET',
|
|
456
|
-
headers: { 'Accept': 'application/json' },
|
|
457
|
-
signal: AbortSignal.timeout(5000),
|
|
458
|
-
credentials: 'include', // Include cookies for cross-origin requests
|
|
459
|
-
});
|
|
460
|
-
if (response.ok) {
|
|
461
|
-
const { accessToken: newToken } = await response.json();
|
|
462
|
-
this.tokenStore.setTokens(newToken);
|
|
463
|
-
this.logger.debug('Token refreshed');
|
|
464
|
-
return `Bearer ${newToken}`;
|
|
465
|
-
}
|
|
457
|
+
const result = await this.tokenRefreshPromise;
|
|
458
|
+
if (result)
|
|
459
|
+
return result;
|
|
466
460
|
}
|
|
467
|
-
|
|
468
|
-
this.
|
|
461
|
+
finally {
|
|
462
|
+
this.tokenRefreshPromise = null;
|
|
469
463
|
}
|
|
470
464
|
}
|
|
471
465
|
return `Bearer ${accessToken}`;
|
|
@@ -475,6 +469,27 @@ class HttpService {
|
|
|
475
469
|
return `Bearer ${accessToken}`;
|
|
476
470
|
}
|
|
477
471
|
}
|
|
472
|
+
async _refreshTokenFromSession(sessionId) {
|
|
473
|
+
try {
|
|
474
|
+
const refreshUrl = `${this.baseURL}/api/session/token/${sessionId}`;
|
|
475
|
+
const response = await fetch(refreshUrl, {
|
|
476
|
+
method: 'GET',
|
|
477
|
+
headers: { 'Accept': 'application/json' },
|
|
478
|
+
signal: AbortSignal.timeout(5000),
|
|
479
|
+
credentials: 'include',
|
|
480
|
+
});
|
|
481
|
+
if (response.ok) {
|
|
482
|
+
const { accessToken: newToken } = await response.json();
|
|
483
|
+
this.tokenStore.setTokens(newToken);
|
|
484
|
+
this.logger.debug('Token refreshed');
|
|
485
|
+
return `Bearer ${newToken}`;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
catch (refreshError) {
|
|
489
|
+
this.logger.warn('Token refresh failed, using current token');
|
|
490
|
+
}
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
478
493
|
/**
|
|
479
494
|
* Unwrap standardized API response format
|
|
480
495
|
*/
|
|
@@ -380,11 +380,15 @@ function OxyServicesFedCMMixin(Base) {
|
|
|
380
380
|
* @private
|
|
381
381
|
*/
|
|
382
382
|
generateNonce() {
|
|
383
|
-
if (typeof
|
|
384
|
-
return
|
|
383
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
384
|
+
return crypto.randomUUID();
|
|
385
385
|
}
|
|
386
|
-
|
|
387
|
-
|
|
386
|
+
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
387
|
+
const bytes = new Uint8Array(16);
|
|
388
|
+
crypto.getRandomValues(bytes);
|
|
389
|
+
return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
|
|
390
|
+
}
|
|
391
|
+
throw new Error('No secure random source available for nonce generation');
|
|
388
392
|
}
|
|
389
393
|
/**
|
|
390
394
|
* Get the client ID for this origin
|
|
@@ -323,10 +323,15 @@ function OxyServicesPopupAuthMixin(Base) {
|
|
|
323
323
|
* @private
|
|
324
324
|
*/
|
|
325
325
|
generateState() {
|
|
326
|
-
if (typeof
|
|
327
|
-
return
|
|
326
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
327
|
+
return crypto.randomUUID();
|
|
328
328
|
}
|
|
329
|
-
|
|
329
|
+
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
330
|
+
const bytes = new Uint8Array(16);
|
|
331
|
+
crypto.getRandomValues(bytes);
|
|
332
|
+
return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
|
|
333
|
+
}
|
|
334
|
+
throw new Error('No secure random source available for state generation');
|
|
330
335
|
}
|
|
331
336
|
/**
|
|
332
337
|
* Generate nonce for replay attack prevention
|
|
@@ -334,10 +339,15 @@ function OxyServicesPopupAuthMixin(Base) {
|
|
|
334
339
|
* @private
|
|
335
340
|
*/
|
|
336
341
|
generateNonce() {
|
|
337
|
-
if (typeof
|
|
338
|
-
return
|
|
342
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
343
|
+
return crypto.randomUUID();
|
|
344
|
+
}
|
|
345
|
+
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
346
|
+
const bytes = new Uint8Array(16);
|
|
347
|
+
crypto.getRandomValues(bytes);
|
|
348
|
+
return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
|
|
339
349
|
}
|
|
340
|
-
|
|
350
|
+
throw new Error('No secure random source available for nonce generation');
|
|
341
351
|
}
|
|
342
352
|
/**
|
|
343
353
|
* Store auth state in session storage
|
|
@@ -254,10 +254,15 @@ function OxyServicesRedirectAuthMixin(Base) {
|
|
|
254
254
|
* @private
|
|
255
255
|
*/
|
|
256
256
|
generateState() {
|
|
257
|
-
if (typeof
|
|
258
|
-
return
|
|
257
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
258
|
+
return crypto.randomUUID();
|
|
259
259
|
}
|
|
260
|
-
|
|
260
|
+
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
261
|
+
const bytes = new Uint8Array(16);
|
|
262
|
+
crypto.getRandomValues(bytes);
|
|
263
|
+
return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
|
|
264
|
+
}
|
|
265
|
+
throw new Error('No secure random source available for state generation');
|
|
261
266
|
}
|
|
262
267
|
/**
|
|
263
268
|
* Generate nonce for replay attack prevention
|
|
@@ -265,10 +270,15 @@ function OxyServicesRedirectAuthMixin(Base) {
|
|
|
265
270
|
* @private
|
|
266
271
|
*/
|
|
267
272
|
generateNonce() {
|
|
268
|
-
if (typeof
|
|
269
|
-
return
|
|
273
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
274
|
+
return crypto.randomUUID();
|
|
275
|
+
}
|
|
276
|
+
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
277
|
+
const bytes = new Uint8Array(16);
|
|
278
|
+
crypto.getRandomValues(bytes);
|
|
279
|
+
return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
|
|
270
280
|
}
|
|
271
|
-
|
|
281
|
+
throw new Error('No secure random source available for nonce generation');
|
|
272
282
|
}
|
|
273
283
|
/**
|
|
274
284
|
* Store auth state in session storage
|
|
@@ -171,16 +171,12 @@ class DeviceManager {
|
|
|
171
171
|
* Generate a unique device ID
|
|
172
172
|
*/
|
|
173
173
|
static generateDeviceId() {
|
|
174
|
-
// Use crypto.getRandomValues if available, otherwise fallback to Math.random
|
|
175
174
|
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
176
175
|
const array = new Uint8Array(32);
|
|
177
176
|
crypto.getRandomValues(array);
|
|
178
177
|
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
|
|
179
178
|
}
|
|
180
|
-
|
|
181
|
-
// Fallback for environments without crypto.getRandomValues
|
|
182
|
-
return 'device_' + Date.now().toString(36) + Math.random().toString(36).substr(2);
|
|
183
|
-
}
|
|
179
|
+
throw new Error('No secure random source available for device ID generation');
|
|
184
180
|
}
|
|
185
181
|
/**
|
|
186
182
|
* Get a user-friendly device name based on platform
|
package/dist/esm/AuthManager.js
CHANGED
|
@@ -99,6 +99,7 @@ export class AuthManager {
|
|
|
99
99
|
this.listeners = new Set();
|
|
100
100
|
this.currentUser = null;
|
|
101
101
|
this.refreshTimer = null;
|
|
102
|
+
this.refreshPromise = null;
|
|
102
103
|
this.oxyServices = oxyServices;
|
|
103
104
|
this.config = {
|
|
104
105
|
storage: config.storage ?? this.getDefaultStorage(),
|
|
@@ -199,9 +200,23 @@ export class AuthManager {
|
|
|
199
200
|
}
|
|
200
201
|
}
|
|
201
202
|
/**
|
|
202
|
-
* Refresh the access token.
|
|
203
|
+
* Refresh the access token. Deduplicates concurrent calls so only one
|
|
204
|
+
* refresh request is in-flight at a time.
|
|
203
205
|
*/
|
|
204
206
|
async refreshToken() {
|
|
207
|
+
// If a refresh is already in-flight, return the same promise
|
|
208
|
+
if (this.refreshPromise) {
|
|
209
|
+
return this.refreshPromise;
|
|
210
|
+
}
|
|
211
|
+
this.refreshPromise = this._doRefreshToken();
|
|
212
|
+
try {
|
|
213
|
+
return await this.refreshPromise;
|
|
214
|
+
}
|
|
215
|
+
finally {
|
|
216
|
+
this.refreshPromise = null;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
async _doRefreshToken() {
|
|
205
220
|
const refreshToken = await this.storage.getItem(STORAGE_KEYS.REFRESH_TOKEN);
|
|
206
221
|
if (!refreshToken) {
|
|
207
222
|
return false;
|
package/dist/esm/HttpService.js
CHANGED
|
@@ -82,6 +82,7 @@ class TokenStore {
|
|
|
82
82
|
*/
|
|
83
83
|
export class HttpService {
|
|
84
84
|
constructor(config) {
|
|
85
|
+
this.tokenRefreshPromise = null;
|
|
85
86
|
// Performance monitoring
|
|
86
87
|
this.requestMetrics = {
|
|
87
88
|
totalRequests: 0,
|
|
@@ -445,24 +446,17 @@ export class HttpService {
|
|
|
445
446
|
const currentTime = Math.floor(Date.now() / 1000);
|
|
446
447
|
// If token expires in less than 60 seconds, refresh it
|
|
447
448
|
if (decoded.exp && decoded.exp - currentTime < 60 && decoded.sessionId) {
|
|
449
|
+
// Deduplicate concurrent refresh attempts
|
|
450
|
+
if (!this.tokenRefreshPromise) {
|
|
451
|
+
this.tokenRefreshPromise = this._refreshTokenFromSession(decoded.sessionId);
|
|
452
|
+
}
|
|
448
453
|
try {
|
|
449
|
-
const
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
method: 'GET',
|
|
453
|
-
headers: { 'Accept': 'application/json' },
|
|
454
|
-
signal: AbortSignal.timeout(5000),
|
|
455
|
-
credentials: 'include', // Include cookies for cross-origin requests
|
|
456
|
-
});
|
|
457
|
-
if (response.ok) {
|
|
458
|
-
const { accessToken: newToken } = await response.json();
|
|
459
|
-
this.tokenStore.setTokens(newToken);
|
|
460
|
-
this.logger.debug('Token refreshed');
|
|
461
|
-
return `Bearer ${newToken}`;
|
|
462
|
-
}
|
|
454
|
+
const result = await this.tokenRefreshPromise;
|
|
455
|
+
if (result)
|
|
456
|
+
return result;
|
|
463
457
|
}
|
|
464
|
-
|
|
465
|
-
this.
|
|
458
|
+
finally {
|
|
459
|
+
this.tokenRefreshPromise = null;
|
|
466
460
|
}
|
|
467
461
|
}
|
|
468
462
|
return `Bearer ${accessToken}`;
|
|
@@ -472,6 +466,27 @@ export class HttpService {
|
|
|
472
466
|
return `Bearer ${accessToken}`;
|
|
473
467
|
}
|
|
474
468
|
}
|
|
469
|
+
async _refreshTokenFromSession(sessionId) {
|
|
470
|
+
try {
|
|
471
|
+
const refreshUrl = `${this.baseURL}/api/session/token/${sessionId}`;
|
|
472
|
+
const response = await fetch(refreshUrl, {
|
|
473
|
+
method: 'GET',
|
|
474
|
+
headers: { 'Accept': 'application/json' },
|
|
475
|
+
signal: AbortSignal.timeout(5000),
|
|
476
|
+
credentials: 'include',
|
|
477
|
+
});
|
|
478
|
+
if (response.ok) {
|
|
479
|
+
const { accessToken: newToken } = await response.json();
|
|
480
|
+
this.tokenStore.setTokens(newToken);
|
|
481
|
+
this.logger.debug('Token refreshed');
|
|
482
|
+
return `Bearer ${newToken}`;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
catch (refreshError) {
|
|
486
|
+
this.logger.warn('Token refresh failed, using current token');
|
|
487
|
+
}
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
475
490
|
/**
|
|
476
491
|
* Unwrap standardized API response format
|
|
477
492
|
*/
|
|
@@ -376,11 +376,15 @@ export function OxyServicesFedCMMixin(Base) {
|
|
|
376
376
|
* @private
|
|
377
377
|
*/
|
|
378
378
|
generateNonce() {
|
|
379
|
-
if (typeof
|
|
380
|
-
return
|
|
379
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
380
|
+
return crypto.randomUUID();
|
|
381
381
|
}
|
|
382
|
-
|
|
383
|
-
|
|
382
|
+
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
383
|
+
const bytes = new Uint8Array(16);
|
|
384
|
+
crypto.getRandomValues(bytes);
|
|
385
|
+
return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
|
|
386
|
+
}
|
|
387
|
+
throw new Error('No secure random source available for nonce generation');
|
|
384
388
|
}
|
|
385
389
|
/**
|
|
386
390
|
* Get the client ID for this origin
|
|
@@ -319,10 +319,15 @@ export function OxyServicesPopupAuthMixin(Base) {
|
|
|
319
319
|
* @private
|
|
320
320
|
*/
|
|
321
321
|
generateState() {
|
|
322
|
-
if (typeof
|
|
323
|
-
return
|
|
322
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
323
|
+
return crypto.randomUUID();
|
|
324
324
|
}
|
|
325
|
-
|
|
325
|
+
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
326
|
+
const bytes = new Uint8Array(16);
|
|
327
|
+
crypto.getRandomValues(bytes);
|
|
328
|
+
return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
|
|
329
|
+
}
|
|
330
|
+
throw new Error('No secure random source available for state generation');
|
|
326
331
|
}
|
|
327
332
|
/**
|
|
328
333
|
* Generate nonce for replay attack prevention
|
|
@@ -330,10 +335,15 @@ export function OxyServicesPopupAuthMixin(Base) {
|
|
|
330
335
|
* @private
|
|
331
336
|
*/
|
|
332
337
|
generateNonce() {
|
|
333
|
-
if (typeof
|
|
334
|
-
return
|
|
338
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
339
|
+
return crypto.randomUUID();
|
|
340
|
+
}
|
|
341
|
+
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
342
|
+
const bytes = new Uint8Array(16);
|
|
343
|
+
crypto.getRandomValues(bytes);
|
|
344
|
+
return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
|
|
335
345
|
}
|
|
336
|
-
|
|
346
|
+
throw new Error('No secure random source available for nonce generation');
|
|
337
347
|
}
|
|
338
348
|
/**
|
|
339
349
|
* Store auth state in session storage
|
|
@@ -250,10 +250,15 @@ export function OxyServicesRedirectAuthMixin(Base) {
|
|
|
250
250
|
* @private
|
|
251
251
|
*/
|
|
252
252
|
generateState() {
|
|
253
|
-
if (typeof
|
|
254
|
-
return
|
|
253
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
254
|
+
return crypto.randomUUID();
|
|
255
255
|
}
|
|
256
|
-
|
|
256
|
+
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
257
|
+
const bytes = new Uint8Array(16);
|
|
258
|
+
crypto.getRandomValues(bytes);
|
|
259
|
+
return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
|
|
260
|
+
}
|
|
261
|
+
throw new Error('No secure random source available for state generation');
|
|
257
262
|
}
|
|
258
263
|
/**
|
|
259
264
|
* Generate nonce for replay attack prevention
|
|
@@ -261,10 +266,15 @@ export function OxyServicesRedirectAuthMixin(Base) {
|
|
|
261
266
|
* @private
|
|
262
267
|
*/
|
|
263
268
|
generateNonce() {
|
|
264
|
-
if (typeof
|
|
265
|
-
return
|
|
269
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
270
|
+
return crypto.randomUUID();
|
|
271
|
+
}
|
|
272
|
+
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
273
|
+
const bytes = new Uint8Array(16);
|
|
274
|
+
crypto.getRandomValues(bytes);
|
|
275
|
+
return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
|
|
266
276
|
}
|
|
267
|
-
|
|
277
|
+
throw new Error('No secure random source available for nonce generation');
|
|
268
278
|
}
|
|
269
279
|
/**
|
|
270
280
|
* Store auth state in session storage
|
|
@@ -135,16 +135,12 @@ export class DeviceManager {
|
|
|
135
135
|
* Generate a unique device ID
|
|
136
136
|
*/
|
|
137
137
|
static generateDeviceId() {
|
|
138
|
-
// Use crypto.getRandomValues if available, otherwise fallback to Math.random
|
|
139
138
|
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
140
139
|
const array = new Uint8Array(32);
|
|
141
140
|
crypto.getRandomValues(array);
|
|
142
141
|
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
|
|
143
142
|
}
|
|
144
|
-
|
|
145
|
-
// Fallback for environments without crypto.getRandomValues
|
|
146
|
-
return 'device_' + Date.now().toString(36) + Math.random().toString(36).substr(2);
|
|
147
|
-
}
|
|
143
|
+
throw new Error('No secure random source available for device ID generation');
|
|
148
144
|
}
|
|
149
145
|
/**
|
|
150
146
|
* Get a user-friendly device name based on platform
|
|
@@ -66,6 +66,7 @@ export declare class AuthManager {
|
|
|
66
66
|
private listeners;
|
|
67
67
|
private currentUser;
|
|
68
68
|
private refreshTimer;
|
|
69
|
+
private refreshPromise;
|
|
69
70
|
private config;
|
|
70
71
|
constructor(oxyServices: OxyServices, config?: AuthManagerConfig);
|
|
71
72
|
/**
|
|
@@ -95,9 +96,11 @@ export declare class AuthManager {
|
|
|
95
96
|
*/
|
|
96
97
|
private setupTokenRefresh;
|
|
97
98
|
/**
|
|
98
|
-
* Refresh the access token.
|
|
99
|
+
* Refresh the access token. Deduplicates concurrent calls so only one
|
|
100
|
+
* refresh request is in-flight at a time.
|
|
99
101
|
*/
|
|
100
102
|
refreshToken(): Promise<boolean>;
|
|
103
|
+
private _doRefreshToken;
|
|
101
104
|
/**
|
|
102
105
|
* Sign out and clear all auth data.
|
|
103
106
|
*/
|
|
@@ -43,6 +43,7 @@ export declare class HttpService {
|
|
|
43
43
|
private requestQueue;
|
|
44
44
|
private logger;
|
|
45
45
|
private config;
|
|
46
|
+
private tokenRefreshPromise;
|
|
46
47
|
private requestMetrics;
|
|
47
48
|
constructor(config: OxyConfig);
|
|
48
49
|
/**
|
|
@@ -72,6 +73,7 @@ export declare class HttpService {
|
|
|
72
73
|
* Get auth header with automatic token refresh
|
|
73
74
|
*/
|
|
74
75
|
private getAuthHeader;
|
|
76
|
+
private _refreshTokenFromSession;
|
|
75
77
|
/**
|
|
76
78
|
* Unwrap standardized API response format
|
|
77
79
|
*/
|
package/package.json
CHANGED
package/src/AuthManager.ts
CHANGED
|
@@ -142,6 +142,7 @@ export class AuthManager {
|
|
|
142
142
|
private listeners: Set<AuthStateChangeCallback> = new Set();
|
|
143
143
|
private currentUser: MinimalUserData | null = null;
|
|
144
144
|
private refreshTimer: ReturnType<typeof setTimeout> | null = null;
|
|
145
|
+
private refreshPromise: Promise<boolean> | null = null;
|
|
145
146
|
private config: Required<AuthManagerConfig>;
|
|
146
147
|
|
|
147
148
|
constructor(oxyServices: OxyServices, config: AuthManagerConfig = {}) {
|
|
@@ -261,9 +262,24 @@ export class AuthManager {
|
|
|
261
262
|
}
|
|
262
263
|
|
|
263
264
|
/**
|
|
264
|
-
* Refresh the access token.
|
|
265
|
+
* Refresh the access token. Deduplicates concurrent calls so only one
|
|
266
|
+
* refresh request is in-flight at a time.
|
|
265
267
|
*/
|
|
266
268
|
async refreshToken(): Promise<boolean> {
|
|
269
|
+
// If a refresh is already in-flight, return the same promise
|
|
270
|
+
if (this.refreshPromise) {
|
|
271
|
+
return this.refreshPromise;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
this.refreshPromise = this._doRefreshToken();
|
|
275
|
+
try {
|
|
276
|
+
return await this.refreshPromise;
|
|
277
|
+
} finally {
|
|
278
|
+
this.refreshPromise = null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private async _doRefreshToken(): Promise<boolean> {
|
|
267
283
|
const refreshToken = await this.storage.getItem(STORAGE_KEYS.REFRESH_TOKEN);
|
|
268
284
|
if (!refreshToken) {
|
|
269
285
|
return false;
|
package/src/HttpService.ts
CHANGED
|
@@ -131,6 +131,7 @@ export class HttpService {
|
|
|
131
131
|
private requestQueue: RequestQueue;
|
|
132
132
|
private logger: SimpleLogger;
|
|
133
133
|
private config: OxyConfig;
|
|
134
|
+
private tokenRefreshPromise: Promise<string | null> | null = null;
|
|
134
135
|
|
|
135
136
|
// Performance monitoring
|
|
136
137
|
private requestMetrics = {
|
|
@@ -563,25 +564,15 @@ export class HttpService {
|
|
|
563
564
|
|
|
564
565
|
// If token expires in less than 60 seconds, refresh it
|
|
565
566
|
if (decoded.exp && decoded.exp - currentTime < 60 && decoded.sessionId) {
|
|
567
|
+
// Deduplicate concurrent refresh attempts
|
|
568
|
+
if (!this.tokenRefreshPromise) {
|
|
569
|
+
this.tokenRefreshPromise = this._refreshTokenFromSession(decoded.sessionId);
|
|
570
|
+
}
|
|
566
571
|
try {
|
|
567
|
-
const
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
method: 'GET',
|
|
572
|
-
headers: { 'Accept': 'application/json' },
|
|
573
|
-
signal: AbortSignal.timeout(5000),
|
|
574
|
-
credentials: 'include', // Include cookies for cross-origin requests
|
|
575
|
-
});
|
|
576
|
-
|
|
577
|
-
if (response.ok) {
|
|
578
|
-
const { accessToken: newToken } = await response.json();
|
|
579
|
-
this.tokenStore.setTokens(newToken);
|
|
580
|
-
this.logger.debug('Token refreshed');
|
|
581
|
-
return `Bearer ${newToken}`;
|
|
582
|
-
}
|
|
583
|
-
} catch (refreshError) {
|
|
584
|
-
this.logger.warn('Token refresh failed, using current token');
|
|
572
|
+
const result = await this.tokenRefreshPromise;
|
|
573
|
+
if (result) return result;
|
|
574
|
+
} finally {
|
|
575
|
+
this.tokenRefreshPromise = null;
|
|
585
576
|
}
|
|
586
577
|
}
|
|
587
578
|
|
|
@@ -592,6 +583,28 @@ export class HttpService {
|
|
|
592
583
|
}
|
|
593
584
|
}
|
|
594
585
|
|
|
586
|
+
private async _refreshTokenFromSession(sessionId: string): Promise<string | null> {
|
|
587
|
+
try {
|
|
588
|
+
const refreshUrl = `${this.baseURL}/api/session/token/${sessionId}`;
|
|
589
|
+
const response = await fetch(refreshUrl, {
|
|
590
|
+
method: 'GET',
|
|
591
|
+
headers: { 'Accept': 'application/json' },
|
|
592
|
+
signal: AbortSignal.timeout(5000),
|
|
593
|
+
credentials: 'include',
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
if (response.ok) {
|
|
597
|
+
const { accessToken: newToken } = await response.json();
|
|
598
|
+
this.tokenStore.setTokens(newToken);
|
|
599
|
+
this.logger.debug('Token refreshed');
|
|
600
|
+
return `Bearer ${newToken}`;
|
|
601
|
+
}
|
|
602
|
+
} catch (refreshError) {
|
|
603
|
+
this.logger.warn('Token refresh failed, using current token');
|
|
604
|
+
}
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
|
|
595
608
|
/**
|
|
596
609
|
* Unwrap standardized API response format
|
|
597
610
|
*/
|
|
@@ -439,11 +439,15 @@ export function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
439
439
|
* @private
|
|
440
440
|
*/
|
|
441
441
|
public generateNonce(): string {
|
|
442
|
-
if (typeof
|
|
443
|
-
return
|
|
442
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
443
|
+
return crypto.randomUUID();
|
|
444
444
|
}
|
|
445
|
-
|
|
446
|
-
|
|
445
|
+
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
446
|
+
const bytes = new Uint8Array(16);
|
|
447
|
+
crypto.getRandomValues(bytes);
|
|
448
|
+
return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
|
|
449
|
+
}
|
|
450
|
+
throw new Error('No secure random source available for nonce generation');
|
|
447
451
|
}
|
|
448
452
|
|
|
449
453
|
/**
|
|
@@ -397,10 +397,15 @@ export function OxyServicesPopupAuthMixin<T extends typeof OxyServicesBase>(Base
|
|
|
397
397
|
* @private
|
|
398
398
|
*/
|
|
399
399
|
public generateState(): string {
|
|
400
|
-
if (typeof
|
|
401
|
-
return
|
|
400
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
401
|
+
return crypto.randomUUID();
|
|
402
402
|
}
|
|
403
|
-
|
|
403
|
+
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
404
|
+
const bytes = new Uint8Array(16);
|
|
405
|
+
crypto.getRandomValues(bytes);
|
|
406
|
+
return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
|
|
407
|
+
}
|
|
408
|
+
throw new Error('No secure random source available for state generation');
|
|
404
409
|
}
|
|
405
410
|
|
|
406
411
|
/**
|
|
@@ -409,10 +414,15 @@ export function OxyServicesPopupAuthMixin<T extends typeof OxyServicesBase>(Base
|
|
|
409
414
|
* @private
|
|
410
415
|
*/
|
|
411
416
|
public generateNonce(): string {
|
|
412
|
-
if (typeof
|
|
413
|
-
return
|
|
417
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
418
|
+
return crypto.randomUUID();
|
|
419
|
+
}
|
|
420
|
+
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
421
|
+
const bytes = new Uint8Array(16);
|
|
422
|
+
crypto.getRandomValues(bytes);
|
|
423
|
+
return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
|
|
414
424
|
}
|
|
415
|
-
|
|
425
|
+
throw new Error('No secure random source available for nonce generation');
|
|
416
426
|
}
|
|
417
427
|
|
|
418
428
|
/**
|
|
@@ -299,10 +299,15 @@ export function OxyServicesRedirectAuthMixin<T extends typeof OxyServicesBase>(B
|
|
|
299
299
|
* @private
|
|
300
300
|
*/
|
|
301
301
|
public generateState(): string {
|
|
302
|
-
if (typeof
|
|
303
|
-
return
|
|
302
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
303
|
+
return crypto.randomUUID();
|
|
304
304
|
}
|
|
305
|
-
|
|
305
|
+
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
306
|
+
const bytes = new Uint8Array(16);
|
|
307
|
+
crypto.getRandomValues(bytes);
|
|
308
|
+
return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
|
|
309
|
+
}
|
|
310
|
+
throw new Error('No secure random source available for state generation');
|
|
306
311
|
}
|
|
307
312
|
|
|
308
313
|
/**
|
|
@@ -311,10 +316,15 @@ export function OxyServicesRedirectAuthMixin<T extends typeof OxyServicesBase>(B
|
|
|
311
316
|
* @private
|
|
312
317
|
*/
|
|
313
318
|
public generateNonce(): string {
|
|
314
|
-
if (typeof
|
|
315
|
-
return
|
|
319
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
320
|
+
return crypto.randomUUID();
|
|
321
|
+
}
|
|
322
|
+
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
323
|
+
const bytes = new Uint8Array(16);
|
|
324
|
+
crypto.getRandomValues(bytes);
|
|
325
|
+
return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
|
|
316
326
|
}
|
|
317
|
-
|
|
327
|
+
throw new Error('No secure random source available for nonce generation');
|
|
318
328
|
}
|
|
319
329
|
|
|
320
330
|
/**
|
|
@@ -170,15 +170,12 @@ export class DeviceManager {
|
|
|
170
170
|
* Generate a unique device ID
|
|
171
171
|
*/
|
|
172
172
|
private static generateDeviceId(): string {
|
|
173
|
-
// Use crypto.getRandomValues if available, otherwise fallback to Math.random
|
|
174
173
|
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
175
174
|
const array = new Uint8Array(32);
|
|
176
175
|
crypto.getRandomValues(array);
|
|
177
176
|
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
|
|
178
|
-
} else {
|
|
179
|
-
// Fallback for environments without crypto.getRandomValues
|
|
180
|
-
return 'device_' + Date.now().toString(36) + Math.random().toString(36).substr(2);
|
|
181
177
|
}
|
|
178
|
+
throw new Error('No secure random source available for device ID generation');
|
|
182
179
|
}
|
|
183
180
|
|
|
184
181
|
/**
|