@oxyhq/core 1.11.18 → 1.11.19
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/.tsbuildinfo +1 -1
- package/dist/cjs/mixins/OxyServices.fedcm.js +87 -27
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/mixins/OxyServices.fedcm.js +87 -27
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/mixins/OxyServices.fedcm.d.ts +19 -1
- package/package.json +1 -1
- package/src/mixins/OxyServices.fedcm.ts +146 -28
- package/src/mixins/__tests__/fedcm.test.ts +136 -0
|
@@ -1,6 +1,38 @@
|
|
|
1
1
|
import { OxyAuthenticationError } from '../OxyServices.errors.js';
|
|
2
2
|
import { createDebugLogger } from '../shared/utils/debugUtils.js';
|
|
3
3
|
const debug = createDebugLogger('FedCM');
|
|
4
|
+
// Modern (W3C spec) → legacy (Chrome 125–131) mode value mapping. Used to
|
|
5
|
+
// retry a credential request when an older browser rejects the modern enum.
|
|
6
|
+
const MODERN_TO_LEGACY_MODE = {
|
|
7
|
+
active: 'button',
|
|
8
|
+
passive: 'widget',
|
|
9
|
+
};
|
|
10
|
+
// Legacy → modern mapping so callers may pass either spelling.
|
|
11
|
+
const LEGACY_TO_MODERN_MODE = {
|
|
12
|
+
button: 'active',
|
|
13
|
+
widget: 'passive',
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Normalise any accepted mode value to the modern W3C spelling
|
|
17
|
+
* (`'active'`/`'passive'`), which is what is sent to the browser first.
|
|
18
|
+
*/
|
|
19
|
+
function toModernMode(mode) {
|
|
20
|
+
return mode === 'button' || mode === 'widget' ? LEGACY_TO_MODERN_MODE[mode] : mode;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Detect the synchronous `TypeError` a pre-spec browser throws when it does not
|
|
24
|
+
* recognise a modern `mode` enum value (e.g. Chrome 125–131 rejecting
|
|
25
|
+
* `'active'`/`'passive'`). Such a browser only understands the legacy
|
|
26
|
+
* `'button'`/`'widget'` values, so the caller can retry with those.
|
|
27
|
+
*/
|
|
28
|
+
function isUnknownModeEnumError(error) {
|
|
29
|
+
if (!(error instanceof TypeError))
|
|
30
|
+
return false;
|
|
31
|
+
const message = error.message.toLowerCase();
|
|
32
|
+
return (message.includes('identitycredentialrequestoptionsmode') ||
|
|
33
|
+
((message.includes('active') || message.includes('passive')) &&
|
|
34
|
+
(message.includes('enum') || message.includes('not a valid'))));
|
|
35
|
+
}
|
|
4
36
|
const FEDCM_LOGIN_HINT_KEY = 'oxy_fedcm_login_hint';
|
|
5
37
|
// Global lock to prevent concurrent FedCM requests
|
|
6
38
|
// FedCM only allows one navigator.credentials.get request at a time
|
|
@@ -90,15 +122,17 @@ export function OxyServicesFedCMMixin(Base) {
|
|
|
90
122
|
// Use provided loginHint, or fall back to stored last-used account ID
|
|
91
123
|
const loginHint = options.loginHint || this.getStoredLoginHint();
|
|
92
124
|
debug.log('Interactive sign-in: Requesting credential for', clientId, loginHint ? `(hint: ${loginHint})` : '');
|
|
93
|
-
// Request credential from browser's native identity flow
|
|
94
|
-
// mode: '
|
|
125
|
+
// Request credential from browser's native identity flow.
|
|
126
|
+
// mode: 'active' signals this is a user-gesture-initiated (button) flow.
|
|
127
|
+
// 'active' is the current W3C spec value; requestIdentityCredential
|
|
128
|
+
// transparently retries with the legacy 'button' value for Chrome 125–131.
|
|
95
129
|
const credential = await this.requestIdentityCredential({
|
|
96
130
|
configURL: this.resolveFedcmConfigUrl(),
|
|
97
131
|
clientId,
|
|
98
132
|
nonce,
|
|
99
133
|
context: options.context,
|
|
100
134
|
loginHint,
|
|
101
|
-
mode: '
|
|
135
|
+
mode: 'active',
|
|
102
136
|
});
|
|
103
137
|
if (!credential || !credential.token) {
|
|
104
138
|
throw new OxyAuthenticationError('No credential received from browser');
|
|
@@ -306,37 +340,63 @@ export function OxyServicesFedCMMixin(Base) {
|
|
|
306
340
|
debug.log('Request timed out after', timeoutMs, 'ms (mediation:', requestedMediation + ')');
|
|
307
341
|
controller.abort();
|
|
308
342
|
}, timeoutMs);
|
|
343
|
+
// Normalise the caller's mode to the modern W3C value first. A modern
|
|
344
|
+
// browser accepts it; an older one (Chrome 125–131) rejects it with a
|
|
345
|
+
// synchronous TypeError, in which case we retry with the legacy value.
|
|
346
|
+
const modernMode = options.mode ? toModernMode(options.mode) : undefined;
|
|
347
|
+
// Build the identity request for a specific mode value. The `mode` field
|
|
348
|
+
// lives on the `identity` object (sibling of `providers`), separate from
|
|
349
|
+
// the top-level `mediation` field.
|
|
350
|
+
const buildCredentialOptions = (modeValue) => ({
|
|
351
|
+
identity: {
|
|
352
|
+
providers: [
|
|
353
|
+
{
|
|
354
|
+
configURL: options.configURL,
|
|
355
|
+
clientId: options.clientId,
|
|
356
|
+
// Older browsers read `nonce` at the top level; Chrome 145+
|
|
357
|
+
// expects it inside `params`. Send both for full coverage.
|
|
358
|
+
nonce: options.nonce,
|
|
359
|
+
params: {
|
|
360
|
+
nonce: options.nonce,
|
|
361
|
+
},
|
|
362
|
+
...(options.loginHint && { loginHint: options.loginHint }),
|
|
363
|
+
},
|
|
364
|
+
],
|
|
365
|
+
...(modeValue && { mode: modeValue }),
|
|
366
|
+
},
|
|
367
|
+
mediation: requestedMediation,
|
|
368
|
+
signal: controller.signal,
|
|
369
|
+
});
|
|
370
|
+
// The DOM lib's `CredentialsContainer` does not declare the FedCM `identity`
|
|
371
|
+
// request in every TypeScript version we build against. Re-type through the
|
|
372
|
+
// minimal structural interface above (not `any`) to keep this typed.
|
|
373
|
+
const credentials = navigator.credentials;
|
|
309
374
|
fedCMRequestPromise = (async () => {
|
|
310
375
|
try {
|
|
311
|
-
debug.log('Calling navigator.credentials.get with mediation:', requestedMediation);
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
},
|
|
330
|
-
mediation: requestedMediation,
|
|
331
|
-
signal: controller.signal,
|
|
332
|
-
};
|
|
333
|
-
const credential = (await navigator.credentials.get(credentialOptions));
|
|
376
|
+
debug.log('Calling navigator.credentials.get with mediation:', requestedMediation, modernMode ? `mode: ${modernMode}` : '');
|
|
377
|
+
let credential;
|
|
378
|
+
try {
|
|
379
|
+
credential = await credentials.get(buildCredentialOptions(modernMode));
|
|
380
|
+
}
|
|
381
|
+
catch (modeError) {
|
|
382
|
+
// Chrome 125–131 only knows the legacy 'button'/'widget' enum and
|
|
383
|
+
// throws a synchronous TypeError for the modern 'active'/'passive'
|
|
384
|
+
// values. Retry once with the legacy value so older browsers work.
|
|
385
|
+
if (modernMode && isUnknownModeEnumError(modeError)) {
|
|
386
|
+
const legacyMode = MODERN_TO_LEGACY_MODE[modernMode];
|
|
387
|
+
debug.log(`Browser rejected modern mode '${modernMode}'; retrying with legacy mode '${legacyMode}'`);
|
|
388
|
+
credential = await credentials.get(buildCredentialOptions(legacyMode));
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
throw modeError;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
334
394
|
debug.log('navigator.credentials.get returned:', {
|
|
335
395
|
hasCredential: !!credential,
|
|
336
396
|
type: credential?.type,
|
|
337
397
|
hasToken: !!credential?.token,
|
|
338
398
|
});
|
|
339
|
-
if (!credential || credential.type !== 'identity') {
|
|
399
|
+
if (!credential || credential.type !== 'identity' || !credential.token) {
|
|
340
400
|
debug.log('No valid identity credential returned');
|
|
341
401
|
return null;
|
|
342
402
|
}
|