@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.
- package/README.md +6 -2
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/HttpService.js +52 -0
- package/dist/cjs/OxyServices.base.js +16 -0
- package/dist/cjs/mixins/OxyServices.fedcm.js +0 -80
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/HttpService.js +52 -0
- package/dist/esm/OxyServices.base.js +16 -0
- package/dist/esm/mixins/OxyServices.fedcm.js +0 -79
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/HttpService.d.ts +31 -0
- package/dist/types/OxyServices.base.d.ts +14 -0
- package/dist/types/mixins/OxyServices.analytics.d.ts +1 -0
- package/dist/types/mixins/OxyServices.appData.d.ts +1 -0
- package/dist/types/mixins/OxyServices.assets.d.ts +1 -0
- package/dist/types/mixins/OxyServices.auth.d.ts +1 -0
- package/dist/types/mixins/OxyServices.contacts.d.ts +1 -0
- package/dist/types/mixins/OxyServices.developer.d.ts +1 -0
- package/dist/types/mixins/OxyServices.devices.d.ts +1 -0
- package/dist/types/mixins/OxyServices.features.d.ts +6 -1
- package/dist/types/mixins/OxyServices.fedcm.d.ts +1 -24
- package/dist/types/mixins/OxyServices.karma.d.ts +1 -0
- package/dist/types/mixins/OxyServices.language.d.ts +1 -0
- package/dist/types/mixins/OxyServices.location.d.ts +1 -0
- package/dist/types/mixins/OxyServices.managedAccounts.d.ts +1 -0
- package/dist/types/mixins/OxyServices.payment.d.ts +1 -0
- package/dist/types/mixins/OxyServices.popup.d.ts +1 -0
- package/dist/types/mixins/OxyServices.privacy.d.ts +1 -0
- package/dist/types/mixins/OxyServices.redirect.d.ts +1 -0
- package/dist/types/mixins/OxyServices.security.d.ts +1 -0
- package/dist/types/mixins/OxyServices.topics.d.ts +1 -0
- package/dist/types/mixins/OxyServices.user.d.ts +1 -0
- package/dist/types/mixins/OxyServices.utility.d.ts +1 -0
- package/package.json +1 -1
- package/src/HttpService.ts +53 -0
- package/src/OxyServices.base.ts +17 -0
- package/src/mixins/OxyServices.fedcm.ts +0 -83
- package/src/mixins/__tests__/fedcm.test.ts +0 -182
- 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
|
|
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
package/src/HttpService.ts
CHANGED
|
@@ -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 {
|
package/src/OxyServices.base.ts
CHANGED
|
@@ -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
|
|