@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.
@@ -5,6 +5,38 @@ exports.FedCMMixin = OxyServicesFedCMMixin;
5
5
  const OxyServices_errors_1 = require("../OxyServices.errors");
6
6
  const debugUtils_1 = require("../shared/utils/debugUtils");
7
7
  const debug = (0, debugUtils_1.createDebugLogger)('FedCM');
8
+ // Modern (W3C spec) → legacy (Chrome 125–131) mode value mapping. Used to
9
+ // retry a credential request when an older browser rejects the modern enum.
10
+ const MODERN_TO_LEGACY_MODE = {
11
+ active: 'button',
12
+ passive: 'widget',
13
+ };
14
+ // Legacy → modern mapping so callers may pass either spelling.
15
+ const LEGACY_TO_MODERN_MODE = {
16
+ button: 'active',
17
+ widget: 'passive',
18
+ };
19
+ /**
20
+ * Normalise any accepted mode value to the modern W3C spelling
21
+ * (`'active'`/`'passive'`), which is what is sent to the browser first.
22
+ */
23
+ function toModernMode(mode) {
24
+ return mode === 'button' || mode === 'widget' ? LEGACY_TO_MODERN_MODE[mode] : mode;
25
+ }
26
+ /**
27
+ * Detect the synchronous `TypeError` a pre-spec browser throws when it does not
28
+ * recognise a modern `mode` enum value (e.g. Chrome 125–131 rejecting
29
+ * `'active'`/`'passive'`). Such a browser only understands the legacy
30
+ * `'button'`/`'widget'` values, so the caller can retry with those.
31
+ */
32
+ function isUnknownModeEnumError(error) {
33
+ if (!(error instanceof TypeError))
34
+ return false;
35
+ const message = error.message.toLowerCase();
36
+ return (message.includes('identitycredentialrequestoptionsmode') ||
37
+ ((message.includes('active') || message.includes('passive')) &&
38
+ (message.includes('enum') || message.includes('not a valid'))));
39
+ }
8
40
  const FEDCM_LOGIN_HINT_KEY = 'oxy_fedcm_login_hint';
9
41
  // Global lock to prevent concurrent FedCM requests
10
42
  // FedCM only allows one navigator.credentials.get request at a time
@@ -94,15 +126,17 @@ function OxyServicesFedCMMixin(Base) {
94
126
  // Use provided loginHint, or fall back to stored last-used account ID
95
127
  const loginHint = options.loginHint || this.getStoredLoginHint();
96
128
  debug.log('Interactive sign-in: Requesting credential for', clientId, loginHint ? `(hint: ${loginHint})` : '');
97
- // Request credential from browser's native identity flow
98
- // mode: 'button' signals this is a user-gesture-initiated flow (Chrome 125+)
129
+ // Request credential from browser's native identity flow.
130
+ // mode: 'active' signals this is a user-gesture-initiated (button) flow.
131
+ // 'active' is the current W3C spec value; requestIdentityCredential
132
+ // transparently retries with the legacy 'button' value for Chrome 125–131.
99
133
  const credential = await this.requestIdentityCredential({
100
134
  configURL: this.resolveFedcmConfigUrl(),
101
135
  clientId,
102
136
  nonce,
103
137
  context: options.context,
104
138
  loginHint,
105
- mode: 'button',
139
+ mode: 'active',
106
140
  });
107
141
  if (!credential || !credential.token) {
108
142
  throw new OxyServices_errors_1.OxyAuthenticationError('No credential received from browser');
@@ -110,9 +144,11 @@ function OxyServicesFedCMMixin(Base) {
110
144
  debug.log('Interactive sign-in: Got credential, exchanging for session');
111
145
  // Exchange FedCM ID token for Oxy session
112
146
  const session = await this.exchangeIdTokenForSession(credential.token);
113
- // Store access token in HttpService (extract from response or get from session)
114
- if (session && session.accessToken) {
115
- this.httpService.setTokens(session.accessToken);
147
+ // Store access token in HttpService. `accessToken`/`refreshToken` are
148
+ // declared optional on SessionLoginResponse; default the refresh token to
149
+ // an empty string when the exchange did not return one.
150
+ if (session?.accessToken) {
151
+ this.httpService.setTokens(session.accessToken, session.refreshToken ?? '');
116
152
  }
117
153
  // Store the user ID as loginHint for future FedCM requests
118
154
  if (session?.user?.id) {
@@ -124,10 +160,13 @@ function OxyServicesFedCMMixin(Base) {
124
160
  catch (error) {
125
161
  debug.log('Interactive sign-in failed:', error);
126
162
  const errorMessage = error instanceof Error ? error.message : String(error);
127
- if (error.name === 'AbortError') {
163
+ // FedCM aborts/network failures surface as DOMException/Error instances,
164
+ // both of which carry a `name`. Anything else has no meaningful name.
165
+ const errorName = error instanceof Error ? error.name : '';
166
+ if (errorName === 'AbortError') {
128
167
  throw new OxyServices_errors_1.OxyAuthenticationError('Sign-in was cancelled by user');
129
168
  }
130
- if (error.name === 'NetworkError') {
169
+ if (errorName === 'NetworkError') {
131
170
  throw new OxyServices_errors_1.OxyAuthenticationError('Network error during sign-in. Please check your connection.');
132
171
  }
133
172
  if (errorMessage.includes('multiple accounts')) {
@@ -239,9 +278,11 @@ function OxyServicesFedCMMixin(Base) {
239
278
  debug.error('Silent SSO: Exchange returned session without user:', session);
240
279
  return null;
241
280
  }
242
- // Set the access token
281
+ // Set the access token. `accessToken`/`refreshToken` are declared optional
282
+ // on SessionLoginResponse; default the refresh token to an empty string when
283
+ // the exchange did not return one.
243
284
  if (session.accessToken) {
244
- this.httpService.setTokens(session.accessToken);
285
+ this.httpService.setTokens(session.accessToken, session.refreshToken ?? '');
245
286
  debug.log('Silent SSO: Access token set');
246
287
  }
247
288
  else {
@@ -310,37 +351,63 @@ function OxyServicesFedCMMixin(Base) {
310
351
  debug.log('Request timed out after', timeoutMs, 'ms (mediation:', requestedMediation + ')');
311
352
  controller.abort();
312
353
  }, timeoutMs);
354
+ // Normalise the caller's mode to the modern W3C value first. A modern
355
+ // browser accepts it; an older one (Chrome 125–131) rejects it with a
356
+ // synchronous TypeError, in which case we retry with the legacy value.
357
+ const modernMode = options.mode ? toModernMode(options.mode) : undefined;
358
+ // Build the identity request for a specific mode value. The `mode` field
359
+ // lives on the `identity` object (sibling of `providers`), separate from
360
+ // the top-level `mediation` field.
361
+ const buildCredentialOptions = (modeValue) => ({
362
+ identity: {
363
+ providers: [
364
+ {
365
+ configURL: options.configURL,
366
+ clientId: options.clientId,
367
+ // Older browsers read `nonce` at the top level; Chrome 145+
368
+ // expects it inside `params`. Send both for full coverage.
369
+ nonce: options.nonce,
370
+ params: {
371
+ nonce: options.nonce,
372
+ },
373
+ ...(options.loginHint && { loginHint: options.loginHint }),
374
+ },
375
+ ],
376
+ ...(modeValue && { mode: modeValue }),
377
+ },
378
+ mediation: requestedMediation,
379
+ signal: controller.signal,
380
+ });
381
+ // The DOM lib's `CredentialsContainer` does not declare the FedCM `identity`
382
+ // request in every TypeScript version we build against. Re-type through the
383
+ // minimal structural interface above (not `any`) to keep this typed.
384
+ const credentials = navigator.credentials;
313
385
  fedCMRequestPromise = (async () => {
314
386
  try {
315
- debug.log('Calling navigator.credentials.get with mediation:', requestedMediation);
316
- // Type assertion needed as FedCM types may not be in all TypeScript versions
317
- const credentialOptions = {
318
- identity: {
319
- providers: [
320
- {
321
- configURL: options.configURL,
322
- clientId: options.clientId,
323
- // Older browsers read `nonce` at the top level; Chrome 145+
324
- // expects it inside `params`. Send both for full coverage.
325
- nonce: options.nonce,
326
- params: {
327
- nonce: options.nonce,
328
- },
329
- ...(options.loginHint && { loginHint: options.loginHint }),
330
- },
331
- ],
332
- ...(options.mode && { mode: options.mode }),
333
- },
334
- mediation: requestedMediation,
335
- signal: controller.signal,
336
- };
337
- const credential = (await navigator.credentials.get(credentialOptions));
387
+ debug.log('Calling navigator.credentials.get with mediation:', requestedMediation, modernMode ? `mode: ${modernMode}` : '');
388
+ let credential;
389
+ try {
390
+ credential = await credentials.get(buildCredentialOptions(modernMode));
391
+ }
392
+ catch (modeError) {
393
+ // Chrome 125–131 only knows the legacy 'button'/'widget' enum and
394
+ // throws a synchronous TypeError for the modern 'active'/'passive'
395
+ // values. Retry once with the legacy value so older browsers work.
396
+ if (modernMode && isUnknownModeEnumError(modeError)) {
397
+ const legacyMode = MODERN_TO_LEGACY_MODE[modernMode];
398
+ debug.log(`Browser rejected modern mode '${modernMode}'; retrying with legacy mode '${legacyMode}'`);
399
+ credential = await credentials.get(buildCredentialOptions(legacyMode));
400
+ }
401
+ else {
402
+ throw modeError;
403
+ }
404
+ }
338
405
  debug.log('navigator.credentials.get returned:', {
339
406
  hasCredential: !!credential,
340
407
  type: credential?.type,
341
408
  hasToken: !!credential?.token,
342
409
  });
343
- if (!credential || credential.type !== 'identity') {
410
+ if (!credential || credential.type !== 'identity' || !credential.token) {
344
411
  debug.log('No valid identity credential returned');
345
412
  return null;
346
413
  }
@@ -401,9 +468,15 @@ function OxyServicesFedCMMixin(Base) {
401
468
  return;
402
469
  }
403
470
  try {
404
- if ('IdentityCredential' in window && 'disconnect' in window.IdentityCredential) {
471
+ // The DOM lib does not declare the global `IdentityCredential` interface
472
+ // object (with its static `disconnect`) in every TypeScript version we
473
+ // build against. Read it off `window` through the minimal structural type
474
+ // (not `any`), guarding that `disconnect` is actually present at runtime.
475
+ const fedCMWindow = window;
476
+ const identityCredential = fedCMWindow.IdentityCredential;
477
+ if (identityCredential && typeof identityCredential.disconnect === 'function') {
405
478
  const clientId = this.getClientId();
406
- await window.IdentityCredential.disconnect({
479
+ await identityCredential.disconnect({
407
480
  configURL: this.resolveFedcmConfigUrl(),
408
481
  clientId,
409
482
  accountHint: accountHint || '*',