@oxyhq/core 1.11.18 → 1.11.20

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.
@@ -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: 'button' signals this is a user-gesture-initiated flow (Chrome 125+)
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: 'button',
135
+ mode: 'active',
102
136
  });
103
137
  if (!credential || !credential.token) {
104
138
  throw new OxyAuthenticationError('No credential received from browser');
@@ -106,9 +140,11 @@ export function OxyServicesFedCMMixin(Base) {
106
140
  debug.log('Interactive sign-in: Got credential, exchanging for session');
107
141
  // Exchange FedCM ID token for Oxy session
108
142
  const session = await this.exchangeIdTokenForSession(credential.token);
109
- // Store access token in HttpService (extract from response or get from session)
110
- if (session && session.accessToken) {
111
- this.httpService.setTokens(session.accessToken);
143
+ // Store access token in HttpService. `accessToken`/`refreshToken` are
144
+ // declared optional on SessionLoginResponse; default the refresh token to
145
+ // an empty string when the exchange did not return one.
146
+ if (session?.accessToken) {
147
+ this.httpService.setTokens(session.accessToken, session.refreshToken ?? '');
112
148
  }
113
149
  // Store the user ID as loginHint for future FedCM requests
114
150
  if (session?.user?.id) {
@@ -120,10 +156,13 @@ export function OxyServicesFedCMMixin(Base) {
120
156
  catch (error) {
121
157
  debug.log('Interactive sign-in failed:', error);
122
158
  const errorMessage = error instanceof Error ? error.message : String(error);
123
- if (error.name === 'AbortError') {
159
+ // FedCM aborts/network failures surface as DOMException/Error instances,
160
+ // both of which carry a `name`. Anything else has no meaningful name.
161
+ const errorName = error instanceof Error ? error.name : '';
162
+ if (errorName === 'AbortError') {
124
163
  throw new OxyAuthenticationError('Sign-in was cancelled by user');
125
164
  }
126
- if (error.name === 'NetworkError') {
165
+ if (errorName === 'NetworkError') {
127
166
  throw new OxyAuthenticationError('Network error during sign-in. Please check your connection.');
128
167
  }
129
168
  if (errorMessage.includes('multiple accounts')) {
@@ -235,9 +274,11 @@ export function OxyServicesFedCMMixin(Base) {
235
274
  debug.error('Silent SSO: Exchange returned session without user:', session);
236
275
  return null;
237
276
  }
238
- // Set the access token
277
+ // Set the access token. `accessToken`/`refreshToken` are declared optional
278
+ // on SessionLoginResponse; default the refresh token to an empty string when
279
+ // the exchange did not return one.
239
280
  if (session.accessToken) {
240
- this.httpService.setTokens(session.accessToken);
281
+ this.httpService.setTokens(session.accessToken, session.refreshToken ?? '');
241
282
  debug.log('Silent SSO: Access token set');
242
283
  }
243
284
  else {
@@ -306,37 +347,63 @@ export function OxyServicesFedCMMixin(Base) {
306
347
  debug.log('Request timed out after', timeoutMs, 'ms (mediation:', requestedMediation + ')');
307
348
  controller.abort();
308
349
  }, timeoutMs);
350
+ // Normalise the caller's mode to the modern W3C value first. A modern
351
+ // browser accepts it; an older one (Chrome 125–131) rejects it with a
352
+ // synchronous TypeError, in which case we retry with the legacy value.
353
+ const modernMode = options.mode ? toModernMode(options.mode) : undefined;
354
+ // Build the identity request for a specific mode value. The `mode` field
355
+ // lives on the `identity` object (sibling of `providers`), separate from
356
+ // the top-level `mediation` field.
357
+ const buildCredentialOptions = (modeValue) => ({
358
+ identity: {
359
+ providers: [
360
+ {
361
+ configURL: options.configURL,
362
+ clientId: options.clientId,
363
+ // Older browsers read `nonce` at the top level; Chrome 145+
364
+ // expects it inside `params`. Send both for full coverage.
365
+ nonce: options.nonce,
366
+ params: {
367
+ nonce: options.nonce,
368
+ },
369
+ ...(options.loginHint && { loginHint: options.loginHint }),
370
+ },
371
+ ],
372
+ ...(modeValue && { mode: modeValue }),
373
+ },
374
+ mediation: requestedMediation,
375
+ signal: controller.signal,
376
+ });
377
+ // The DOM lib's `CredentialsContainer` does not declare the FedCM `identity`
378
+ // request in every TypeScript version we build against. Re-type through the
379
+ // minimal structural interface above (not `any`) to keep this typed.
380
+ const credentials = navigator.credentials;
309
381
  fedCMRequestPromise = (async () => {
310
382
  try {
311
- debug.log('Calling navigator.credentials.get with mediation:', requestedMediation);
312
- // Type assertion needed as FedCM types may not be in all TypeScript versions
313
- const credentialOptions = {
314
- identity: {
315
- providers: [
316
- {
317
- configURL: options.configURL,
318
- clientId: options.clientId,
319
- // Older browsers read `nonce` at the top level; Chrome 145+
320
- // expects it inside `params`. Send both for full coverage.
321
- nonce: options.nonce,
322
- params: {
323
- nonce: options.nonce,
324
- },
325
- ...(options.loginHint && { loginHint: options.loginHint }),
326
- },
327
- ],
328
- ...(options.mode && { mode: options.mode }),
329
- },
330
- mediation: requestedMediation,
331
- signal: controller.signal,
332
- };
333
- const credential = (await navigator.credentials.get(credentialOptions));
383
+ debug.log('Calling navigator.credentials.get with mediation:', requestedMediation, modernMode ? `mode: ${modernMode}` : '');
384
+ let credential;
385
+ try {
386
+ credential = await credentials.get(buildCredentialOptions(modernMode));
387
+ }
388
+ catch (modeError) {
389
+ // Chrome 125–131 only knows the legacy 'button'/'widget' enum and
390
+ // throws a synchronous TypeError for the modern 'active'/'passive'
391
+ // values. Retry once with the legacy value so older browsers work.
392
+ if (modernMode && isUnknownModeEnumError(modeError)) {
393
+ const legacyMode = MODERN_TO_LEGACY_MODE[modernMode];
394
+ debug.log(`Browser rejected modern mode '${modernMode}'; retrying with legacy mode '${legacyMode}'`);
395
+ credential = await credentials.get(buildCredentialOptions(legacyMode));
396
+ }
397
+ else {
398
+ throw modeError;
399
+ }
400
+ }
334
401
  debug.log('navigator.credentials.get returned:', {
335
402
  hasCredential: !!credential,
336
403
  type: credential?.type,
337
404
  hasToken: !!credential?.token,
338
405
  });
339
- if (!credential || credential.type !== 'identity') {
406
+ if (!credential || credential.type !== 'identity' || !credential.token) {
340
407
  debug.log('No valid identity credential returned');
341
408
  return null;
342
409
  }
@@ -397,9 +464,15 @@ export function OxyServicesFedCMMixin(Base) {
397
464
  return;
398
465
  }
399
466
  try {
400
- if ('IdentityCredential' in window && 'disconnect' in window.IdentityCredential) {
467
+ // The DOM lib does not declare the global `IdentityCredential` interface
468
+ // object (with its static `disconnect`) in every TypeScript version we
469
+ // build against. Read it off `window` through the minimal structural type
470
+ // (not `any`), guarding that `disconnect` is actually present at runtime.
471
+ const fedCMWindow = window;
472
+ const identityCredential = fedCMWindow.IdentityCredential;
473
+ if (identityCredential && typeof identityCredential.disconnect === 'function') {
401
474
  const clientId = this.getClientId();
402
- await window.IdentityCredential.disconnect({
475
+ await identityCredential.disconnect({
403
476
  configURL: this.resolveFedcmConfigUrl(),
404
477
  clientId,
405
478
  accountHint: accountHint || '*',