@oxyhq/core 1.2.3 → 1.2.5
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 +4 -0
- package/dist/cjs/HttpService.js +24 -1
- package/dist/cjs/mixins/OxyServices.fedcm.js +19 -1
- package/dist/esm/AuthManager.js +4 -0
- package/dist/esm/HttpService.js +24 -1
- package/dist/esm/mixins/OxyServices.fedcm.js +19 -1
- package/dist/types/HttpService.d.ts +4 -0
- package/package.json +1 -1
- package/src/AuthManager.ts +5 -0
- package/src/HttpService.ts +31 -6
- package/src/mixins/OxyServices.fedcm.ts +20 -1
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.
|
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();
|
|
@@ -105,12 +105,20 @@ function OxyServicesFedCMMixin(Base) {
|
|
|
105
105
|
}
|
|
106
106
|
catch (error) {
|
|
107
107
|
debug.log('Interactive sign-in failed:', error);
|
|
108
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
108
109
|
if (error.name === 'AbortError') {
|
|
109
110
|
throw new OxyServices_errors_1.OxyAuthenticationError('Sign-in was cancelled by user');
|
|
110
111
|
}
|
|
111
112
|
if (error.name === 'NetworkError') {
|
|
112
113
|
throw new OxyServices_errors_1.OxyAuthenticationError('Network error during sign-in. Please check your connection.');
|
|
113
114
|
}
|
|
115
|
+
if (errorMessage.includes('multiple accounts')) {
|
|
116
|
+
throw new OxyServices_errors_1.OxyAuthenticationError('Please sign out and sign in again to use FedCM with a single account');
|
|
117
|
+
}
|
|
118
|
+
if (errorMessage.includes('retrieving a token') || errorMessage.includes('Error retrieving')) {
|
|
119
|
+
debug.error('FedCM token retrieval error - this may be a browser or IdP configuration issue');
|
|
120
|
+
throw new OxyServices_errors_1.OxyAuthenticationError('Authentication failed. Please try again or use an alternative sign-in method.');
|
|
121
|
+
}
|
|
114
122
|
throw error;
|
|
115
123
|
}
|
|
116
124
|
}
|
|
@@ -170,7 +178,17 @@ function OxyServicesFedCMMixin(Base) {
|
|
|
170
178
|
catch (silentError) {
|
|
171
179
|
const errorName = silentError instanceof Error ? silentError.name : 'Unknown';
|
|
172
180
|
const errorMessage = silentError instanceof Error ? silentError.message : String(silentError);
|
|
173
|
-
|
|
181
|
+
// Handle specific FedCM errors with better logging
|
|
182
|
+
if (errorMessage.includes('multiple accounts')) {
|
|
183
|
+
debug.log('Silent SSO: User has used multiple accounts - silent mediation not available');
|
|
184
|
+
debug.log('Silent SSO: User needs to explicitly sign in to choose account');
|
|
185
|
+
}
|
|
186
|
+
else if (errorMessage.includes('conditions')) {
|
|
187
|
+
debug.log('Silent SSO: Conditions not met (user may not be logged in at IdP or not in approved_clients)');
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
debug.log('Silent SSO: Silent mediation failed:', { name: errorName, message: errorMessage });
|
|
191
|
+
}
|
|
174
192
|
return null;
|
|
175
193
|
}
|
|
176
194
|
if (!credential || !credential.token) {
|
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.
|
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();
|
|
@@ -101,12 +101,20 @@ export function OxyServicesFedCMMixin(Base) {
|
|
|
101
101
|
}
|
|
102
102
|
catch (error) {
|
|
103
103
|
debug.log('Interactive sign-in failed:', error);
|
|
104
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
104
105
|
if (error.name === 'AbortError') {
|
|
105
106
|
throw new OxyAuthenticationError('Sign-in was cancelled by user');
|
|
106
107
|
}
|
|
107
108
|
if (error.name === 'NetworkError') {
|
|
108
109
|
throw new OxyAuthenticationError('Network error during sign-in. Please check your connection.');
|
|
109
110
|
}
|
|
111
|
+
if (errorMessage.includes('multiple accounts')) {
|
|
112
|
+
throw new OxyAuthenticationError('Please sign out and sign in again to use FedCM with a single account');
|
|
113
|
+
}
|
|
114
|
+
if (errorMessage.includes('retrieving a token') || errorMessage.includes('Error retrieving')) {
|
|
115
|
+
debug.error('FedCM token retrieval error - this may be a browser or IdP configuration issue');
|
|
116
|
+
throw new OxyAuthenticationError('Authentication failed. Please try again or use an alternative sign-in method.');
|
|
117
|
+
}
|
|
110
118
|
throw error;
|
|
111
119
|
}
|
|
112
120
|
}
|
|
@@ -166,7 +174,17 @@ export function OxyServicesFedCMMixin(Base) {
|
|
|
166
174
|
catch (silentError) {
|
|
167
175
|
const errorName = silentError instanceof Error ? silentError.name : 'Unknown';
|
|
168
176
|
const errorMessage = silentError instanceof Error ? silentError.message : String(silentError);
|
|
169
|
-
|
|
177
|
+
// Handle specific FedCM errors with better logging
|
|
178
|
+
if (errorMessage.includes('multiple accounts')) {
|
|
179
|
+
debug.log('Silent SSO: User has used multiple accounts - silent mediation not available');
|
|
180
|
+
debug.log('Silent SSO: User needs to explicitly sign in to choose account');
|
|
181
|
+
}
|
|
182
|
+
else if (errorMessage.includes('conditions')) {
|
|
183
|
+
debug.log('Silent SSO: Conditions not met (user may not be logged in at IdP or not in approved_clients)');
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
debug.log('Silent SSO: Silent mediation failed:', { name: errorName, message: errorMessage });
|
|
187
|
+
}
|
|
170
188
|
return null;
|
|
171
189
|
}
|
|
172
190
|
if (!credential || !credential.token) {
|
|
@@ -28,6 +28,8 @@ interface RequestConfig extends RequestOptions {
|
|
|
28
28
|
url: string;
|
|
29
29
|
data?: unknown;
|
|
30
30
|
params?: Record<string, unknown>;
|
|
31
|
+
/** @internal Used to prevent infinite auth retry loops */
|
|
32
|
+
_isAuthRetry?: boolean;
|
|
31
33
|
}
|
|
32
34
|
/**
|
|
33
35
|
* Unified HTTP Service
|
|
@@ -44,6 +46,7 @@ export declare class HttpService {
|
|
|
44
46
|
private logger;
|
|
45
47
|
private config;
|
|
46
48
|
private tokenRefreshPromise;
|
|
49
|
+
private _onTokenRefreshed;
|
|
47
50
|
private requestMetrics;
|
|
48
51
|
constructor(config: OxyConfig);
|
|
49
52
|
/**
|
|
@@ -140,6 +143,7 @@ export declare class HttpService {
|
|
|
140
143
|
data: T;
|
|
141
144
|
}>;
|
|
142
145
|
setTokens(accessToken: string, refreshToken?: string): void;
|
|
146
|
+
set onTokenRefreshed(callback: ((accessToken: string) => void) | null);
|
|
143
147
|
clearTokens(): void;
|
|
144
148
|
getAccessToken(): string | null;
|
|
145
149
|
hasAccessToken(): boolean;
|
package/package.json
CHANGED
package/src/AuthManager.ts
CHANGED
|
@@ -153,6 +153,11 @@ export class AuthManager {
|
|
|
153
153
|
refreshBuffer: config.refreshBuffer ?? 5 * 60 * 1000, // 5 minutes
|
|
154
154
|
};
|
|
155
155
|
this.storage = this.config.storage;
|
|
156
|
+
|
|
157
|
+
// Persist tokens to storage when HttpService refreshes them automatically
|
|
158
|
+
this.oxyServices.httpService.onTokenRefreshed = (accessToken: string) => {
|
|
159
|
+
this.storage.setItem(STORAGE_KEYS.ACCESS_TOKEN, accessToken);
|
|
160
|
+
};
|
|
156
161
|
}
|
|
157
162
|
|
|
158
163
|
/**
|
package/src/HttpService.ts
CHANGED
|
@@ -52,6 +52,8 @@ interface RequestConfig extends RequestOptions {
|
|
|
52
52
|
url: string;
|
|
53
53
|
data?: unknown;
|
|
54
54
|
params?: Record<string, unknown>;
|
|
55
|
+
/** @internal Used to prevent infinite auth retry loops */
|
|
56
|
+
_isAuthRetry?: boolean;
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
/**
|
|
@@ -132,6 +134,7 @@ export class HttpService {
|
|
|
132
134
|
private logger: SimpleLogger;
|
|
133
135
|
private config: OxyConfig;
|
|
134
136
|
private tokenRefreshPromise: Promise<string | null> | null = null;
|
|
137
|
+
private _onTokenRefreshed: ((accessToken: string) => void) | null = null;
|
|
135
138
|
|
|
136
139
|
// Performance monitoring
|
|
137
140
|
private requestMetrics = {
|
|
@@ -322,10 +325,27 @@ export class HttpService {
|
|
|
322
325
|
|
|
323
326
|
// Handle response
|
|
324
327
|
if (!response.ok) {
|
|
325
|
-
|
|
328
|
+
// On 401, attempt token refresh and retry once before giving up
|
|
329
|
+
if (response.status === 401 && !config._isAuthRetry) {
|
|
330
|
+
const currentToken = this.tokenStore.getAccessToken();
|
|
331
|
+
if (currentToken) {
|
|
332
|
+
try {
|
|
333
|
+
const decoded = jwtDecode<JwtPayload>(currentToken);
|
|
334
|
+
if (decoded.sessionId) {
|
|
335
|
+
const refreshResult = await this._refreshTokenFromSession(decoded.sessionId);
|
|
336
|
+
if (refreshResult) {
|
|
337
|
+
// Retry the request with the new token
|
|
338
|
+
return this.request<T>({ ...config, _isAuthRetry: true, retry: false });
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
} catch {
|
|
342
|
+
// Token decode failed, fall through to clear
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
// Refresh failed or no token — clear tokens
|
|
326
346
|
this.tokenStore.clearTokens();
|
|
327
347
|
}
|
|
328
|
-
|
|
348
|
+
|
|
329
349
|
// Try to parse error response (handle empty/malformed JSON)
|
|
330
350
|
let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
|
|
331
351
|
const contentType = response.headers.get('content-type');
|
|
@@ -343,10 +363,10 @@ export class HttpService {
|
|
|
343
363
|
this.logger.warn('Failed to parse error response JSON:', parseError);
|
|
344
364
|
}
|
|
345
365
|
}
|
|
346
|
-
|
|
347
|
-
const error = new Error(errorMessage) as Error & {
|
|
348
|
-
status?: number;
|
|
349
|
-
response?: { status: number; statusText: string }
|
|
366
|
+
|
|
367
|
+
const error = new Error(errorMessage) as Error & {
|
|
368
|
+
status?: number;
|
|
369
|
+
response?: { status: number; statusText: string }
|
|
350
370
|
};
|
|
351
371
|
error.status = response.status;
|
|
352
372
|
error.response = { status: response.status, statusText: response.statusText };
|
|
@@ -596,6 +616,7 @@ export class HttpService {
|
|
|
596
616
|
if (response.ok) {
|
|
597
617
|
const { accessToken: newToken } = await response.json();
|
|
598
618
|
this.tokenStore.setTokens(newToken);
|
|
619
|
+
this._onTokenRefreshed?.(newToken);
|
|
599
620
|
this.logger.debug('Token refreshed');
|
|
600
621
|
return `Bearer ${newToken}`;
|
|
601
622
|
}
|
|
@@ -712,6 +733,10 @@ export class HttpService {
|
|
|
712
733
|
this.tokenStore.setTokens(accessToken, refreshToken);
|
|
713
734
|
}
|
|
714
735
|
|
|
736
|
+
set onTokenRefreshed(callback: ((accessToken: string) => void) | null) {
|
|
737
|
+
this._onTokenRefreshed = callback;
|
|
738
|
+
}
|
|
739
|
+
|
|
715
740
|
clearTokens(): void {
|
|
716
741
|
this.tokenStore.clearTokens();
|
|
717
742
|
this.tokenStore.clearCsrfToken();
|
|
@@ -131,12 +131,21 @@ export function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
131
131
|
return session;
|
|
132
132
|
} catch (error) {
|
|
133
133
|
debug.log('Interactive sign-in failed:', error);
|
|
134
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
135
|
+
|
|
134
136
|
if ((error as any).name === 'AbortError') {
|
|
135
137
|
throw new OxyAuthenticationError('Sign-in was cancelled by user');
|
|
136
138
|
}
|
|
137
139
|
if ((error as any).name === 'NetworkError') {
|
|
138
140
|
throw new OxyAuthenticationError('Network error during sign-in. Please check your connection.');
|
|
139
141
|
}
|
|
142
|
+
if (errorMessage.includes('multiple accounts')) {
|
|
143
|
+
throw new OxyAuthenticationError('Please sign out and sign in again to use FedCM with a single account');
|
|
144
|
+
}
|
|
145
|
+
if (errorMessage.includes('retrieving a token') || errorMessage.includes('Error retrieving')) {
|
|
146
|
+
debug.error('FedCM token retrieval error - this may be a browser or IdP configuration issue');
|
|
147
|
+
throw new OxyAuthenticationError('Authentication failed. Please try again or use an alternative sign-in method.');
|
|
148
|
+
}
|
|
140
149
|
throw error;
|
|
141
150
|
}
|
|
142
151
|
}
|
|
@@ -201,7 +210,17 @@ export function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
201
210
|
} catch (silentError) {
|
|
202
211
|
const errorName = silentError instanceof Error ? silentError.name : 'Unknown';
|
|
203
212
|
const errorMessage = silentError instanceof Error ? silentError.message : String(silentError);
|
|
204
|
-
|
|
213
|
+
|
|
214
|
+
// Handle specific FedCM errors with better logging
|
|
215
|
+
if (errorMessage.includes('multiple accounts')) {
|
|
216
|
+
debug.log('Silent SSO: User has used multiple accounts - silent mediation not available');
|
|
217
|
+
debug.log('Silent SSO: User needs to explicitly sign in to choose account');
|
|
218
|
+
} else if (errorMessage.includes('conditions')) {
|
|
219
|
+
debug.log('Silent SSO: Conditions not met (user may not be logged in at IdP or not in approved_clients)');
|
|
220
|
+
} else {
|
|
221
|
+
debug.log('Silent SSO: Silent mediation failed:', { name: errorName, message: errorMessage });
|
|
222
|
+
}
|
|
223
|
+
|
|
205
224
|
return null;
|
|
206
225
|
}
|
|
207
226
|
|