@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.
@@ -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);
@@ -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 `Bearer ${accessToken}`;
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 credential = (await navigator.credentials.get({
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
- debug.log('Got valid identity credential with token');
333
- return { token: credential.token };
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
- * This tells the browser to forget the FedCM credential for this app.
377
- * The user will need to re-authenticate next time.
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
- // FedCM logout API (if available)
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.logout({
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
- // Silent failure
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
@@ -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);
@@ -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 `Bearer ${accessToken}`;
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 credential = (await navigator.credentials.get({
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
- debug.log('Got valid identity credential with token');
329
- return { token: credential.token };
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
- * This tells the browser to forget the FedCM credential for this app.
373
- * The user will need to re-authenticate next time.
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
- // FedCM logout API (if available)
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.logout({
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
- // Silent failure
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
- * This tells the browser to forget the FedCM credential for this app.
129
- * The user will need to re-authenticate next time.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/core",
3
- "version": "1.6.6",
3
+ "version": "1.8.0",
4
4
  "description": "OxyHQ SDK Foundation — API client, authentication, cryptographic identity, and shared utilities",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -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 {
@@ -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 `Bearer ${accessToken}`;
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
- }): Promise<{ token: string } | null> {
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 credential = (await (navigator.credentials as any).get({
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
- })) as any;
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
- debug.log('Got valid identity credential with token');
390
- return { token: credential.token };
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
- * This tells the browser to forget the FedCM credential for this app.
442
- * The user will need to re-authenticate next time.
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
- // FedCM logout API (if available)
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.logout({
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
- // Silent failure
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