@xh/hoist 72.1.0 → 72.3.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.
- package/CHANGELOG.md +44 -0
- package/admin/tabs/activity/clienterrors/ClientErrorsModel.ts +23 -4
- package/admin/tabs/cluster/instances/InstancesTabModel.ts +4 -3
- package/admin/tabs/cluster/instances/logs/LogDisplay.ts +12 -14
- package/admin/tabs/cluster/instances/logs/LogDisplayModel.ts +0 -2
- package/admin/tabs/cluster/instances/logs/LogViewer.ts +6 -5
- package/admin/tabs/cluster/instances/logs/LogViewerModel.ts +8 -1
- package/admin/tabs/cluster/instances/memory/MemoryMonitorModel.ts +1 -0
- package/admin/tabs/cluster/instances/services/DetailsModel.ts +1 -2
- package/admin/tabs/cluster/instances/services/DetailsPanel.ts +19 -14
- package/admin/tabs/cluster/instances/services/ServiceModel.ts +14 -6
- package/admin/tabs/cluster/instances/services/ServicePanel.ts +9 -10
- package/admin/tabs/cluster/instances/websocket/WebSocketColumns.ts +9 -0
- package/admin/tabs/cluster/instances/websocket/WebSocketModel.ts +2 -1
- package/admin/tabs/userData/roles/RoleModel.ts +1 -1
- package/admin/tabs/userData/roles/details/RoleDetailsModel.ts +2 -1
- package/admin/tabs/userData/roles/recategorize/RecategorizeDialogModel.ts +1 -1
- package/appcontainer/AppStateModel.ts +6 -1
- package/build/types/admin/tabs/activity/tracking/ActivityTrackingModel.d.ts +4 -1
- package/build/types/admin/tabs/cluster/instances/services/DetailsModel.d.ts +2 -3
- package/build/types/admin/tabs/cluster/instances/websocket/WebSocketColumns.d.ts +1 -0
- package/build/types/appcontainer/AppStateModel.d.ts +5 -1
- package/build/types/cmp/tab/TabContainerModel.d.ts +5 -5
- package/build/types/core/HoistProps.d.ts +1 -0
- package/build/types/core/XH.d.ts +5 -5
- package/build/types/core/types/Interfaces.d.ts +9 -0
- package/build/types/desktop/cmp/appOption/AutoRefreshAppOption.d.ts +1 -0
- package/build/types/desktop/cmp/appOption/ThemeAppOption.d.ts +1 -0
- package/build/types/desktop/cmp/tab/TabSwitcher.d.ts +1 -1
- package/build/types/kit/blueprint/Wrappers.d.ts +1 -1
- package/build/types/kit/swiper/index.d.ts +4 -4
- package/build/types/security/BaseOAuthClient.d.ts +25 -28
- package/build/types/security/Token.d.ts +0 -1
- package/build/types/security/Types.d.ts +39 -0
- package/build/types/security/authzero/AuthZeroClient.d.ts +3 -4
- package/build/types/security/msal/MsalClient.d.ts +14 -4
- package/build/types/svc/TrackService.d.ts +31 -1
- package/build/types/utils/js/BrowserUtils.d.ts +38 -1
- package/cmp/tab/TabContainerModel.ts +5 -5
- package/core/HoistProps.ts +1 -0
- package/core/XH.ts +13 -5
- package/core/exception/Exception.ts +19 -12
- package/core/types/Interfaces.ts +11 -0
- package/data/Store.ts +3 -0
- package/desktop/appcontainer/ExceptionDialog.ts +1 -1
- package/desktop/cmp/dash/canvas/DashCanvas.ts +2 -1
- package/desktop/cmp/grid/editors/BooleanEditor.ts +15 -3
- package/desktop/cmp/tab/TabSwitcher.ts +1 -1
- package/package.json +2 -2
- package/security/BaseOAuthClient.ts +52 -45
- package/security/Token.ts +0 -2
- package/security/Types.ts +51 -0
- package/security/authzero/AuthZeroClient.ts +6 -8
- package/security/msal/MsalClient.ts +130 -27
- package/svc/FetchService.ts +3 -2
- package/svc/TrackService.ts +94 -8
- package/svc/WebSocketService.ts +1 -2
- package/tsconfig.tsbuildinfo +1 -1
- package/utils/js/BrowserUtils.ts +72 -21
- package/utils/react/LayoutPropUtils.ts +2 -1
|
@@ -49,11 +49,23 @@ export const [BooleanEditor, booleanEditor] = hoistCmp.withFactory<BooleanEditor
|
|
|
49
49
|
}
|
|
50
50
|
});
|
|
51
51
|
|
|
52
|
-
function useInstantEditor(
|
|
52
|
+
function useInstantEditor(
|
|
53
|
+
{onValueChange, initialValue, stopEditing, eventKey, eGridCell}: CustomCellEditorProps,
|
|
54
|
+
ref
|
|
55
|
+
) {
|
|
56
|
+
// Don't toggle if the user has tabbed into the editor. See https://github.com/xh/hoist-react/issues/3943.
|
|
57
|
+
// Fortunately, `eventKey` is null for tab, so we can use that to accept other keyboard events.
|
|
58
|
+
// Unfortunately, it is also null for mouse events, so we check if the grid cell is currently
|
|
59
|
+
// underneath the mouse position via `:hover` selector.
|
|
53
60
|
useEffect(() => {
|
|
54
|
-
|
|
61
|
+
const els = document.querySelectorAll(':hover'),
|
|
62
|
+
topEl = els[els.length - 1];
|
|
63
|
+
|
|
64
|
+
if (eventKey || topEl === eGridCell) {
|
|
65
|
+
onValueChange(!initialValue);
|
|
66
|
+
}
|
|
55
67
|
stopEditing();
|
|
56
|
-
}, [stopEditing, initialValue, onValueChange]);
|
|
68
|
+
}, [stopEditing, initialValue, onValueChange, eventKey, eGridCell]);
|
|
57
69
|
|
|
58
70
|
return null;
|
|
59
71
|
}
|
|
@@ -41,7 +41,7 @@ import {CSSProperties, ReactElement, KeyboardEvent} from 'react';
|
|
|
41
41
|
*
|
|
42
42
|
* Overflowing tabs can be displayed in a dropdown menu if `enableOverflow` is true.
|
|
43
43
|
* Note that in order for tabs to overflow, the TabSwitcher or it's wrapper must have a
|
|
44
|
-
*
|
|
44
|
+
* maximum width.
|
|
45
45
|
*/
|
|
46
46
|
export const [TabSwitcher, tabSwitcher] = hoistCmp.withFactory<TabSwitcherProps>({
|
|
47
47
|
displayName: 'TabSwitcher',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xh/hoist",
|
|
3
|
-
"version": "72.
|
|
3
|
+
"version": "72.3.0",
|
|
4
4
|
"description": "Hoist add-on for building and deploying React Applications.",
|
|
5
5
|
"repository": "github:xh/hoist-react",
|
|
6
6
|
"homepage": "https://xh.io",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@auth0/auth0-spa-js": "~2.1.3",
|
|
33
|
-
"@azure/msal-browser": "~
|
|
33
|
+
"@azure/msal-browser": "~4.8.0",
|
|
34
34
|
"@blueprintjs/core": "^5.10.5",
|
|
35
35
|
"@blueprintjs/datetime": "^5.3.7",
|
|
36
36
|
"@blueprintjs/datetime2": "^2.3.7",
|
|
@@ -9,16 +9,17 @@ import {HoistBase, managed, XH} from '@xh/hoist/core';
|
|
|
9
9
|
import {Icon} from '@xh/hoist/icon';
|
|
10
10
|
import {action, makeObservable} from '@xh/hoist/mobx';
|
|
11
11
|
import {never, wait} from '@xh/hoist/promise';
|
|
12
|
-
import {Token
|
|
12
|
+
import {Token} from '@xh/hoist/security/Token';
|
|
13
|
+
import {AccessTokenSpec, TokenMap} from './Types';
|
|
13
14
|
import {Timer} from '@xh/hoist/utils/async';
|
|
14
15
|
import {MINUTES, olderThan, ONE_MINUTE, SECONDS} from '@xh/hoist/utils/datetime';
|
|
15
16
|
import {isJSON, throwIf} from '@xh/hoist/utils/js';
|
|
16
|
-
import {find, forEach, isEmpty, isObject, keys, pickBy, union} from 'lodash';
|
|
17
|
+
import {find, forEach, isEmpty, isObject, keys, map, pickBy, union} from 'lodash';
|
|
17
18
|
import ShortUniqueId from 'short-unique-id';
|
|
18
19
|
|
|
19
20
|
export type LoginMethod = 'REDIRECT' | 'POPUP';
|
|
20
21
|
|
|
21
|
-
export interface BaseOAuthClientConfig<S> {
|
|
22
|
+
export interface BaseOAuthClientConfig<S extends AccessTokenSpec> {
|
|
22
23
|
/** Client ID (GUID) of your app registered with your Oauth provider. */
|
|
23
24
|
clientId: string;
|
|
24
25
|
|
|
@@ -45,26 +46,17 @@ export interface BaseOAuthClientConfig<S> {
|
|
|
45
46
|
* Governs an optional refresh timer that will work to keep the tokens fresh.
|
|
46
47
|
*
|
|
47
48
|
* A typical refresh will use the underlying provider cache, and should not result in
|
|
48
|
-
* network activity. However, if any token
|
|
49
|
+
* network activity. However, if any token would expire before the next autoRefresh,
|
|
49
50
|
* this client will force a call to the underlying provider to get the token.
|
|
50
51
|
*
|
|
51
52
|
* In order to allow aging tokens to be replaced in a timely manner, this value should be
|
|
52
53
|
* significantly shorter than both the minimum token lifetime that will be
|
|
53
|
-
* returned by the underlying API
|
|
54
|
+
* returned by the underlying API.
|
|
54
55
|
*
|
|
55
56
|
* Default is -1, disabling this behavior.
|
|
56
57
|
*/
|
|
57
58
|
autoRefreshSecs?: number;
|
|
58
59
|
|
|
59
|
-
/**
|
|
60
|
-
* During auto-refresh, if the remaining lifetime for any token is below this threshold,
|
|
61
|
-
* force the provider to skip the local cache and go directly to the underlying provider for
|
|
62
|
-
* new tokens and refresh tokens.
|
|
63
|
-
*
|
|
64
|
-
* Default is -1, disabling this behavior.
|
|
65
|
-
*/
|
|
66
|
-
autoRefreshSkipCacheSecs?: number;
|
|
67
|
-
|
|
68
60
|
/**
|
|
69
61
|
* Scopes to request - if any - beyond the core `['openid', 'email']` scopes, which
|
|
70
62
|
* this client will always request.
|
|
@@ -72,14 +64,13 @@ export interface BaseOAuthClientConfig<S> {
|
|
|
72
64
|
idScopes?: string[];
|
|
73
65
|
|
|
74
66
|
/**
|
|
75
|
-
* Optional
|
|
76
|
-
*
|
|
77
|
-
* Map of code to a spec for an access token. The code is app-determined and
|
|
78
|
-
* will simply be used to get the loaded token via tha getAccessToken() method. The
|
|
79
|
-
* spec is implementation specific, but will typically include scopes to be loaded
|
|
80
|
-
* for the access token and potentially other meta-data required by the underlying provider.
|
|
67
|
+
* Optional spec for access tokens to be loaded and maintained to support access to one or more
|
|
68
|
+
* different back-end resources, distinct from the core Hoist auth flow via ID token.
|
|
81
69
|
*
|
|
82
|
-
*
|
|
70
|
+
* Map of key to a spec for an access token. The key is an arbitrary, app-determined string
|
|
71
|
+
* used to retrieve the loaded token via {@link getAccessTokenAsync}. The spec is implementation
|
|
72
|
+
* specific, but will typically include scopes to be loaded for the access token and potentially
|
|
73
|
+
* other metadata required by the underlying provider.
|
|
83
74
|
*/
|
|
84
75
|
accessTokens?: Record<string, S>;
|
|
85
76
|
}
|
|
@@ -89,13 +80,15 @@ export interface BaseOAuthClientConfig<S> {
|
|
|
89
80
|
* suitable concrete implementation to power a client-side OauthService. See `MsalClient` and
|
|
90
81
|
* `AuthZeroClient`
|
|
91
82
|
*
|
|
92
|
-
* Initialize such a service and this client within
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* flow as necessary.
|
|
83
|
+
* Initialize such a service and this client within an app's primary {@link HoistAuthModel} to use
|
|
84
|
+
* the tokens it acquires to authenticate with the Hoist server. (Note this requires a suitable
|
|
85
|
+
* server-side `AuthenticationService` implementation to validate the token and actually resolve
|
|
86
|
+
* the user.) On init, the client impl will initiate a pop-up or redirect flow as necessary.
|
|
97
87
|
*/
|
|
98
|
-
export abstract class BaseOAuthClient<
|
|
88
|
+
export abstract class BaseOAuthClient<
|
|
89
|
+
C extends BaseOAuthClientConfig<S>,
|
|
90
|
+
S extends AccessTokenSpec
|
|
91
|
+
> extends HoistBase {
|
|
99
92
|
/** Config loaded from UI server + init method. */
|
|
100
93
|
protected config: C;
|
|
101
94
|
|
|
@@ -121,13 +114,12 @@ export abstract class BaseOAuthClient<C extends BaseOAuthClientConfig<S>, S> ext
|
|
|
121
114
|
redirectUrl: 'APP_BASE_URL',
|
|
122
115
|
postLogoutRedirectUrl: 'APP_BASE_URL',
|
|
123
116
|
autoRefreshSecs: -1,
|
|
124
|
-
autoRefreshSkipCacheSecs: -1,
|
|
125
117
|
...config
|
|
126
118
|
};
|
|
127
119
|
throwIf(!config.clientId, 'Missing OAuth clientId. Please review your configuration.');
|
|
128
120
|
|
|
129
121
|
this.idScopes = union(['openid', 'email'], config.idScopes);
|
|
130
|
-
this.accessSpecs = this.config.accessTokens;
|
|
122
|
+
this.accessSpecs = this.config.accessTokens ?? {};
|
|
131
123
|
}
|
|
132
124
|
|
|
133
125
|
/**
|
|
@@ -171,14 +163,17 @@ export abstract class BaseOAuthClient<C extends BaseOAuthClientConfig<S>, S> ext
|
|
|
171
163
|
* Get an Access token.
|
|
172
164
|
*/
|
|
173
165
|
async getAccessTokenAsync(key: string): Promise<Token> {
|
|
174
|
-
|
|
166
|
+
const spec = this.accessSpecs[key];
|
|
167
|
+
if (!spec) throw XH.exception(`No access token spec configured for key "${key}"`);
|
|
168
|
+
|
|
169
|
+
return this.fetchAccessTokenAsync(spec, true);
|
|
175
170
|
}
|
|
176
171
|
|
|
177
172
|
/**
|
|
178
|
-
* Get all
|
|
173
|
+
* Get all configured tokens.
|
|
179
174
|
*/
|
|
180
|
-
async getAllTokensAsync(): Promise<TokenMap> {
|
|
181
|
-
return this.fetchAllTokensAsync(
|
|
175
|
+
async getAllTokensAsync(opts?: {eagerOnly?: boolean; useCache?: boolean}): Promise<TokenMap> {
|
|
176
|
+
return this.fetchAllTokensAsync(opts);
|
|
182
177
|
}
|
|
183
178
|
|
|
184
179
|
/**
|
|
@@ -314,12 +309,27 @@ export abstract class BaseOAuthClient<C extends BaseOAuthClientConfig<S>, S> ext
|
|
|
314
309
|
await never();
|
|
315
310
|
}
|
|
316
311
|
|
|
317
|
-
protected async fetchAllTokensAsync(
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
312
|
+
protected async fetchAllTokensAsync(opts?: {
|
|
313
|
+
eagerOnly?: boolean;
|
|
314
|
+
useCache?: boolean;
|
|
315
|
+
}): Promise<TokenMap> {
|
|
316
|
+
const eagerOnly = opts?.eagerOnly ?? false,
|
|
317
|
+
useCache = opts?.useCache ?? true,
|
|
318
|
+
accessSpecs = eagerOnly
|
|
319
|
+
? pickBy(this.accessSpecs, spec => spec.fetchMode !== 'lazy') // specs are eager by default - opt-in to lazy
|
|
320
|
+
: this.accessSpecs,
|
|
321
|
+
ret: TokenMap = {};
|
|
322
|
+
|
|
323
|
+
await Promise.allSettled(
|
|
324
|
+
map(accessSpecs, async (spec, key) => {
|
|
325
|
+
try {
|
|
326
|
+
ret[key] = await this.fetchAccessTokenAsync(spec, useCache);
|
|
327
|
+
} catch (e) {
|
|
328
|
+
XH.handleException(e, {logOnServer: true, showAlert: false});
|
|
329
|
+
}
|
|
330
|
+
})
|
|
331
|
+
);
|
|
332
|
+
|
|
323
333
|
// Do this after getting any access tokens --which can also populate the idToken cache!
|
|
324
334
|
ret.id = await this.fetchIdTokenSafeAsync(useCache);
|
|
325
335
|
|
|
@@ -361,22 +371,19 @@ export abstract class BaseOAuthClient<C extends BaseOAuthClientConfig<S>, S> ext
|
|
|
361
371
|
private async onTimerAsync(): Promise<void> {
|
|
362
372
|
const {config, lastRefreshAttempt} = this,
|
|
363
373
|
refreshSecs = config.autoRefreshSecs * SECONDS,
|
|
364
|
-
skipCacheSecs =
|
|
374
|
+
skipCacheSecs = refreshSecs + 5 * SECONDS;
|
|
365
375
|
|
|
366
376
|
if (olderThan(lastRefreshAttempt, refreshSecs)) {
|
|
367
377
|
this.lastRefreshAttempt = Date.now();
|
|
368
378
|
try {
|
|
369
379
|
this.logDebug('Refreshing all tokens:');
|
|
370
380
|
let tokens = await this.fetchAllTokensAsync(),
|
|
371
|
-
aging = pickBy(
|
|
372
|
-
tokens,
|
|
373
|
-
v => skipCacheSecs > 0 && v.expiresWithin(skipCacheSecs)
|
|
374
|
-
);
|
|
381
|
+
aging = pickBy(tokens, v => v.expiresWithin(skipCacheSecs));
|
|
375
382
|
if (!isEmpty(aging)) {
|
|
376
383
|
this.logDebug(
|
|
377
384
|
`Tokens [${keys(aging).join(', ')}] have < ${skipCacheSecs}s remaining, reloading without cache.`
|
|
378
385
|
);
|
|
379
|
-
tokens = await this.fetchAllTokensAsync(false);
|
|
386
|
+
tokens = await this.fetchAllTokensAsync({useCache: false});
|
|
380
387
|
}
|
|
381
388
|
this.logTokensDebug(tokens);
|
|
382
389
|
} catch (e) {
|
package/security/Token.ts
CHANGED
|
@@ -11,8 +11,6 @@ import {jwtDecode} from 'jwt-decode';
|
|
|
11
11
|
import {getRelativeTimestamp} from '@xh/hoist/cmp/relativetimestamp';
|
|
12
12
|
import {isNil} from 'lodash';
|
|
13
13
|
|
|
14
|
-
export type TokenMap = Record<string, Token>;
|
|
15
|
-
|
|
16
14
|
export class Token {
|
|
17
15
|
readonly value: string;
|
|
18
16
|
readonly decoded: PlainObject;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file belongs to Hoist, an application development toolkit
|
|
3
|
+
* developed by Extremely Heavy Industries (www.xh.io | info@xh.io)
|
|
4
|
+
*
|
|
5
|
+
* Copyright © 2025 Extremely Heavy Industries Inc.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {Token} from './Token';
|
|
9
|
+
|
|
10
|
+
export interface AccessTokenSpec {
|
|
11
|
+
/**
|
|
12
|
+
* Mode governing when the access token should be requested from provider:
|
|
13
|
+
* - eager (or undefined) - load on overall initialization, but do not block on failure.
|
|
14
|
+
* Useful for tokens that an app is almost certain to require during a user session.
|
|
15
|
+
* - lazy - defer loading until first requested by client. Useful for tokens that might
|
|
16
|
+
* never be needed by the app during a given user session.
|
|
17
|
+
*/
|
|
18
|
+
fetchMode?: 'eager' | 'lazy';
|
|
19
|
+
|
|
20
|
+
/** Scopes for the desired access token.*/
|
|
21
|
+
scopes: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type TokenMap = Record<string, Token>;
|
|
25
|
+
|
|
26
|
+
/** Aggregated telemetry results, produced by {@link MsalClient} when enabled via config. */
|
|
27
|
+
export interface TelemetryResults {
|
|
28
|
+
/** Stats by event type - */
|
|
29
|
+
events: Record<string, TelemetryEventResults>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Aggregated telemetry results for a single type of event. */
|
|
33
|
+
export interface TelemetryEventResults {
|
|
34
|
+
firstTime: Date;
|
|
35
|
+
lastTime: Date;
|
|
36
|
+
successCount: number;
|
|
37
|
+
failureCount: number;
|
|
38
|
+
/** Timing info (in ms) for event instances reported with duration. */
|
|
39
|
+
duration: {
|
|
40
|
+
count: number;
|
|
41
|
+
total: number;
|
|
42
|
+
average: number;
|
|
43
|
+
worst: number;
|
|
44
|
+
};
|
|
45
|
+
lastFailure?: {
|
|
46
|
+
time: Date;
|
|
47
|
+
duration: number;
|
|
48
|
+
code: string;
|
|
49
|
+
name: string;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -8,7 +8,8 @@ import type {Auth0ClientOptions} from '@auth0/auth0-spa-js';
|
|
|
8
8
|
import {Auth0Client} from '@auth0/auth0-spa-js';
|
|
9
9
|
import {XH} from '@xh/hoist/core';
|
|
10
10
|
import {wait} from '@xh/hoist/promise';
|
|
11
|
-
import {Token
|
|
11
|
+
import {Token} from '@xh/hoist/security/Token';
|
|
12
|
+
import {AccessTokenSpec, TokenMap} from '../Types';
|
|
12
13
|
import {SECONDS} from '@xh/hoist/utils/datetime';
|
|
13
14
|
import {mergeDeep, throwIf} from '@xh/hoist/utils/js';
|
|
14
15
|
import {flatMap, union} from 'lodash';
|
|
@@ -40,10 +41,7 @@ export interface AuthZeroClientConfig extends BaseOAuthClientConfig<AuthZeroToke
|
|
|
40
41
|
authZeroClientOptions?: Partial<Auth0ClientOptions>;
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
export interface AuthZeroTokenSpec {
|
|
44
|
-
/** Scopes for the desired access token.*/
|
|
45
|
-
scopes: string[];
|
|
46
|
-
|
|
44
|
+
export interface AuthZeroTokenSpec extends AccessTokenSpec {
|
|
47
45
|
/**
|
|
48
46
|
* Audience (i.e. API) identifier for AccessToken. Must be registered with Auth0.
|
|
49
47
|
* Note that this is required to ensure that issued token is a JWT and not an opaque string.
|
|
@@ -75,7 +73,7 @@ export class AuthZeroClient extends BaseOAuthClient<AuthZeroClientConfig, AuthZe
|
|
|
75
73
|
const {appState} = await client.handleRedirectCallback();
|
|
76
74
|
this.restoreRedirectState(appState);
|
|
77
75
|
await this.noteUserAuthenticatedAsync();
|
|
78
|
-
return this.fetchAllTokensAsync();
|
|
76
|
+
return this.fetchAllTokensAsync({eagerOnly: true});
|
|
79
77
|
}
|
|
80
78
|
|
|
81
79
|
// 1) If we are logged in, try to just reload tokens silently. This is the happy path on
|
|
@@ -83,7 +81,7 @@ export class AuthZeroClient extends BaseOAuthClient<AuthZeroClientConfig, AuthZe
|
|
|
83
81
|
if (await client.isAuthenticated()) {
|
|
84
82
|
try {
|
|
85
83
|
this.logDebug('Attempting silent token load.');
|
|
86
|
-
return await this.fetchAllTokensAsync();
|
|
84
|
+
return await this.fetchAllTokensAsync({eagerOnly: true});
|
|
87
85
|
} catch (e) {
|
|
88
86
|
this.logDebug('Failed to load tokens on init, fall back to login', e.message ?? e);
|
|
89
87
|
}
|
|
@@ -94,7 +92,7 @@ export class AuthZeroClient extends BaseOAuthClient<AuthZeroClientConfig, AuthZe
|
|
|
94
92
|
await this.loginAsync();
|
|
95
93
|
|
|
96
94
|
// 3) return tokens
|
|
97
|
-
return this.fetchAllTokensAsync();
|
|
95
|
+
return this.fetchAllTokensAsync({eagerOnly: true});
|
|
98
96
|
}
|
|
99
97
|
|
|
100
98
|
protected override async doLoginRedirectAsync(): Promise<void> {
|
|
@@ -7,16 +7,19 @@
|
|
|
7
7
|
import * as msal from '@azure/msal-browser';
|
|
8
8
|
import {
|
|
9
9
|
AccountInfo,
|
|
10
|
+
BrowserPerformanceClient,
|
|
11
|
+
Configuration,
|
|
10
12
|
IPublicClientApplication,
|
|
11
13
|
LogLevel,
|
|
12
14
|
PopupRequest,
|
|
13
15
|
SilentRequest
|
|
14
16
|
} from '@azure/msal-browser';
|
|
15
17
|
import {XH} from '@xh/hoist/core';
|
|
16
|
-
import {Token
|
|
18
|
+
import {Token} from '@xh/hoist/security/Token';
|
|
17
19
|
import {logDebug, logError, logInfo, logWarn, mergeDeep, throwIf} from '@xh/hoist/utils/js';
|
|
18
20
|
import {flatMap, union, uniq} from 'lodash';
|
|
19
21
|
import {BaseOAuthClient, BaseOAuthClientConfig} from '../BaseOAuthClient';
|
|
22
|
+
import {AccessTokenSpec, TelemetryResults, TokenMap} from '../Types';
|
|
20
23
|
|
|
21
24
|
export interface MsalClientConfig extends BaseOAuthClientConfig<MsalTokenSpec> {
|
|
22
25
|
/**
|
|
@@ -33,6 +36,13 @@ export interface MsalClientConfig extends BaseOAuthClientConfig<MsalTokenSpec> {
|
|
|
33
36
|
*/
|
|
34
37
|
domainHint?: string;
|
|
35
38
|
|
|
39
|
+
/**
|
|
40
|
+
* True to enable support for built-in telemetry provided by this class's internal MSAL client.
|
|
41
|
+
* Captured performance events will be summarized via {@link telemetryResults}.
|
|
42
|
+
* See https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/performance.md
|
|
43
|
+
*/
|
|
44
|
+
enableTelemetry?: boolean;
|
|
45
|
+
|
|
36
46
|
/**
|
|
37
47
|
* If specified, the client will use this value when initializing the app to enforce a minimum
|
|
38
48
|
* amount of time during which no further auth flow with the provider should be necessary.
|
|
@@ -65,10 +75,7 @@ export interface MsalClientConfig extends BaseOAuthClientConfig<MsalTokenSpec> {
|
|
|
65
75
|
msalClientOptions?: Partial<msal.Configuration>;
|
|
66
76
|
}
|
|
67
77
|
|
|
68
|
-
export interface MsalTokenSpec {
|
|
69
|
-
/** Scopes for the desired access token. */
|
|
70
|
-
scopes: string[];
|
|
71
|
-
|
|
78
|
+
export interface MsalTokenSpec extends AccessTokenSpec {
|
|
72
79
|
/**
|
|
73
80
|
* Scopes to be added to the scopes requested during interactive and SSO logins.
|
|
74
81
|
* See the `scopes` property on `PopupRequest`, `RedirectRequest`, and `SSORequest`
|
|
@@ -106,6 +113,10 @@ export class MsalClient extends BaseOAuthClient<MsalClientConfig, MsalTokenSpec>
|
|
|
106
113
|
private account: AccountInfo; // Authenticated account
|
|
107
114
|
private initialTokenLoad: boolean;
|
|
108
115
|
|
|
116
|
+
/** Enable telemetry via `enableTelemetry` ctor config, or via {@link enableTelemetry}. */
|
|
117
|
+
telemetryResults: TelemetryResults = {events: {}};
|
|
118
|
+
private _telemetryCbHandle: string = null;
|
|
119
|
+
|
|
109
120
|
constructor(config: MsalClientConfig) {
|
|
110
121
|
super({
|
|
111
122
|
initRefreshTokenExpirationOffsetSecs: -1,
|
|
@@ -120,6 +131,9 @@ export class MsalClient extends BaseOAuthClient<MsalClientConfig, MsalTokenSpec>
|
|
|
120
131
|
//-------------------------------------------
|
|
121
132
|
protected override async doInitAsync(): Promise<TokenMap> {
|
|
122
133
|
const client = (this.client = await this.createClientAsync());
|
|
134
|
+
if (this.config.enableTelemetry) {
|
|
135
|
+
this.enableTelemetry();
|
|
136
|
+
}
|
|
123
137
|
|
|
124
138
|
// 0) Handle redirect return
|
|
125
139
|
const redirectResp = await client.handleRedirectPromise();
|
|
@@ -127,7 +141,7 @@ export class MsalClient extends BaseOAuthClient<MsalClientConfig, MsalTokenSpec>
|
|
|
127
141
|
this.logDebug('Completing Redirect login');
|
|
128
142
|
this.noteUserAuthenticated(redirectResp.account);
|
|
129
143
|
this.restoreRedirectState(redirectResp.state);
|
|
130
|
-
return this.fetchAllTokensAsync();
|
|
144
|
+
return this.fetchAllTokensAsync({eagerOnly: true});
|
|
131
145
|
}
|
|
132
146
|
|
|
133
147
|
// 1) If we are logged in, try to just reload tokens silently. This is the happy path on
|
|
@@ -142,7 +156,7 @@ export class MsalClient extends BaseOAuthClient<MsalClientConfig, MsalTokenSpec>
|
|
|
142
156
|
try {
|
|
143
157
|
this.initialTokenLoad = true;
|
|
144
158
|
this.logDebug('Attempting silent token load.');
|
|
145
|
-
return await this.fetchAllTokensAsync();
|
|
159
|
+
return await this.fetchAllTokensAsync({eagerOnly: true});
|
|
146
160
|
} catch (e) {
|
|
147
161
|
this.account = null;
|
|
148
162
|
this.logDebug('Failed to load tokens on init, fall back to login', e.message ?? e);
|
|
@@ -171,7 +185,7 @@ export class MsalClient extends BaseOAuthClient<MsalClientConfig, MsalTokenSpec>
|
|
|
171
185
|
}
|
|
172
186
|
|
|
173
187
|
// 3) Return tokens
|
|
174
|
-
return this.fetchAllTokensAsync();
|
|
188
|
+
return this.fetchAllTokensAsync({eagerOnly: true});
|
|
175
189
|
}
|
|
176
190
|
|
|
177
191
|
protected override async doLoginPopupAsync(): Promise<void> {
|
|
@@ -249,6 +263,86 @@ export class MsalClient extends BaseOAuthClient<MsalClientConfig, MsalTokenSpec>
|
|
|
249
263
|
: await client.logoutPopup(opts);
|
|
250
264
|
}
|
|
251
265
|
|
|
266
|
+
//------------------------
|
|
267
|
+
// Telemetry
|
|
268
|
+
//------------------------
|
|
269
|
+
enableTelemetry(): void {
|
|
270
|
+
if (this._telemetryCbHandle) {
|
|
271
|
+
this.logInfo('Telemetry already enabled', this.telemetryResults);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
this.telemetryResults = {events: {}};
|
|
276
|
+
|
|
277
|
+
this._telemetryCbHandle = this.client.addPerformanceCallback(events => {
|
|
278
|
+
events.forEach(e => {
|
|
279
|
+
try {
|
|
280
|
+
const {events} = this.telemetryResults,
|
|
281
|
+
{name, startTimeMs, durationMs, success, errorName, errorCode} = e,
|
|
282
|
+
eTime = startTimeMs ? new Date(startTimeMs) : new Date();
|
|
283
|
+
|
|
284
|
+
const eResult = (events[name] ??= {
|
|
285
|
+
firstTime: eTime,
|
|
286
|
+
lastTime: eTime,
|
|
287
|
+
successCount: 0,
|
|
288
|
+
failureCount: 0,
|
|
289
|
+
duration: {count: 0, total: 0, average: 0, worst: 0},
|
|
290
|
+
lastFailure: null
|
|
291
|
+
});
|
|
292
|
+
eResult.lastTime = eTime;
|
|
293
|
+
|
|
294
|
+
if (success) {
|
|
295
|
+
eResult.successCount++;
|
|
296
|
+
} else {
|
|
297
|
+
eResult.failureCount++;
|
|
298
|
+
eResult.lastFailure = {
|
|
299
|
+
time: eTime,
|
|
300
|
+
duration: e.durationMs,
|
|
301
|
+
code: errorCode,
|
|
302
|
+
name: errorName
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (durationMs) {
|
|
307
|
+
const {duration} = eResult;
|
|
308
|
+
duration.count++;
|
|
309
|
+
duration.total += durationMs;
|
|
310
|
+
duration.average = Math.round(duration.total / duration.count);
|
|
311
|
+
duration.worst = Math.max(duration.worst, durationMs);
|
|
312
|
+
}
|
|
313
|
+
} catch (e) {
|
|
314
|
+
this.logError(`Error processing telemetry event`, e);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// Ask TrackService to include in background health check report, if enabled on that service.
|
|
320
|
+
// Handle TrackService not yet initialized (common, this client likely initialized before.)
|
|
321
|
+
this.addReaction({
|
|
322
|
+
when: () => XH.appIsRunning,
|
|
323
|
+
run: () =>
|
|
324
|
+
XH.trackService.addClientHealthReportSource(
|
|
325
|
+
'msalClient',
|
|
326
|
+
() => this.telemetryResults
|
|
327
|
+
)
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
this.logInfo('Telemetry enabled');
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
disableTelemetry(): void {
|
|
334
|
+
if (!this._telemetryCbHandle) {
|
|
335
|
+
this.logInfo('Telemetry already disabled');
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
this.client.removePerformanceCallback(this._telemetryCbHandle);
|
|
340
|
+
this._telemetryCbHandle = null;
|
|
341
|
+
|
|
342
|
+
XH.trackService.removeClientHealthReportSource('msalClient');
|
|
343
|
+
this.logInfo('Telemetry disabled', this.telemetryResults);
|
|
344
|
+
}
|
|
345
|
+
|
|
252
346
|
//------------------------
|
|
253
347
|
// Private implementation
|
|
254
348
|
//------------------------
|
|
@@ -265,29 +359,38 @@ export class MsalClient extends BaseOAuthClient<MsalClientConfig, MsalTokenSpec>
|
|
|
265
359
|
}
|
|
266
360
|
|
|
267
361
|
private async createClientAsync(): Promise<IPublicClientApplication> {
|
|
268
|
-
const {clientId, authority, msalLogLevel, msalClientOptions} = this.config;
|
|
362
|
+
const {clientId, authority, msalLogLevel, msalClientOptions, enableTelemetry} = this.config;
|
|
269
363
|
throwIf(!authority, 'Missing MSAL authority. Please review your configuration.');
|
|
270
364
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
{
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
logLevel: msalLogLevel
|
|
283
|
-
}
|
|
284
|
-
},
|
|
285
|
-
cache: {
|
|
286
|
-
cacheLocation: 'localStorage' // allows sharing auth info across tabs.
|
|
365
|
+
const mergedConf: Configuration = mergeDeep(
|
|
366
|
+
{
|
|
367
|
+
auth: {
|
|
368
|
+
clientId,
|
|
369
|
+
authority,
|
|
370
|
+
postLogoutRedirectUri: this.postLogoutRedirectUrl
|
|
371
|
+
},
|
|
372
|
+
system: {
|
|
373
|
+
loggerOptions: {
|
|
374
|
+
loggerCallback: this.logFromMsal,
|
|
375
|
+
logLevel: msalLogLevel
|
|
287
376
|
}
|
|
288
377
|
},
|
|
289
|
-
|
|
290
|
-
|
|
378
|
+
cache: {
|
|
379
|
+
cacheLocation: 'localStorage' // allows sharing auth info across tabs.
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
msalClientOptions
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
return msal.PublicClientApplication.createPublicClientApplication(
|
|
386
|
+
enableTelemetry
|
|
387
|
+
? {
|
|
388
|
+
...mergedConf,
|
|
389
|
+
telemetry: {
|
|
390
|
+
client: new BrowserPerformanceClient(mergedConf)
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
: mergedConf
|
|
291
394
|
);
|
|
292
395
|
}
|
|
293
396
|
|
package/svc/FetchService.ts
CHANGED
|
@@ -217,8 +217,9 @@ export class FetchService extends HoistService {
|
|
|
217
217
|
//-----------------------
|
|
218
218
|
private async fetchInternalAsync(opts: FetchOptions): Promise<any> {
|
|
219
219
|
opts = this.withCorrelationId(opts);
|
|
220
|
-
|
|
221
|
-
|
|
220
|
+
|
|
221
|
+
// Core Promise - chained with custom headers callback to ensure that work is included in overall tracked time.
|
|
222
|
+
let ret = this.withDefaultHeadersAsync(opts).then(opts => this.managedFetchAsync(opts));
|
|
222
223
|
|
|
223
224
|
// Apply tracking
|
|
224
225
|
const {correlationId, loadSpec, track} = opts;
|