@oxyhq/core 1.11.21 → 1.11.23

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.
Files changed (39) hide show
  1. package/README.md +6 -2
  2. package/dist/cjs/.tsbuildinfo +1 -1
  3. package/dist/cjs/HttpService.js +52 -0
  4. package/dist/cjs/OxyServices.base.js +16 -0
  5. package/dist/cjs/mixins/OxyServices.fedcm.js +0 -80
  6. package/dist/esm/.tsbuildinfo +1 -1
  7. package/dist/esm/HttpService.js +52 -0
  8. package/dist/esm/OxyServices.base.js +16 -0
  9. package/dist/esm/mixins/OxyServices.fedcm.js +0 -79
  10. package/dist/types/.tsbuildinfo +1 -1
  11. package/dist/types/HttpService.d.ts +31 -0
  12. package/dist/types/OxyServices.base.d.ts +14 -0
  13. package/dist/types/mixins/OxyServices.analytics.d.ts +1 -0
  14. package/dist/types/mixins/OxyServices.appData.d.ts +1 -0
  15. package/dist/types/mixins/OxyServices.assets.d.ts +1 -0
  16. package/dist/types/mixins/OxyServices.auth.d.ts +1 -0
  17. package/dist/types/mixins/OxyServices.contacts.d.ts +1 -0
  18. package/dist/types/mixins/OxyServices.developer.d.ts +1 -0
  19. package/dist/types/mixins/OxyServices.devices.d.ts +1 -0
  20. package/dist/types/mixins/OxyServices.features.d.ts +6 -1
  21. package/dist/types/mixins/OxyServices.fedcm.d.ts +1 -24
  22. package/dist/types/mixins/OxyServices.karma.d.ts +1 -0
  23. package/dist/types/mixins/OxyServices.language.d.ts +1 -0
  24. package/dist/types/mixins/OxyServices.location.d.ts +1 -0
  25. package/dist/types/mixins/OxyServices.managedAccounts.d.ts +1 -0
  26. package/dist/types/mixins/OxyServices.payment.d.ts +1 -0
  27. package/dist/types/mixins/OxyServices.popup.d.ts +1 -0
  28. package/dist/types/mixins/OxyServices.privacy.d.ts +1 -0
  29. package/dist/types/mixins/OxyServices.redirect.d.ts +1 -0
  30. package/dist/types/mixins/OxyServices.security.d.ts +1 -0
  31. package/dist/types/mixins/OxyServices.topics.d.ts +1 -0
  32. package/dist/types/mixins/OxyServices.user.d.ts +1 -0
  33. package/dist/types/mixins/OxyServices.utility.d.ts +1 -0
  34. package/package.json +1 -1
  35. package/src/HttpService.ts +53 -0
  36. package/src/OxyServices.base.ts +17 -0
  37. package/src/mixins/OxyServices.fedcm.ts +0 -83
  38. package/src/mixins/__tests__/fedcm.test.ts +0 -182
  39. package/src/mixins/__tests__/onTokensChanged.test.ts +130 -0
@@ -50,6 +50,16 @@ export declare class HttpService {
50
50
  private tokenRefreshPromise;
51
51
  private tokenRefreshCooldownUntil;
52
52
  private _onTokenRefreshed;
53
+ /**
54
+ * Fan-out listeners notified on EVERY access-token change on this instance:
55
+ * explicit `setTokens`, `clearTokens`, a successful silent refresh, and the
56
+ * internal 401-driven clear. Unlike the single-slot `_onTokenRefreshed`
57
+ * (owned by AuthManager for the refresh path only), this is a Set so multiple
58
+ * independent observers can mirror token state without clobbering each other.
59
+ *
60
+ * Each listener receives the resulting access token, or `null` when cleared.
61
+ */
62
+ private _tokenChangeListeners;
53
63
  private _actingAsUserId;
54
64
  private requestMetrics;
55
65
  constructor(config: OxyConfig);
@@ -134,6 +144,27 @@ export declare class HttpService {
134
144
  setTokens(accessToken: string, refreshToken?: string): void;
135
145
  set onTokenRefreshed(callback: ((accessToken: string) => void) | null);
136
146
  clearTokens(): void;
147
+ /**
148
+ * Subscribe to access-token changes on this instance.
149
+ *
150
+ * Fires on every mutation of the access token — `setTokens`, `clearTokens`,
151
+ * a successful silent refresh, and the internal 401-driven clear — passing
152
+ * the resulting token (or `null` when cleared). Returns an unsubscribe
153
+ * function; call it on teardown to avoid leaks.
154
+ *
155
+ * This is the single hook downstream code (e.g. @oxyhq/services' OxyProvider)
156
+ * uses to keep an external token sink — such as the shared `oxyClient`
157
+ * singleton — in lockstep with the active session, regardless of which code
158
+ * path mutated the token.
159
+ */
160
+ addTokenChangeListener(listener: (accessToken: string | null) => void): () => void;
161
+ /**
162
+ * Notify all token-change listeners with the current access token.
163
+ * Listener exceptions are isolated so one bad subscriber cannot break token
164
+ * propagation to the others or to the calling auth flow.
165
+ * @internal
166
+ */
167
+ private notifyTokenChange;
137
168
  getAccessToken(): string | null;
138
169
  hasAccessToken(): boolean;
139
170
  getBaseURL(): string;
@@ -73,6 +73,20 @@ export declare class OxyServicesBase {
73
73
  * Clear stored authentication tokens
74
74
  */
75
75
  clearTokens(): void;
76
+ /**
77
+ * Subscribe to access-token changes on this client.
78
+ *
79
+ * The listener fires on every access-token mutation — explicit
80
+ * `setTokens`/`clearTokens`, a successful silent refresh, and the internal
81
+ * 401-driven clear — receiving the resulting token, or `null` when cleared.
82
+ * Returns an unsubscribe function.
83
+ *
84
+ * Primary use: keeping an external token sink (e.g. the shared `oxyClient`
85
+ * singleton) in lockstep with whichever `OxyServices` instance actually owns
86
+ * the session, so imperative consumers reading the singleton always observe
87
+ * the live token regardless of the code path that changed it.
88
+ */
89
+ onTokensChanged(listener: (accessToken: string | null) => void): () => void;
76
90
  /** @internal */ _cachedUserId: string | null | undefined;
77
91
  /** @internal */ _cachedAccessToken: string | null;
78
92
  /**
@@ -46,6 +46,7 @@ export declare function OxyServicesAnalyticsMixin<T extends typeof OxyServicesBa
46
46
  getCloudURL(): string;
47
47
  setTokens(accessToken: string, refreshToken?: string): void;
48
48
  clearTokens(): void;
49
+ onTokensChanged(listener: (accessToken: string | null) => void): () => void;
49
50
  _cachedUserId: string | null | undefined;
50
51
  _cachedAccessToken: string | null;
51
52
  getCurrentUserId(): string | null;
@@ -82,6 +82,7 @@ export declare function OxyServicesAppDataMixin<T extends typeof OxyServicesBase
82
82
  getCloudURL(): string;
83
83
  setTokens(accessToken: string, refreshToken?: string): void;
84
84
  clearTokens(): void;
85
+ onTokensChanged(listener: (accessToken: string | null) => void): () => void;
85
86
  _cachedUserId: string | null | undefined;
86
87
  _cachedAccessToken: string | null;
87
88
  getCurrentUserId(): string | null;
@@ -119,6 +119,7 @@ export declare function OxyServicesAssetsMixin<T extends typeof OxyServicesBase>
119
119
  getCloudURL(): string;
120
120
  setTokens(accessToken: string, refreshToken?: string): void;
121
121
  clearTokens(): void;
122
+ onTokensChanged(listener: (accessToken: string | null) => void): () => void;
122
123
  _cachedUserId: string | null | undefined;
123
124
  _cachedAccessToken: string | null;
124
125
  getCurrentUserId(): string | null;
@@ -313,6 +313,7 @@ export declare function OxyServicesAuthMixin<T extends typeof OxyServicesBase>(B
313
313
  getCloudURL(): string;
314
314
  setTokens(accessToken: string, refreshToken?: string): void;
315
315
  clearTokens(): void;
316
+ onTokensChanged(listener: (accessToken: string | null) => void): () => void;
316
317
  _cachedUserId: string | null | undefined;
317
318
  _cachedAccessToken: string | null;
318
319
  getCurrentUserId(): string | null;
@@ -74,6 +74,7 @@ export declare function OxyServicesContactsMixin<T extends typeof OxyServicesBas
74
74
  getCloudURL(): string;
75
75
  setTokens(accessToken: string, refreshToken?: string): void;
76
76
  clearTokens(): void;
77
+ onTokensChanged(listener: (accessToken: string | null) => void): () => void;
77
78
  _cachedUserId: string | null | undefined;
78
79
  _cachedAccessToken: string | null;
79
80
  getCurrentUserId(): string | null;
@@ -79,6 +79,7 @@ export declare function OxyServicesDeveloperMixin<T extends typeof OxyServicesBa
79
79
  getCloudURL(): string;
80
80
  setTokens(accessToken: string, refreshToken?: string): void;
81
81
  clearTokens(): void;
82
+ onTokensChanged(listener: (accessToken: string | null) => void): () => void;
82
83
  _cachedUserId: string | null | undefined;
83
84
  _cachedAccessToken: string | null;
84
85
  getCurrentUserId(): string | null;
@@ -76,6 +76,7 @@ export declare function OxyServicesDevicesMixin<T extends typeof OxyServicesBase
76
76
  getCloudURL(): string;
77
77
  setTokens(accessToken: string, refreshToken?: string): void;
78
78
  clearTokens(): void;
79
+ onTokensChanged(listener: (accessToken: string | null) => void): () => void;
79
80
  _cachedUserId: string | null | undefined;
80
81
  _cachedAccessToken: string | null;
81
82
  getCurrentUserId(): string | null;
@@ -204,6 +204,7 @@ export declare function OxyServicesFeaturesMixin<T extends typeof OxyServicesBas
204
204
  getCloudURL(): string;
205
205
  setTokens(accessToken: string, refreshToken?: string): void;
206
206
  clearTokens(): void;
207
+ onTokensChanged(listener: (accessToken: string | null) => void): () => void;
207
208
  _cachedUserId: string | null | undefined;
208
209
  _cachedAccessToken: string | null;
209
210
  getCurrentUserId(): string | null;
@@ -215,7 +216,11 @@ export declare function OxyServicesFeaturesMixin<T extends typeof OxyServicesBas
215
216
  withAuthRetry<T_1>(operation: () => Promise<T_1>, operationName: string, options?: {
216
217
  maxRetries?: number;
217
218
  retryDelay?: number;
218
- authTimeoutMs?: number;
219
+ authTimeoutMs
220
+ /**
221
+ * Get user statistics
222
+ */
223
+ ?: number;
219
224
  }): Promise<T_1>;
220
225
  validate(): Promise<boolean>;
221
226
  handleError(error: unknown): Error;
@@ -29,14 +29,6 @@ interface FedCMTokenResult {
29
29
  token: string;
30
30
  isAutoSelected: boolean;
31
31
  }
32
- /**
33
- * Test-only reset of the page-load silent-SSO memo. The memo is module-scoped
34
- * and never cleared at runtime (a fresh page load resets it naturally), but
35
- * tests sharing one module instance need to start from a clean slate.
36
- *
37
- * @internal
38
- */
39
- export declare function __resetSilentSSOMemoForTests(): void;
40
32
  /**
41
33
  * Federated Credential Management (FedCM) Authentication Mixin
42
34
  *
@@ -121,22 +113,6 @@ export declare function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(
121
113
  * ```
122
114
  */
123
115
  silentSignInWithFedCM(): Promise<SessionLoginResponse | null>;
124
- /**
125
- * Build the page-load silent-SSO memo key from the current origin and the
126
- * configured API base URL. Two providers pointed at the same API from the
127
- * same origin share a single silent attempt per page load.
128
- *
129
- * @internal
130
- */
131
- silentSSOMemoKey(): string;
132
- /**
133
- * Perform the actual silent FedCM sign-in. Always wrapped by
134
- * {@link silentSignInWithFedCM}'s page-load memo — never call this directly
135
- * (doing so bypasses the run-once guard).
136
- *
137
- * @internal
138
- */
139
- _performSilentSignInWithFedCM(): Promise<SessionLoginResponse | null>;
140
116
  /**
141
117
  * Request identity credential from browser using FedCM API
142
118
  *
@@ -267,6 +243,7 @@ export declare function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(
267
243
  getCloudURL(): string;
268
244
  setTokens(accessToken: string, refreshToken?: string): void;
269
245
  clearTokens(): void;
246
+ onTokensChanged(listener: (accessToken: string | null) => void): () => void;
270
247
  _cachedUserId: string | null | undefined;
271
248
  _cachedAccessToken: string | null;
272
249
  getCurrentUserId(): string | null;
@@ -65,6 +65,7 @@ export declare function OxyServicesKarmaMixin<T extends typeof OxyServicesBase>(
65
65
  getCloudURL(): string;
66
66
  setTokens(accessToken: string, refreshToken?: string): void;
67
67
  clearTokens(): void;
68
+ onTokensChanged(listener: (accessToken: string | null) => void): () => void;
68
69
  _cachedUserId: string | null | undefined;
69
70
  _cachedAccessToken: string | null;
70
71
  getCurrentUserId(): string | null;
@@ -61,6 +61,7 @@ export declare function OxyServicesLanguageMixin<T extends typeof OxyServicesBas
61
61
  getCloudURL(): string;
62
62
  setTokens(accessToken: string, refreshToken?: string): void;
63
63
  clearTokens(): void;
64
+ onTokensChanged(listener: (accessToken: string | null) => void): () => void;
64
65
  _cachedUserId: string | null | undefined;
65
66
  _cachedAccessToken: string | null;
66
67
  getCurrentUserId(): string | null;
@@ -44,6 +44,7 @@ export declare function OxyServicesLocationMixin<T extends typeof OxyServicesBas
44
44
  getCloudURL(): string;
45
45
  setTokens(accessToken: string, refreshToken?: string): void;
46
46
  clearTokens(): void;
47
+ onTokensChanged(listener: (accessToken: string | null) => void): () => void;
47
48
  _cachedUserId: string | null | undefined;
48
49
  _cachedAccessToken: string | null;
49
50
  getCurrentUserId(): string | null;
@@ -101,6 +101,7 @@ export declare function OxyServicesManagedAccountsMixin<T extends typeof OxyServ
101
101
  getCloudURL(): string;
102
102
  setTokens(accessToken: string, refreshToken?: string): void;
103
103
  clearTokens(): void;
104
+ onTokensChanged(listener: (accessToken: string | null) => void): () => void;
104
105
  _cachedUserId: string | null | undefined;
105
106
  _cachedAccessToken: string | null;
106
107
  getCurrentUserId(): string | null;
@@ -91,6 +91,7 @@ export declare function OxyServicesPaymentMixin<T extends typeof OxyServicesBase
91
91
  getCloudURL(): string;
92
92
  setTokens(accessToken: string, refreshToken?: string): void;
93
93
  clearTokens(): void;
94
+ onTokensChanged(listener: (accessToken: string | null) => void): () => void;
94
95
  _cachedUserId: string | null | undefined;
95
96
  _cachedAccessToken: string | null;
96
97
  getCurrentUserId(): string | null;
@@ -181,6 +181,7 @@ export declare function OxyServicesPopupAuthMixin<T extends typeof OxyServicesBa
181
181
  getCloudURL(): string;
182
182
  setTokens(accessToken: string, refreshToken?: string): void;
183
183
  clearTokens(): void;
184
+ onTokensChanged(listener: (accessToken: string | null) => void): () => void;
184
185
  _cachedUserId: string | null | undefined;
185
186
  _cachedAccessToken: string | null;
186
187
  getCurrentUserId(): string | null;
@@ -102,6 +102,7 @@ export declare function OxyServicesPrivacyMixin<T extends typeof OxyServicesBase
102
102
  getCloudURL(): string;
103
103
  setTokens(accessToken: string, refreshToken?: string): void;
104
104
  clearTokens(): void;
105
+ onTokensChanged(listener: (accessToken: string | null) => void): () => void;
105
106
  _cachedUserId: string | null | undefined;
106
107
  _cachedAccessToken: string | null;
107
108
  getCurrentUserId(): string | null;
@@ -218,6 +218,7 @@ export declare function OxyServicesRedirectAuthMixin<T extends typeof OxyService
218
218
  getCloudURL(): string;
219
219
  setTokens(accessToken: string, refreshToken?: string): void;
220
220
  clearTokens(): void;
221
+ onTokensChanged(listener: (accessToken: string | null) => void): () => void;
221
222
  _cachedUserId: string | null | undefined;
222
223
  _cachedAccessToken: string | null;
223
224
  getCurrentUserId(): string | null;
@@ -58,6 +58,7 @@ export declare function OxyServicesSecurityMixin<T extends typeof OxyServicesBas
58
58
  getCloudURL(): string;
59
59
  setTokens(accessToken: string, refreshToken?: string): void;
60
60
  clearTokens(): void;
61
+ onTokensChanged(listener: (accessToken: string | null) => void): () => void;
61
62
  _cachedUserId: string | null | undefined;
62
63
  _cachedAccessToken: string | null;
63
64
  getCurrentUserId(): string | null;
@@ -84,6 +84,7 @@ export declare function OxyServicesTopicsMixin<T extends typeof OxyServicesBase>
84
84
  getCloudURL(): string;
85
85
  setTokens(accessToken: string, refreshToken?: string): void;
86
86
  clearTokens(): void;
87
+ onTokensChanged(listener: (accessToken: string | null) => void): () => void;
87
88
  _cachedUserId: string | null | undefined;
88
89
  _cachedAccessToken: string | null;
89
90
  getCurrentUserId(): string | null;
@@ -229,6 +229,7 @@ export declare function OxyServicesUserMixin<T extends typeof OxyServicesBase>(B
229
229
  getCloudURL(): string;
230
230
  setTokens(accessToken: string, refreshToken?: string): void;
231
231
  clearTokens(): void;
232
+ onTokensChanged(listener: (accessToken: string | null) => void): () => void;
232
233
  _cachedUserId: string | null | undefined;
233
234
  _cachedAccessToken: string | null;
234
235
  getCurrentUserId(): string | null;
@@ -267,6 +267,7 @@ export declare function OxyServicesUtilityMixin<T extends typeof OxyServicesBase
267
267
  getCloudURL(): string;
268
268
  setTokens(accessToken: string, refreshToken?: string): void;
269
269
  clearTokens(): void;
270
+ onTokensChanged(listener: (accessToken: string | null) => void): () => void;
270
271
  _cachedUserId: string | null | undefined;
271
272
  _cachedAccessToken: string | null;
272
273
  getCurrentUserId(): string | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/core",
3
- "version": "1.11.21",
3
+ "version": "1.11.23",
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",
@@ -176,6 +176,17 @@ export class HttpService {
176
176
  private tokenRefreshCooldownUntil: number = 0;
177
177
  private _onTokenRefreshed: ((accessToken: string) => void) | null = null;
178
178
 
179
+ /**
180
+ * Fan-out listeners notified on EVERY access-token change on this instance:
181
+ * explicit `setTokens`, `clearTokens`, a successful silent refresh, and the
182
+ * internal 401-driven clear. Unlike the single-slot `_onTokenRefreshed`
183
+ * (owned by AuthManager for the refresh path only), this is a Set so multiple
184
+ * independent observers can mirror token state without clobbering each other.
185
+ *
186
+ * Each listener receives the resulting access token, or `null` when cleared.
187
+ */
188
+ private _tokenChangeListeners = new Set<(accessToken: string | null) => void>();
189
+
179
190
  // Acting-as identity for managed accounts
180
191
  private _actingAsUserId: string | null = null;
181
192
 
@@ -430,6 +441,7 @@ export class HttpService {
430
441
  // Refresh failed or no token — clear tokens and stale CSRF
431
442
  this.tokenStore.clearTokens();
432
443
  this.tokenStore.clearCsrfToken();
444
+ this.notifyTokenChange();
433
445
  }
434
446
 
435
447
  // On 403 with CSRF error, clear cached token and retry once
@@ -844,6 +856,7 @@ export class HttpService {
844
856
  const { accessToken: newToken } = await response.json();
845
857
  this.tokenStore.setTokens(newToken);
846
858
  this._onTokenRefreshed?.(newToken);
859
+ this.notifyTokenChange();
847
860
  this.logger.debug('Token refreshed');
848
861
  return `Bearer ${newToken}`;
849
862
  }
@@ -920,6 +933,7 @@ export class HttpService {
920
933
  // Token management
921
934
  setTokens(accessToken: string, refreshToken = ''): void {
922
935
  this.tokenStore.setTokens(accessToken, refreshToken);
936
+ this.notifyTokenChange();
923
937
  }
924
938
 
925
939
  set onTokenRefreshed(callback: ((accessToken: string) => void) | null) {
@@ -929,6 +943,45 @@ export class HttpService {
929
943
  clearTokens(): void {
930
944
  this.tokenStore.clearTokens();
931
945
  this.tokenStore.clearCsrfToken();
946
+ this.notifyTokenChange();
947
+ }
948
+
949
+ /**
950
+ * Subscribe to access-token changes on this instance.
951
+ *
952
+ * Fires on every mutation of the access token — `setTokens`, `clearTokens`,
953
+ * a successful silent refresh, and the internal 401-driven clear — passing
954
+ * the resulting token (or `null` when cleared). Returns an unsubscribe
955
+ * function; call it on teardown to avoid leaks.
956
+ *
957
+ * This is the single hook downstream code (e.g. @oxyhq/services' OxyProvider)
958
+ * uses to keep an external token sink — such as the shared `oxyClient`
959
+ * singleton — in lockstep with the active session, regardless of which code
960
+ * path mutated the token.
961
+ */
962
+ addTokenChangeListener(listener: (accessToken: string | null) => void): () => void {
963
+ this._tokenChangeListeners.add(listener);
964
+ return () => {
965
+ this._tokenChangeListeners.delete(listener);
966
+ };
967
+ }
968
+
969
+ /**
970
+ * Notify all token-change listeners with the current access token.
971
+ * Listener exceptions are isolated so one bad subscriber cannot break token
972
+ * propagation to the others or to the calling auth flow.
973
+ * @internal
974
+ */
975
+ private notifyTokenChange(): void {
976
+ if (this._tokenChangeListeners.size === 0) return;
977
+ const accessToken = this.tokenStore.getAccessToken();
978
+ for (const listener of this._tokenChangeListeners) {
979
+ try {
980
+ listener(accessToken);
981
+ } catch (error) {
982
+ this.logger.error('Token change listener threw:', error);
983
+ }
984
+ }
932
985
  }
933
986
 
934
987
  getAccessToken(): string | null {
@@ -146,6 +146,23 @@ export class OxyServicesBase {
146
146
  this._cachedAccessToken = null;
147
147
  }
148
148
 
149
+ /**
150
+ * Subscribe to access-token changes on this client.
151
+ *
152
+ * The listener fires on every access-token mutation — explicit
153
+ * `setTokens`/`clearTokens`, a successful silent refresh, and the internal
154
+ * 401-driven clear — receiving the resulting token, or `null` when cleared.
155
+ * Returns an unsubscribe function.
156
+ *
157
+ * Primary use: keeping an external token sink (e.g. the shared `oxyClient`
158
+ * singleton) in lockstep with whichever `OxyServices` instance actually owns
159
+ * the session, so imperative consumers reading the singleton always observe
160
+ * the live token regardless of the code path that changed it.
161
+ */
162
+ public onTokensChanged(listener: (accessToken: string | null) => void): () => void {
163
+ return this.httpService.addTokenChangeListener(listener);
164
+ }
165
+
149
166
  /** @internal */ _cachedUserId: string | null | undefined = undefined;
150
167
  /** @internal */ _cachedAccessToken: string | null = null;
151
168
 
@@ -130,46 +130,6 @@ let fedCMRequestInProgress = false;
130
130
  let fedCMRequestPromise: Promise<FedCMTokenResult | null> | null = null;
131
131
  let currentMediationMode: string | null = null;
132
132
 
133
- /**
134
- * Page-load-persistent memo for SILENT FedCM sign-in.
135
- *
136
- * Silent SSO (`mediation: 'silent'`) is the one FedCM flow that runs WITHOUT a
137
- * user gesture — on app startup / provider mount. Multiple consumers
138
- * (`@oxyhq/auth`'s `WebOxyProvider` / `useWebSSO`, `@oxyhq/services`'
139
- * `useWebSSO`) can each mount and trigger it, and a remount storm (route churn,
140
- * React StrictMode double-invoke, error-boundary recovery) previously turned
141
- * into a `navigator.credentials.get` storm. This memo collapses every silent
142
- * attempt for a given `origin + baseURL` into AT MOST ONE browser credential
143
- * request per page load:
144
- *
145
- * - the FIRST silent call runs the real flow and stores its in-flight promise;
146
- * - concurrent silent calls share that same in-flight promise;
147
- * - once it settles, the memo retains the resolved value (a session OR `null`)
148
- * and every subsequent silent call returns it WITHOUT re-invoking the
149
- * browser.
150
- *
151
- * Keyed on `origin + baseURL` (not the OxyServices instance) so it survives
152
- * instance churn across remounts. Intentionally never cleared: only a fresh
153
- * page load — which starts a fresh module scope — can change the IdP session
154
- * state that silent mediation observes.
155
- *
156
- * This guard is SILENT-ONLY. Interactive flows (`signInWithFedCM`,
157
- * `mediation: 'optional'|'required'`, `mode: 'active'|'passive'`) must always
158
- * be able to re-prompt and are never memoized here.
159
- */
160
- const silentSSOMemo = new Map<string, Promise<SessionLoginResponse | null>>();
161
-
162
- /**
163
- * Test-only reset of the page-load silent-SSO memo. The memo is module-scoped
164
- * and never cleared at runtime (a fresh page load resets it naturally), but
165
- * tests sharing one module instance need to start from a clean slate.
166
- *
167
- * @internal
168
- */
169
- export function __resetSilentSSOMemoForTests(): void {
170
- silentSSOMemo.clear();
171
- }
172
-
173
133
  /**
174
134
  * Federated Credential Management (FedCM) Authentication Mixin
175
135
  *
@@ -362,49 +322,6 @@ export function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(Base: T)
362
322
  return null;
363
323
  }
364
324
 
365
- // Page-load run-once guard. The first silent attempt for this
366
- // origin + API runs; concurrent callers share the in-flight promise; once
367
- // it settles, every later caller gets the memoized result (session OR
368
- // null) WITHOUT re-invoking `navigator.credentials.get`. This is the single
369
- // chokepoint for silent SSO across all consumers and remounts.
370
- const memoKey = this.silentSSOMemoKey();
371
- const existing = silentSSOMemo.get(memoKey);
372
- if (existing) {
373
- debug.log('Silent SSO: Returning memoized page-load result (no re-invocation)');
374
- return existing;
375
- }
376
-
377
- const attempt = this._performSilentSignInWithFedCM();
378
- silentSSOMemo.set(memoKey, attempt);
379
- return attempt;
380
- }
381
-
382
- /**
383
- * Build the page-load silent-SSO memo key from the current origin and the
384
- * configured API base URL. Two providers pointed at the same API from the
385
- * same origin share a single silent attempt per page load.
386
- *
387
- * @internal
388
- */
389
- public silentSSOMemoKey(): string {
390
- const origin = typeof window !== 'undefined' ? window.location.origin : 'no-origin';
391
- let baseURL = '';
392
- try {
393
- baseURL = this.getBaseURL();
394
- } catch {
395
- baseURL = '';
396
- }
397
- return `${origin}|${baseURL}`;
398
- }
399
-
400
- /**
401
- * Perform the actual silent FedCM sign-in. Always wrapped by
402
- * {@link silentSignInWithFedCM}'s page-load memo — never call this directly
403
- * (doing so bypasses the run-once guard).
404
- *
405
- * @internal
406
- */
407
- public async _performSilentSignInWithFedCM(): Promise<SessionLoginResponse | null> {
408
325
  const clientId = this.getClientId();
409
326
  debug.log('Silent SSO: Starting for', clientId);
410
327