@oxyhq/core 1.6.6 → 1.8.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 +2 -1
- package/dist/cjs/HttpService.js +5 -2
- package/dist/cjs/mixins/OxyServices.fedcm.js +31 -10
- package/dist/esm/AuthManager.js +2 -1
- package/dist/esm/HttpService.js +5 -2
- package/dist/esm/mixins/OxyServices.fedcm.js +31 -10
- package/dist/types/mixins/OxyServices.fedcm.d.ts +7 -2
- package/package.json +1 -1
- package/src/AuthManager.ts +2 -1
- package/src/HttpService.ts +5 -2
- package/src/mixins/OxyServices.fedcm.ts +34 -12
package/dist/cjs/AuthManager.js
CHANGED
|
@@ -296,11 +296,12 @@ class AuthManager {
|
|
|
296
296
|
* Sign out and clear all auth data.
|
|
297
297
|
*/
|
|
298
298
|
async signOut() {
|
|
299
|
-
// Clear refresh timer
|
|
299
|
+
// Clear refresh timer and cancel any in-flight refresh
|
|
300
300
|
if (this.refreshTimer) {
|
|
301
301
|
clearTimeout(this.refreshTimer);
|
|
302
302
|
this.refreshTimer = null;
|
|
303
303
|
}
|
|
304
|
+
this.refreshPromise = null;
|
|
304
305
|
// Invalidate current session on the server (best-effort)
|
|
305
306
|
try {
|
|
306
307
|
const sessionJson = await this.storage.getItem(STORAGE_KEYS.SESSION);
|
package/dist/cjs/HttpService.js
CHANGED
|
@@ -244,8 +244,9 @@ class HttpService {
|
|
|
244
244
|
// Token decode failed, fall through to clear
|
|
245
245
|
}
|
|
246
246
|
}
|
|
247
|
-
// Refresh failed or no token — clear tokens
|
|
247
|
+
// Refresh failed or no token — clear tokens and stale CSRF
|
|
248
248
|
this.tokenStore.clearTokens();
|
|
249
|
+
this.tokenStore.clearCsrfToken();
|
|
249
250
|
}
|
|
250
251
|
// On 403 with CSRF error, clear cached token and retry once
|
|
251
252
|
if (response.status === 403 && !config._isCsrfRetry) {
|
|
@@ -503,12 +504,14 @@ class HttpService {
|
|
|
503
504
|
const result = await this.tokenRefreshPromise;
|
|
504
505
|
if (result)
|
|
505
506
|
return result;
|
|
507
|
+
// Refresh failed — don't use the expired token (would cause 401 loop)
|
|
508
|
+
return null;
|
|
506
509
|
}
|
|
507
510
|
return `Bearer ${accessToken}`;
|
|
508
511
|
}
|
|
509
512
|
catch (error) {
|
|
510
513
|
this.logger.error('Error processing token:', error);
|
|
511
|
-
return
|
|
514
|
+
return null;
|
|
512
515
|
}
|
|
513
516
|
}
|
|
514
517
|
async _refreshTokenFromSession(sessionId) {
|
|
@@ -87,12 +87,14 @@ function OxyServicesFedCMMixin(Base) {
|
|
|
87
87
|
const loginHint = options.loginHint || this.getStoredLoginHint();
|
|
88
88
|
debug.log('Interactive sign-in: Requesting credential for', clientId, loginHint ? `(hint: ${loginHint})` : '');
|
|
89
89
|
// Request credential from browser's native identity flow
|
|
90
|
+
// mode: 'button' signals this is a user-gesture-initiated flow (Chrome 125+)
|
|
90
91
|
const credential = await this.requestIdentityCredential({
|
|
91
92
|
configURL: this.constructor.DEFAULT_CONFIG_URL,
|
|
92
93
|
clientId,
|
|
93
94
|
nonce,
|
|
94
95
|
context: options.context,
|
|
95
96
|
loginHint,
|
|
97
|
+
mode: 'button',
|
|
96
98
|
});
|
|
97
99
|
if (!credential || !credential.token) {
|
|
98
100
|
throw new OxyServices_errors_1.OxyAuthenticationError('No credential received from browser');
|
|
@@ -302,7 +304,7 @@ function OxyServicesFedCMMixin(Base) {
|
|
|
302
304
|
try {
|
|
303
305
|
debug.log('Calling navigator.credentials.get with mediation:', requestedMediation);
|
|
304
306
|
// Type assertion needed as FedCM types may not be in all TypeScript versions
|
|
305
|
-
const
|
|
307
|
+
const credentialOptions = {
|
|
306
308
|
identity: {
|
|
307
309
|
providers: [
|
|
308
310
|
{
|
|
@@ -316,10 +318,12 @@ function OxyServicesFedCMMixin(Base) {
|
|
|
316
318
|
...(options.loginHint && { loginHint: options.loginHint }),
|
|
317
319
|
},
|
|
318
320
|
],
|
|
321
|
+
...(options.mode && { mode: options.mode }),
|
|
319
322
|
},
|
|
320
323
|
mediation: requestedMediation,
|
|
321
324
|
signal: controller.signal,
|
|
322
|
-
}
|
|
325
|
+
};
|
|
326
|
+
const credential = (await navigator.credentials.get(credentialOptions));
|
|
323
327
|
debug.log('navigator.credentials.get returned:', {
|
|
324
328
|
hasCredential: !!credential,
|
|
325
329
|
type: credential?.type,
|
|
@@ -329,8 +333,9 @@ function OxyServicesFedCMMixin(Base) {
|
|
|
329
333
|
debug.log('No valid identity credential returned');
|
|
330
334
|
return null;
|
|
331
335
|
}
|
|
332
|
-
|
|
333
|
-
|
|
336
|
+
const isAutoSelected = !!credential.isAutoSelected;
|
|
337
|
+
debug.log('Got valid identity credential with token', { isAutoSelected });
|
|
338
|
+
return { token: credential.token, isAutoSelected };
|
|
334
339
|
}
|
|
335
340
|
catch (error) {
|
|
336
341
|
const errorName = error instanceof Error ? error.name : 'Unknown';
|
|
@@ -373,25 +378,30 @@ function OxyServicesFedCMMixin(Base) {
|
|
|
373
378
|
/**
|
|
374
379
|
* Revoke FedCM credential (sign out)
|
|
375
380
|
*
|
|
376
|
-
*
|
|
377
|
-
*
|
|
381
|
+
* Uses IdentityCredential.disconnect() to tell the browser to forget
|
|
382
|
+
* the RP-IdP-account association. This resets the "returning account"
|
|
383
|
+
* state, which is required for silent mediation to work again.
|
|
378
384
|
*/
|
|
379
385
|
async revokeFedCMCredential() {
|
|
386
|
+
// Read hint before clearing so we can pass it to disconnect()
|
|
387
|
+
const accountHint = this.getStoredLoginHint();
|
|
388
|
+
this.clearLoginHint();
|
|
380
389
|
if (!this.isFedCMSupported()) {
|
|
381
390
|
return;
|
|
382
391
|
}
|
|
383
392
|
try {
|
|
384
|
-
|
|
385
|
-
if ('IdentityCredential' in window && 'logout' in window.IdentityCredential) {
|
|
393
|
+
if ('IdentityCredential' in window && 'disconnect' in window.IdentityCredential) {
|
|
386
394
|
const clientId = this.getClientId();
|
|
387
|
-
await window.IdentityCredential.
|
|
395
|
+
await window.IdentityCredential.disconnect({
|
|
388
396
|
configURL: this.constructor.DEFAULT_CONFIG_URL,
|
|
389
397
|
clientId,
|
|
398
|
+
accountHint: accountHint || '*',
|
|
390
399
|
});
|
|
400
|
+
debug.log('FedCM credential disconnected');
|
|
391
401
|
}
|
|
392
402
|
}
|
|
393
403
|
catch (error) {
|
|
394
|
-
|
|
404
|
+
debug.log('FedCM disconnect failed (non-critical):', error instanceof Error ? error.message : String(error));
|
|
395
405
|
}
|
|
396
406
|
}
|
|
397
407
|
/**
|
|
@@ -455,6 +465,17 @@ function OxyServicesFedCMMixin(Base) {
|
|
|
455
465
|
// Storage full or blocked
|
|
456
466
|
}
|
|
457
467
|
}
|
|
468
|
+
/** @internal */
|
|
469
|
+
clearLoginHint() {
|
|
470
|
+
if (typeof window === 'undefined')
|
|
471
|
+
return;
|
|
472
|
+
try {
|
|
473
|
+
localStorage.removeItem(FEDCM_LOGIN_HINT_KEY);
|
|
474
|
+
}
|
|
475
|
+
catch {
|
|
476
|
+
// Storage blocked
|
|
477
|
+
}
|
|
478
|
+
}
|
|
458
479
|
},
|
|
459
480
|
_a.DEFAULT_CONFIG_URL = 'https://auth.oxy.so/fedcm.json',
|
|
460
481
|
_a.FEDCM_TIMEOUT = 15000 // 15 seconds for interactive
|
package/dist/esm/AuthManager.js
CHANGED
|
@@ -292,11 +292,12 @@ export class AuthManager {
|
|
|
292
292
|
* Sign out and clear all auth data.
|
|
293
293
|
*/
|
|
294
294
|
async signOut() {
|
|
295
|
-
// Clear refresh timer
|
|
295
|
+
// Clear refresh timer and cancel any in-flight refresh
|
|
296
296
|
if (this.refreshTimer) {
|
|
297
297
|
clearTimeout(this.refreshTimer);
|
|
298
298
|
this.refreshTimer = null;
|
|
299
299
|
}
|
|
300
|
+
this.refreshPromise = null;
|
|
300
301
|
// Invalidate current session on the server (best-effort)
|
|
301
302
|
try {
|
|
302
303
|
const sessionJson = await this.storage.getItem(STORAGE_KEYS.SESSION);
|
package/dist/esm/HttpService.js
CHANGED
|
@@ -241,8 +241,9 @@ export class HttpService {
|
|
|
241
241
|
// Token decode failed, fall through to clear
|
|
242
242
|
}
|
|
243
243
|
}
|
|
244
|
-
// Refresh failed or no token — clear tokens
|
|
244
|
+
// Refresh failed or no token — clear tokens and stale CSRF
|
|
245
245
|
this.tokenStore.clearTokens();
|
|
246
|
+
this.tokenStore.clearCsrfToken();
|
|
246
247
|
}
|
|
247
248
|
// On 403 with CSRF error, clear cached token and retry once
|
|
248
249
|
if (response.status === 403 && !config._isCsrfRetry) {
|
|
@@ -500,12 +501,14 @@ export class HttpService {
|
|
|
500
501
|
const result = await this.tokenRefreshPromise;
|
|
501
502
|
if (result)
|
|
502
503
|
return result;
|
|
504
|
+
// Refresh failed — don't use the expired token (would cause 401 loop)
|
|
505
|
+
return null;
|
|
503
506
|
}
|
|
504
507
|
return `Bearer ${accessToken}`;
|
|
505
508
|
}
|
|
506
509
|
catch (error) {
|
|
507
510
|
this.logger.error('Error processing token:', error);
|
|
508
|
-
return
|
|
511
|
+
return null;
|
|
509
512
|
}
|
|
510
513
|
}
|
|
511
514
|
async _refreshTokenFromSession(sessionId) {
|
|
@@ -83,12 +83,14 @@ export function OxyServicesFedCMMixin(Base) {
|
|
|
83
83
|
const loginHint = options.loginHint || this.getStoredLoginHint();
|
|
84
84
|
debug.log('Interactive sign-in: Requesting credential for', clientId, loginHint ? `(hint: ${loginHint})` : '');
|
|
85
85
|
// Request credential from browser's native identity flow
|
|
86
|
+
// mode: 'button' signals this is a user-gesture-initiated flow (Chrome 125+)
|
|
86
87
|
const credential = await this.requestIdentityCredential({
|
|
87
88
|
configURL: this.constructor.DEFAULT_CONFIG_URL,
|
|
88
89
|
clientId,
|
|
89
90
|
nonce,
|
|
90
91
|
context: options.context,
|
|
91
92
|
loginHint,
|
|
93
|
+
mode: 'button',
|
|
92
94
|
});
|
|
93
95
|
if (!credential || !credential.token) {
|
|
94
96
|
throw new OxyAuthenticationError('No credential received from browser');
|
|
@@ -298,7 +300,7 @@ export function OxyServicesFedCMMixin(Base) {
|
|
|
298
300
|
try {
|
|
299
301
|
debug.log('Calling navigator.credentials.get with mediation:', requestedMediation);
|
|
300
302
|
// Type assertion needed as FedCM types may not be in all TypeScript versions
|
|
301
|
-
const
|
|
303
|
+
const credentialOptions = {
|
|
302
304
|
identity: {
|
|
303
305
|
providers: [
|
|
304
306
|
{
|
|
@@ -312,10 +314,12 @@ export function OxyServicesFedCMMixin(Base) {
|
|
|
312
314
|
...(options.loginHint && { loginHint: options.loginHint }),
|
|
313
315
|
},
|
|
314
316
|
],
|
|
317
|
+
...(options.mode && { mode: options.mode }),
|
|
315
318
|
},
|
|
316
319
|
mediation: requestedMediation,
|
|
317
320
|
signal: controller.signal,
|
|
318
|
-
}
|
|
321
|
+
};
|
|
322
|
+
const credential = (await navigator.credentials.get(credentialOptions));
|
|
319
323
|
debug.log('navigator.credentials.get returned:', {
|
|
320
324
|
hasCredential: !!credential,
|
|
321
325
|
type: credential?.type,
|
|
@@ -325,8 +329,9 @@ export function OxyServicesFedCMMixin(Base) {
|
|
|
325
329
|
debug.log('No valid identity credential returned');
|
|
326
330
|
return null;
|
|
327
331
|
}
|
|
328
|
-
|
|
329
|
-
|
|
332
|
+
const isAutoSelected = !!credential.isAutoSelected;
|
|
333
|
+
debug.log('Got valid identity credential with token', { isAutoSelected });
|
|
334
|
+
return { token: credential.token, isAutoSelected };
|
|
330
335
|
}
|
|
331
336
|
catch (error) {
|
|
332
337
|
const errorName = error instanceof Error ? error.name : 'Unknown';
|
|
@@ -369,25 +374,30 @@ export function OxyServicesFedCMMixin(Base) {
|
|
|
369
374
|
/**
|
|
370
375
|
* Revoke FedCM credential (sign out)
|
|
371
376
|
*
|
|
372
|
-
*
|
|
373
|
-
*
|
|
377
|
+
* Uses IdentityCredential.disconnect() to tell the browser to forget
|
|
378
|
+
* the RP-IdP-account association. This resets the "returning account"
|
|
379
|
+
* state, which is required for silent mediation to work again.
|
|
374
380
|
*/
|
|
375
381
|
async revokeFedCMCredential() {
|
|
382
|
+
// Read hint before clearing so we can pass it to disconnect()
|
|
383
|
+
const accountHint = this.getStoredLoginHint();
|
|
384
|
+
this.clearLoginHint();
|
|
376
385
|
if (!this.isFedCMSupported()) {
|
|
377
386
|
return;
|
|
378
387
|
}
|
|
379
388
|
try {
|
|
380
|
-
|
|
381
|
-
if ('IdentityCredential' in window && 'logout' in window.IdentityCredential) {
|
|
389
|
+
if ('IdentityCredential' in window && 'disconnect' in window.IdentityCredential) {
|
|
382
390
|
const clientId = this.getClientId();
|
|
383
|
-
await window.IdentityCredential.
|
|
391
|
+
await window.IdentityCredential.disconnect({
|
|
384
392
|
configURL: this.constructor.DEFAULT_CONFIG_URL,
|
|
385
393
|
clientId,
|
|
394
|
+
accountHint: accountHint || '*',
|
|
386
395
|
});
|
|
396
|
+
debug.log('FedCM credential disconnected');
|
|
387
397
|
}
|
|
388
398
|
}
|
|
389
399
|
catch (error) {
|
|
390
|
-
|
|
400
|
+
debug.log('FedCM disconnect failed (non-critical):', error instanceof Error ? error.message : String(error));
|
|
391
401
|
}
|
|
392
402
|
}
|
|
393
403
|
/**
|
|
@@ -451,6 +461,17 @@ export function OxyServicesFedCMMixin(Base) {
|
|
|
451
461
|
// Storage full or blocked
|
|
452
462
|
}
|
|
453
463
|
}
|
|
464
|
+
/** @internal */
|
|
465
|
+
clearLoginHint() {
|
|
466
|
+
if (typeof window === 'undefined')
|
|
467
|
+
return;
|
|
468
|
+
try {
|
|
469
|
+
localStorage.removeItem(FEDCM_LOGIN_HINT_KEY);
|
|
470
|
+
}
|
|
471
|
+
catch {
|
|
472
|
+
// Storage blocked
|
|
473
|
+
}
|
|
474
|
+
}
|
|
454
475
|
},
|
|
455
476
|
_a.DEFAULT_CONFIG_URL = 'https://auth.oxy.so/fedcm.json',
|
|
456
477
|
_a.FEDCM_TIMEOUT = 15000 // 15 seconds for interactive
|
|
@@ -110,8 +110,10 @@ export declare function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(
|
|
|
110
110
|
context?: string;
|
|
111
111
|
loginHint?: string;
|
|
112
112
|
mediation?: "silent" | "optional" | "required";
|
|
113
|
+
mode?: "button" | "widget";
|
|
113
114
|
}): Promise<{
|
|
114
115
|
token: string;
|
|
116
|
+
isAutoSelected: boolean;
|
|
115
117
|
} | null>;
|
|
116
118
|
/**
|
|
117
119
|
* Exchange FedCM ID token for Oxy session
|
|
@@ -125,8 +127,9 @@ export declare function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(
|
|
|
125
127
|
/**
|
|
126
128
|
* Revoke FedCM credential (sign out)
|
|
127
129
|
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
+
* Uses IdentityCredential.disconnect() to tell the browser to forget
|
|
131
|
+
* the RP-IdP-account association. This resets the "returning account"
|
|
132
|
+
* state, which is required for silent mediation to work again.
|
|
130
133
|
*/
|
|
131
134
|
revokeFedCMCredential(): Promise<void>;
|
|
132
135
|
/**
|
|
@@ -151,6 +154,8 @@ export declare function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(
|
|
|
151
154
|
getStoredLoginHint(): string | undefined;
|
|
152
155
|
/** @internal */
|
|
153
156
|
storeLoginHint(userId: string): void;
|
|
157
|
+
/** @internal */
|
|
158
|
+
clearLoginHint(): void;
|
|
154
159
|
httpService: import("../HttpService").HttpService;
|
|
155
160
|
cloudURL: string;
|
|
156
161
|
config: import("../OxyServices.base").OxyConfig;
|
package/package.json
CHANGED
package/src/AuthManager.ts
CHANGED
|
@@ -361,11 +361,12 @@ export class AuthManager {
|
|
|
361
361
|
* Sign out and clear all auth data.
|
|
362
362
|
*/
|
|
363
363
|
async signOut(): Promise<void> {
|
|
364
|
-
// Clear refresh timer
|
|
364
|
+
// Clear refresh timer and cancel any in-flight refresh
|
|
365
365
|
if (this.refreshTimer) {
|
|
366
366
|
clearTimeout(this.refreshTimer);
|
|
367
367
|
this.refreshTimer = null;
|
|
368
368
|
}
|
|
369
|
+
this.refreshPromise = null;
|
|
369
370
|
|
|
370
371
|
// Invalidate current session on the server (best-effort)
|
|
371
372
|
try {
|
package/src/HttpService.ts
CHANGED
|
@@ -337,8 +337,9 @@ export class HttpService {
|
|
|
337
337
|
// Token decode failed, fall through to clear
|
|
338
338
|
}
|
|
339
339
|
}
|
|
340
|
-
// Refresh failed or no token — clear tokens
|
|
340
|
+
// Refresh failed or no token — clear tokens and stale CSRF
|
|
341
341
|
this.tokenStore.clearTokens();
|
|
342
|
+
this.tokenStore.clearCsrfToken();
|
|
342
343
|
}
|
|
343
344
|
|
|
344
345
|
// On 403 with CSRF error, clear cached token and retry once
|
|
@@ -616,12 +617,14 @@ export class HttpService {
|
|
|
616
617
|
}
|
|
617
618
|
const result = await this.tokenRefreshPromise;
|
|
618
619
|
if (result) return result;
|
|
620
|
+
// Refresh failed — don't use the expired token (would cause 401 loop)
|
|
621
|
+
return null;
|
|
619
622
|
}
|
|
620
623
|
|
|
621
624
|
return `Bearer ${accessToken}`;
|
|
622
625
|
} catch (error) {
|
|
623
626
|
this.logger.error('Error processing token:', error);
|
|
624
|
-
return
|
|
627
|
+
return null;
|
|
625
628
|
}
|
|
626
629
|
}
|
|
627
630
|
|
|
@@ -111,12 +111,14 @@ export function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
111
111
|
debug.log('Interactive sign-in: Requesting credential for', clientId, loginHint ? `(hint: ${loginHint})` : '');
|
|
112
112
|
|
|
113
113
|
// Request credential from browser's native identity flow
|
|
114
|
+
// mode: 'button' signals this is a user-gesture-initiated flow (Chrome 125+)
|
|
114
115
|
const credential = await this.requestIdentityCredential({
|
|
115
116
|
configURL: (this.constructor as any).DEFAULT_CONFIG_URL,
|
|
116
117
|
clientId,
|
|
117
118
|
nonce,
|
|
118
119
|
context: options.context,
|
|
119
120
|
loginHint,
|
|
121
|
+
mode: 'button',
|
|
120
122
|
});
|
|
121
123
|
|
|
122
124
|
if (!credential || !credential.token) {
|
|
@@ -205,7 +207,7 @@ export function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
205
207
|
// We intentionally do NOT fall back to optional mediation here because
|
|
206
208
|
// this runs on app startup — showing browser UI without user action is bad UX.
|
|
207
209
|
// Optional/interactive mediation should only happen when the user clicks "Sign In".
|
|
208
|
-
let credential: { token: string } | null = null;
|
|
210
|
+
let credential: { token: string; isAutoSelected: boolean } | null = null;
|
|
209
211
|
|
|
210
212
|
const loginHint = this.getStoredLoginHint();
|
|
211
213
|
|
|
@@ -308,7 +310,8 @@ export function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
308
310
|
context?: string;
|
|
309
311
|
loginHint?: string;
|
|
310
312
|
mediation?: 'silent' | 'optional' | 'required';
|
|
311
|
-
|
|
313
|
+
mode?: 'button' | 'widget';
|
|
314
|
+
}): Promise<{ token: string; isAutoSelected: boolean } | null> {
|
|
312
315
|
const requestedMediation = options.mediation || 'optional';
|
|
313
316
|
const isInteractive = requestedMediation !== 'silent';
|
|
314
317
|
|
|
@@ -356,7 +359,7 @@ export function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
356
359
|
try {
|
|
357
360
|
debug.log('Calling navigator.credentials.get with mediation:', requestedMediation);
|
|
358
361
|
// Type assertion needed as FedCM types may not be in all TypeScript versions
|
|
359
|
-
const
|
|
362
|
+
const credentialOptions: any = {
|
|
360
363
|
identity: {
|
|
361
364
|
providers: [
|
|
362
365
|
{
|
|
@@ -370,10 +373,12 @@ export function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
370
373
|
...(options.loginHint && { loginHint: options.loginHint }),
|
|
371
374
|
},
|
|
372
375
|
],
|
|
376
|
+
...(options.mode && { mode: options.mode }),
|
|
373
377
|
},
|
|
374
378
|
mediation: requestedMediation,
|
|
375
379
|
signal: controller.signal,
|
|
376
|
-
}
|
|
380
|
+
};
|
|
381
|
+
const credential = (await (navigator.credentials as any).get(credentialOptions)) as any;
|
|
377
382
|
|
|
378
383
|
debug.log('navigator.credentials.get returned:', {
|
|
379
384
|
hasCredential: !!credential,
|
|
@@ -386,8 +391,9 @@ export function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
386
391
|
return null;
|
|
387
392
|
}
|
|
388
393
|
|
|
389
|
-
|
|
390
|
-
|
|
394
|
+
const isAutoSelected = !!credential.isAutoSelected;
|
|
395
|
+
debug.log('Got valid identity credential with token', { isAutoSelected });
|
|
396
|
+
return { token: credential.token, isAutoSelected };
|
|
391
397
|
} catch (error) {
|
|
392
398
|
const errorName = error instanceof Error ? error.name : 'Unknown';
|
|
393
399
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -438,25 +444,31 @@ export function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
438
444
|
/**
|
|
439
445
|
* Revoke FedCM credential (sign out)
|
|
440
446
|
*
|
|
441
|
-
*
|
|
442
|
-
*
|
|
447
|
+
* Uses IdentityCredential.disconnect() to tell the browser to forget
|
|
448
|
+
* the RP-IdP-account association. This resets the "returning account"
|
|
449
|
+
* state, which is required for silent mediation to work again.
|
|
443
450
|
*/
|
|
444
451
|
async revokeFedCMCredential(): Promise<void> {
|
|
452
|
+
// Read hint before clearing so we can pass it to disconnect()
|
|
453
|
+
const accountHint = this.getStoredLoginHint();
|
|
454
|
+
this.clearLoginHint();
|
|
455
|
+
|
|
445
456
|
if (!this.isFedCMSupported()) {
|
|
446
457
|
return;
|
|
447
458
|
}
|
|
448
459
|
|
|
449
460
|
try {
|
|
450
|
-
|
|
451
|
-
if ('IdentityCredential' in window && 'logout' in (window as any).IdentityCredential) {
|
|
461
|
+
if ('IdentityCredential' in window && 'disconnect' in (window as any).IdentityCredential) {
|
|
452
462
|
const clientId = this.getClientId();
|
|
453
|
-
await (window as any).IdentityCredential.
|
|
463
|
+
await (window as any).IdentityCredential.disconnect({
|
|
454
464
|
configURL: (this.constructor as any).DEFAULT_CONFIG_URL,
|
|
455
465
|
clientId,
|
|
466
|
+
accountHint: accountHint || '*',
|
|
456
467
|
});
|
|
468
|
+
debug.log('FedCM credential disconnected');
|
|
457
469
|
}
|
|
458
470
|
} catch (error) {
|
|
459
|
-
|
|
471
|
+
debug.log('FedCM disconnect failed (non-critical):', error instanceof Error ? error.message : String(error));
|
|
460
472
|
}
|
|
461
473
|
}
|
|
462
474
|
|
|
@@ -521,6 +533,16 @@ export function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(Base: T)
|
|
|
521
533
|
// Storage full or blocked
|
|
522
534
|
}
|
|
523
535
|
}
|
|
536
|
+
|
|
537
|
+
/** @internal */
|
|
538
|
+
public clearLoginHint(): void {
|
|
539
|
+
if (typeof window === 'undefined') return;
|
|
540
|
+
try {
|
|
541
|
+
localStorage.removeItem(FEDCM_LOGIN_HINT_KEY);
|
|
542
|
+
} catch {
|
|
543
|
+
// Storage blocked
|
|
544
|
+
}
|
|
545
|
+
}
|
|
524
546
|
};
|
|
525
547
|
}
|
|
526
548
|
|