@oxyhq/core 1.11.22 → 1.11.24

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 (41) hide show
  1. package/README.md +2 -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 +169 -73
  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 +169 -73
  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/OxyServices.d.ts +9 -0
  14. package/dist/types/mixins/OxyServices.analytics.d.ts +1 -0
  15. package/dist/types/mixins/OxyServices.appData.d.ts +1 -0
  16. package/dist/types/mixins/OxyServices.assets.d.ts +1 -0
  17. package/dist/types/mixins/OxyServices.auth.d.ts +1 -0
  18. package/dist/types/mixins/OxyServices.contacts.d.ts +1 -0
  19. package/dist/types/mixins/OxyServices.developer.d.ts +1 -0
  20. package/dist/types/mixins/OxyServices.devices.d.ts +1 -0
  21. package/dist/types/mixins/OxyServices.features.d.ts +6 -1
  22. package/dist/types/mixins/OxyServices.fedcm.d.ts +20 -1
  23. package/dist/types/mixins/OxyServices.karma.d.ts +1 -0
  24. package/dist/types/mixins/OxyServices.language.d.ts +1 -0
  25. package/dist/types/mixins/OxyServices.location.d.ts +1 -0
  26. package/dist/types/mixins/OxyServices.managedAccounts.d.ts +1 -0
  27. package/dist/types/mixins/OxyServices.payment.d.ts +1 -0
  28. package/dist/types/mixins/OxyServices.popup.d.ts +1 -0
  29. package/dist/types/mixins/OxyServices.privacy.d.ts +1 -0
  30. package/dist/types/mixins/OxyServices.redirect.d.ts +1 -0
  31. package/dist/types/mixins/OxyServices.security.d.ts +1 -0
  32. package/dist/types/mixins/OxyServices.topics.d.ts +1 -0
  33. package/dist/types/mixins/OxyServices.user.d.ts +1 -0
  34. package/dist/types/mixins/OxyServices.utility.d.ts +1 -0
  35. package/package.json +1 -1
  36. package/src/HttpService.ts +53 -0
  37. package/src/OxyServices.base.ts +17 -0
  38. package/src/OxyServices.ts +10 -0
  39. package/src/mixins/OxyServices.fedcm.ts +187 -78
  40. package/src/mixins/__tests__/fedcm.test.ts +231 -0
  41. 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
  /**
@@ -98,6 +98,15 @@ import { composeOxyServices } from './mixins';
98
98
  */
99
99
  declare const OxyServicesComposed: import("./mixins").ComposedOxyServicesConstructor;
100
100
  export declare class OxyServices extends OxyServicesComposed {
101
+ /**
102
+ * FedCM credential-request timeouts (ms). The runtime values are defined on
103
+ * the FedCM mixin and inherited here via `extends`; these `declare` members
104
+ * surface their types to TypeScript without re-emitting (or duplicating) the
105
+ * literals, so consumers/tests can reference `OxyServices.FEDCM_SILENT_TIMEOUT`
106
+ * with full typing.
107
+ */
108
+ static readonly FEDCM_TIMEOUT: number;
109
+ static readonly FEDCM_SILENT_TIMEOUT: number;
101
110
  constructor(config: OxyConfig);
102
111
  }
103
112
  export interface OxyServices extends InstanceType<ReturnType<typeof composeOxyServices>> {
@@ -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;
@@ -82,6 +82,24 @@ export declare function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(
82
82
  * ```
83
83
  */
84
84
  signInWithFedCM(options?: FedCMAuthOptions): Promise<SessionLoginResponse>;
85
+ /**
86
+ * Run a single interactive FedCM credential request + token exchange for the
87
+ * given (possibly undefined) loginHint. A successful exchange plants the
88
+ * access token and persists the user id as the future loginHint — the hint is
89
+ * therefore only ever stored after a GENUINELY successful sign-in, never
90
+ * speculatively.
91
+ *
92
+ * @private
93
+ */
94
+ attemptInteractiveSignIn(options: FedCMAuthOptions, loginHint: string | undefined): Promise<SessionLoginResponse>;
95
+ /**
96
+ * Map a raw FedCM/exchange failure to a user-facing {@link OxyAuthenticationError}
97
+ * (or pass it through). Extracted so the clear-and-retry path can reuse the
98
+ * exact same error normalisation as the first attempt.
99
+ *
100
+ * @private
101
+ */
102
+ normalizeInteractiveSignInError(error: unknown): unknown;
85
103
  /**
86
104
  * Silent sign-in using FedCM
87
105
  *
@@ -243,6 +261,7 @@ export declare function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(
243
261
  getCloudURL(): string;
244
262
  setTokens(accessToken: string, refreshToken?: string): void;
245
263
  clearTokens(): void;
264
+ onTokensChanged(listener: (accessToken: string | null) => void): () => void;
246
265
  _cachedUserId: string | null | undefined;
247
266
  _cachedAccessToken: string | null;
248
267
  getCurrentUserId(): string | null;
@@ -267,7 +286,7 @@ export declare function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(
267
286
  };
268
287
  readonly DEFAULT_CONFIG_URL: "https://auth.oxy.so/fedcm.json";
269
288
  readonly FEDCM_TIMEOUT: 15000;
270
- readonly FEDCM_SILENT_TIMEOUT: 3000;
289
+ readonly FEDCM_SILENT_TIMEOUT: 10000;
271
290
  /**
272
291
  * Check if FedCM is supported in the current browser
273
292
  */
@@ -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.22",
3
+ "version": "1.11.24",
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
 
@@ -109,6 +109,16 @@ const OxyServicesComposed = composeOxyServices();
109
109
  // We extend the composed constructor directly — its public surface is broadened
110
110
  // to the full mixin set via the interface declaration that follows.
111
111
  export class OxyServices extends OxyServicesComposed {
112
+ /**
113
+ * FedCM credential-request timeouts (ms). The runtime values are defined on
114
+ * the FedCM mixin and inherited here via `extends`; these `declare` members
115
+ * surface their types to TypeScript without re-emitting (or duplicating) the
116
+ * literals, so consumers/tests can reference `OxyServices.FEDCM_SILENT_TIMEOUT`
117
+ * with full typing.
118
+ */
119
+ declare static readonly FEDCM_TIMEOUT: number;
120
+ declare static readonly FEDCM_SILENT_TIMEOUT: number;
121
+
112
122
  constructor(config: OxyConfig) {
113
123
  super(config);
114
124
  }