@webex/webex-core 3.12.0-webex-services-ready.2 → 3.12.0-webex-services-ready.4
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 +5 -2
- package/dist/config.js +15 -0
- package/dist/config.js.map +1 -1
- package/dist/lib/batcher.js +1 -1
- package/dist/lib/credentials/credentials.js +1 -1
- package/dist/lib/credentials/token.js +1 -1
- package/dist/lib/domains.js +90 -0
- package/dist/lib/domains.js.map +1 -0
- package/dist/lib/services/service-catalog.js +6 -10
- package/dist/lib/services/service-catalog.js.map +1 -1
- package/dist/lib/services/services.js +26 -22
- package/dist/lib/services/services.js.map +1 -1
- package/dist/lib/services-v2/service-catalog.js +6 -12
- package/dist/lib/services-v2/service-catalog.js.map +1 -1
- package/dist/lib/services-v2/services-v2.js +33 -24
- package/dist/lib/services-v2/services-v2.js.map +1 -1
- package/dist/plugins/logger.js +1 -1
- package/dist/webex-core.js +2 -2
- package/package.json +13 -13
- package/src/config.js +17 -0
- package/src/lib/domains.ts +94 -0
- package/src/lib/services/service-catalog.js +6 -10
- package/src/lib/services/services.js +37 -31
- package/src/lib/services-v2/service-catalog.ts +6 -11
- package/src/lib/services-v2/services-v2.ts +41 -31
- package/test/fixtures/activation-email.ts +22 -0
- package/test/integration/spec/services/service-catalog.js +7 -6
- package/test/integration/spec/services/services.js +11 -8
- package/test/integration/spec/services-v2/services-v2.js +11 -8
- package/test/unit/spec/interceptors/auth.js +56 -0
- package/test/unit/spec/services/service-catalog.js +93 -11
- package/test/unit/spec/services/services.js +6 -1
- package/test/unit/spec/services-v2/service-catalog.ts +93 -11
- package/test/unit/spec/services-v2/services-v2.ts +22 -1
- package/test/unit/spec/webex-core.js +0 -2
- package/test/unit/spec/webex-internal-core.js +0 -2
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import Url from 'url';
|
|
2
|
+
|
|
3
|
+
import {uniq} from 'lodash';
|
|
4
|
+
|
|
5
|
+
// Canonicalise a hostname for comparison: lowercase, drop the brackets around
|
|
6
|
+
// an IPv6 literal, and drop leading/trailing dots. DNS treats `Example.com`,
|
|
7
|
+
// `example.com.` and `example.com` as the same name.
|
|
8
|
+
//
|
|
9
|
+
// Node's `url.domainToASCII` looks like the standard way to do this, but the
|
|
10
|
+
// `url` polyfill this package bundles for the browser does not implement it,
|
|
11
|
+
// so it cannot be used here. It also leaves trailing dots in place.
|
|
12
|
+
const normalizeHostname = (value: string): string =>
|
|
13
|
+
typeof value === 'string'
|
|
14
|
+
? value
|
|
15
|
+
.toLowerCase()
|
|
16
|
+
.replace(/^\[|\]$/g, '')
|
|
17
|
+
.replace(/^\.+/, '')
|
|
18
|
+
.replace(/\.+$/, '')
|
|
19
|
+
: '';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Canonicalise a list of configured allowed domains, discarding any entry that
|
|
23
|
+
* is not a usable hostname. Callers normalise on the way in so the stored list
|
|
24
|
+
* is already canonical, rather than re-deriving it on every request.
|
|
25
|
+
*
|
|
26
|
+
* @param {Array<string>} allowedDomains - The configured allowed domains.
|
|
27
|
+
* @returns {Array<string>} - Normalized, de-duplicated, non-empty entries.
|
|
28
|
+
*/
|
|
29
|
+
export const normalizeAllowedDomains = (allowedDomains: Array<string>): Array<string> =>
|
|
30
|
+
uniq(
|
|
31
|
+
(Array.isArray(allowedDomains) ? allowedDomains : []).map(normalizeHostname).filter(Boolean)
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Determine if a hostname is covered by an allowed domain, matching only on DNS
|
|
36
|
+
* label boundaries, so that a hostname is allowed only when it is the domain
|
|
37
|
+
* itself or a subdomain of it. Matching on a substring instead would treat
|
|
38
|
+
* unrelated hostnames that merely contain the domain as allowed.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} hostname - Hostname to test. Must not include a port.
|
|
41
|
+
* @param {string} allowedDomain - The configured allowed domain.
|
|
42
|
+
* @returns {boolean} - True when the hostname is the domain or a subdomain of it.
|
|
43
|
+
*/
|
|
44
|
+
const hostnameMatchesDomain = (hostname: string, allowedDomain: string): boolean => {
|
|
45
|
+
// The stored list is normalized on write, but `allowedDomains` is a public
|
|
46
|
+
// property, so normalize again here rather than trust it.
|
|
47
|
+
const host = normalizeHostname(hostname);
|
|
48
|
+
const domain = normalizeHostname(allowedDomain);
|
|
49
|
+
|
|
50
|
+
return !!host && !!domain && (host === domain || host.endsWith(`.${domain}`));
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Find the allowed domain covering a url, or `undefined` if there is none.
|
|
55
|
+
*
|
|
56
|
+
* Parsing lives here rather than in the callers, and deliberately uses both url
|
|
57
|
+
* parsers, because this check gates an `Authorization` header. The two
|
|
58
|
+
* transports behind `@webex/http-core` do not use the same url parser: the
|
|
59
|
+
* browser transport parses per WHATWG, the node transport uses Node's legacy
|
|
60
|
+
* `Url.parse`, and for some inputs the two resolve different hosts.
|
|
61
|
+
*
|
|
62
|
+
* Rather than picking one, require both to agree and fail closed when they do
|
|
63
|
+
* not, so this check can never authorize a host that differs from the one a
|
|
64
|
+
* transport would actually connect to. Do not narrow this to a single parser.
|
|
65
|
+
*
|
|
66
|
+
* @param {string} url - The url to match the allowed domains against.
|
|
67
|
+
* @param {Array<string>} allowedDomains - The configured allowed domains.
|
|
68
|
+
* @returns {string} - The matching allowed domain, or undefined if there is none.
|
|
69
|
+
*/
|
|
70
|
+
export const matchAllowedDomain = (
|
|
71
|
+
url: string,
|
|
72
|
+
allowedDomains: Array<string>
|
|
73
|
+
): string | undefined => {
|
|
74
|
+
let hostname: string;
|
|
75
|
+
let legacyHostname: string;
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
({hostname} = new URL(url));
|
|
79
|
+
({hostname: legacyHostname} = Url.parse(url));
|
|
80
|
+
} catch {
|
|
81
|
+
// Not a parsable absolute url, so it cannot belong to an allowed domain.
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (normalizeHostname(hostname) !== normalizeHostname(legacyHostname)) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return (allowedDomains || []).find((allowedDomain) =>
|
|
90
|
+
hostnameMatchesDomain(hostname, allowedDomain)
|
|
91
|
+
);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export default matchAllowedDomain;
|
|
@@ -4,6 +4,7 @@ import AmpState from 'ampersand-state';
|
|
|
4
4
|
|
|
5
5
|
import {union} from 'lodash';
|
|
6
6
|
import ServiceUrl from './service-url';
|
|
7
|
+
import {matchAllowedDomain, normalizeAllowedDomains} from '../domains';
|
|
7
8
|
|
|
8
9
|
/* eslint-disable no-underscore-dangle */
|
|
9
10
|
/**
|
|
@@ -268,19 +269,14 @@ const ServiceCatalog = AmpState.extend({
|
|
|
268
269
|
},
|
|
269
270
|
|
|
270
271
|
/**
|
|
271
|
-
* Finds an allowed domain that matches a specific url.
|
|
272
|
+
* Finds an allowed domain that matches a specific url. The url's hostname
|
|
273
|
+
* must be the allowed domain itself or a subdomain of it.
|
|
272
274
|
*
|
|
273
275
|
* @param {string} url - The url to match the allowed domains against.
|
|
274
276
|
* @returns {string} - The matching allowed domain.
|
|
275
277
|
*/
|
|
276
278
|
findAllowedDomain(url) {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
if (!urlObj.host) {
|
|
280
|
-
return undefined;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
return this.allowedDomains.find((allowedDomain) => urlObj.host.includes(allowedDomain));
|
|
279
|
+
return matchAllowedDomain(url, this.allowedDomains);
|
|
284
280
|
},
|
|
285
281
|
|
|
286
282
|
/**
|
|
@@ -367,7 +363,7 @@ const ServiceCatalog = AmpState.extend({
|
|
|
367
363
|
* @returns {void}
|
|
368
364
|
*/
|
|
369
365
|
setAllowedDomains(allowedDomains) {
|
|
370
|
-
this.allowedDomains =
|
|
366
|
+
this.allowedDomains = normalizeAllowedDomains(allowedDomains);
|
|
371
367
|
},
|
|
372
368
|
|
|
373
369
|
/**
|
|
@@ -376,7 +372,7 @@ const ServiceCatalog = AmpState.extend({
|
|
|
376
372
|
* @returns {void}
|
|
377
373
|
*/
|
|
378
374
|
addAllowedDomains(newAllowedDomains) {
|
|
379
|
-
this.allowedDomains = union(this.allowedDomains, newAllowedDomains);
|
|
375
|
+
this.allowedDomains = union(this.allowedDomains, normalizeAllowedDomains(newAllowedDomains));
|
|
380
376
|
},
|
|
381
377
|
|
|
382
378
|
/**
|
|
@@ -23,12 +23,6 @@ const DEFAULT_CLUSTER_IDENTIFIER =
|
|
|
23
23
|
const CATALOG_CACHE_KEY_V1 = 'services.v1.u2cHostMap';
|
|
24
24
|
const CATALOG_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
25
25
|
|
|
26
|
-
// Maximum time we will wait for the initial catalog collection before letting
|
|
27
|
-
// `services.ready` (and therefore `webex.ready`) fire anyway. A hung request
|
|
28
|
-
// must never leave the app on a permanent spinner - past this point downstream
|
|
29
|
-
// consumers must fall through to their normal error/login paths.
|
|
30
|
-
const SERVICES_INIT_TIMEOUT_MS = 15_000;
|
|
31
|
-
|
|
32
26
|
/* eslint-disable no-underscore-dangle */
|
|
33
27
|
/**
|
|
34
28
|
* @class
|
|
@@ -1368,6 +1362,7 @@ const Services = WebexPlugin.extend({
|
|
|
1368
1362
|
|
|
1369
1363
|
// Destructure the credentials plugin.
|
|
1370
1364
|
const {credentials} = this.webex;
|
|
1365
|
+
const catalog = this._getCatalog();
|
|
1371
1366
|
|
|
1372
1367
|
// Init a promise chain. Must be done as a Promise.resolve() to allow
|
|
1373
1368
|
// credentials#getOrgId() to properly throw.
|
|
@@ -1380,12 +1375,18 @@ const Services = WebexPlugin.extend({
|
|
|
1380
1375
|
.then(() => {
|
|
1381
1376
|
// Validate if the token is authorized.
|
|
1382
1377
|
if (credentials.canAuthorize) {
|
|
1383
|
-
// Attempt to collect the postauth catalog
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1378
|
+
// Attempt to collect the postauth catalog, then mark the catalog
|
|
1379
|
+
// ready. Setting `isReady` here - rather than only in the init
|
|
1380
|
+
// callers - means a slow postauth fetch that loses the gated-init
|
|
1381
|
+
// timeout race still marks the catalog ready once it completes.
|
|
1382
|
+
return this.updateServices()
|
|
1383
|
+
.then(() => {
|
|
1384
|
+
catalog.isReady = true;
|
|
1385
|
+
})
|
|
1386
|
+
.catch(() => {
|
|
1387
|
+
this.initFailed = true;
|
|
1388
|
+
this.logger.warn('services: cannot retrieve postauth catalog');
|
|
1389
|
+
});
|
|
1389
1390
|
}
|
|
1390
1391
|
|
|
1391
1392
|
// Return a resolved promise for consistent return value.
|
|
@@ -1480,16 +1481,14 @@ const Services = WebexPlugin.extend({
|
|
|
1480
1481
|
const {supertoken} = this.webex.credentials;
|
|
1481
1482
|
// Validate if the supertoken exists.
|
|
1482
1483
|
if (supertoken && supertoken.access_token) {
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
.
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
);
|
|
1492
|
-
});
|
|
1484
|
+
// `initServiceCatalogs` marks the catalog ready internally once the
|
|
1485
|
+
// postauth catalog is collected.
|
|
1486
|
+
this.initServiceCatalogs().catch((error) => {
|
|
1487
|
+
this.initFailed = true;
|
|
1488
|
+
this.logger.error(
|
|
1489
|
+
`services: failed to init initial services when credentials available, ${error?.message}`
|
|
1490
|
+
);
|
|
1491
|
+
});
|
|
1493
1492
|
} else {
|
|
1494
1493
|
const {email} = this.webex.config;
|
|
1495
1494
|
|
|
@@ -1529,20 +1528,23 @@ const Services = WebexPlugin.extend({
|
|
|
1529
1528
|
|
|
1530
1529
|
// Race init against a hard timeout so a hung request never leaves
|
|
1531
1530
|
// `services.ready` false forever - that would stall `webex.ready` and
|
|
1532
|
-
// leave
|
|
1533
|
-
|
|
1531
|
+
// leave consumers waiting on it indefinitely. Timeout is configurable via
|
|
1532
|
+
// `config.services.catalogInitTimeout` (defaults to 15s in config).
|
|
1533
|
+
const initTimeoutMs = this.webex.config?.services?.catalogInitTimeout;
|
|
1534
|
+
|
|
1535
|
+
const initServiceCatalogsTimeout = new Promise((_, reject) => {
|
|
1534
1536
|
setTimeout(
|
|
1535
|
-
() => reject(new Error(`services: init timed out after ${
|
|
1536
|
-
|
|
1537
|
+
() => reject(new Error(`services: init timed out after ${initTimeoutMs}ms`)),
|
|
1538
|
+
initTimeoutMs
|
|
1537
1539
|
);
|
|
1538
1540
|
});
|
|
1539
1541
|
|
|
1540
1542
|
// Validate if the supertoken exists.
|
|
1541
1543
|
if (supertoken && supertoken.access_token) {
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1544
|
+
// `initServiceCatalogs` marks the catalog ready internally once the
|
|
1545
|
+
// postauth catalog is collected - even if it loses the timeout race
|
|
1546
|
+
// above, so a slow fetch still eventually flips `catalog.isReady`.
|
|
1547
|
+
Promise.race([this.initServiceCatalogs(), initServiceCatalogsTimeout])
|
|
1546
1548
|
.catch((error) => {
|
|
1547
1549
|
this.initFailed = true;
|
|
1548
1550
|
this.logger.error(
|
|
@@ -1553,7 +1555,10 @@ const Services = WebexPlugin.extend({
|
|
|
1553
1555
|
} else {
|
|
1554
1556
|
const {email} = this.webex.config;
|
|
1555
1557
|
|
|
1556
|
-
Promise.race([
|
|
1558
|
+
Promise.race([
|
|
1559
|
+
this.collectPreauthCatalog(email ? {email} : undefined),
|
|
1560
|
+
initServiceCatalogsTimeout,
|
|
1561
|
+
])
|
|
1557
1562
|
.catch((error) => {
|
|
1558
1563
|
this.initFailed = true;
|
|
1559
1564
|
this.logger.error(
|
|
@@ -1566,6 +1571,7 @@ const Services = WebexPlugin.extend({
|
|
|
1566
1571
|
// for `canAuthorize` flipping true and then collect the postauth catalog.
|
|
1567
1572
|
this.listenToOnce(this.webex, 'change:canAuthorize', () => {
|
|
1568
1573
|
if (this.webex.canAuthorize && !catalog.status.postauth.ready) {
|
|
1574
|
+
// `initServiceCatalogs` marks the catalog ready internally.
|
|
1569
1575
|
this.initServiceCatalogs().catch((error) => {
|
|
1570
1576
|
this.logger.error(
|
|
1571
1577
|
`services: failed to init service catalogs after auth, ${error?.message}`
|
|
@@ -3,6 +3,7 @@ import AmpState from 'ampersand-state';
|
|
|
3
3
|
import {union} from 'lodash';
|
|
4
4
|
import ServiceDetail from './service-detail';
|
|
5
5
|
import {IServiceDetail, ServiceGroup} from './types';
|
|
6
|
+
import {matchAllowedDomain, normalizeAllowedDomains} from '../domains';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* @class
|
|
@@ -210,20 +211,14 @@ const ServiceCatalog = AmpState.extend({
|
|
|
210
211
|
},
|
|
211
212
|
|
|
212
213
|
/**
|
|
213
|
-
* Finds an allowed domain that matches a specific url.
|
|
214
|
+
* Finds an allowed domain that matches a specific url. The url's hostname
|
|
215
|
+
* must be the allowed domain itself or a subdomain of it.
|
|
214
216
|
*
|
|
215
217
|
* @param {string} url - The url to match the allowed domains against.
|
|
216
218
|
* @returns {string} - The matching allowed domain.
|
|
217
219
|
*/
|
|
218
220
|
findAllowedDomain(url: string): string {
|
|
219
|
-
|
|
220
|
-
const urlObj = new URL(url);
|
|
221
|
-
|
|
222
|
-
return this.allowedDomains.find((allowedDomain) => urlObj.host.includes(allowedDomain));
|
|
223
|
-
} catch {
|
|
224
|
-
// If the URL is invalid or can't be found, return undefined
|
|
225
|
-
return undefined;
|
|
226
|
-
}
|
|
221
|
+
return matchAllowedDomain(url, this.allowedDomains);
|
|
227
222
|
},
|
|
228
223
|
|
|
229
224
|
/**
|
|
@@ -282,7 +277,7 @@ const ServiceCatalog = AmpState.extend({
|
|
|
282
277
|
* @returns {void}
|
|
283
278
|
*/
|
|
284
279
|
setAllowedDomains(allowedDomains: Array<string>): void {
|
|
285
|
-
this.allowedDomains =
|
|
280
|
+
this.allowedDomains = normalizeAllowedDomains(allowedDomains);
|
|
286
281
|
},
|
|
287
282
|
|
|
288
283
|
/**
|
|
@@ -291,7 +286,7 @@ const ServiceCatalog = AmpState.extend({
|
|
|
291
286
|
* @returns {void}
|
|
292
287
|
*/
|
|
293
288
|
addAllowedDomains(newAllowedDomains: Array<string>): void {
|
|
294
|
-
this.allowedDomains = union(this.allowedDomains, newAllowedDomains);
|
|
289
|
+
this.allowedDomains = union(this.allowedDomains, normalizeAllowedDomains(newAllowedDomains));
|
|
295
290
|
},
|
|
296
291
|
|
|
297
292
|
/**
|
|
@@ -32,12 +32,6 @@ const DEFAULT_CLUSTER_IDENTIFIER =
|
|
|
32
32
|
const CATALOG_CACHE_KEY_V2 = 'services.v2.u2cHostMap';
|
|
33
33
|
const CATALOG_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
34
34
|
|
|
35
|
-
// Maximum time we will wait for the initial catalog collection before letting
|
|
36
|
-
// `services.ready` (and therefore `webex.ready`) fire anyway. A hung request
|
|
37
|
-
// must never leave the app on a permanent spinner - past this point downstream
|
|
38
|
-
// consumers must fall through to their normal error/login paths.
|
|
39
|
-
const SERVICES_INIT_TIMEOUT_MS = 15_000;
|
|
40
|
-
|
|
41
35
|
/* eslint-disable no-underscore-dangle */
|
|
42
36
|
/**
|
|
43
37
|
* @class
|
|
@@ -1241,7 +1235,11 @@ const Services = WebexPlugin.extend({
|
|
|
1241
1235
|
): Promise<object> {
|
|
1242
1236
|
const service = 'u2c';
|
|
1243
1237
|
const resource = from ? `/${from}/catalog` : '/catalog';
|
|
1244
|
-
const qs = {
|
|
1238
|
+
const qs = {
|
|
1239
|
+
...(query || {}),
|
|
1240
|
+
format: 'U2CV2',
|
|
1241
|
+
...(this.webex.config?.services?.useCatalogOverride && {useCatalogOverride: true}),
|
|
1242
|
+
};
|
|
1245
1243
|
|
|
1246
1244
|
if (forceRefresh) {
|
|
1247
1245
|
qs.timestamp = new Date().getTime();
|
|
@@ -1334,6 +1332,7 @@ const Services = WebexPlugin.extend({
|
|
|
1334
1332
|
|
|
1335
1333
|
// Destructure the credentials plugin.
|
|
1336
1334
|
const {credentials} = this.webex;
|
|
1335
|
+
const catalog = this._getCatalog();
|
|
1337
1336
|
|
|
1338
1337
|
// Init a promise chain. Must be done as a Promise.resolve() to allow
|
|
1339
1338
|
// credentials#getOrgId() to properly throw.
|
|
@@ -1346,11 +1345,18 @@ const Services = WebexPlugin.extend({
|
|
|
1346
1345
|
.then(() => {
|
|
1347
1346
|
// Validate if the token is authorized.
|
|
1348
1347
|
if (credentials.canAuthorize) {
|
|
1349
|
-
// Attempt to collect the postauth catalog
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
})
|
|
1348
|
+
// Attempt to collect the postauth catalog, then mark the catalog
|
|
1349
|
+
// ready. Setting `isReady` here - rather than only in the init
|
|
1350
|
+
// callers - means a slow postauth fetch that loses the gated-init
|
|
1351
|
+
// timeout race still marks the catalog ready once it completes.
|
|
1352
|
+
return this.updateServices({forceRefresh: refresh})
|
|
1353
|
+
.then(() => {
|
|
1354
|
+
catalog.isReady = true;
|
|
1355
|
+
})
|
|
1356
|
+
.catch(() => {
|
|
1357
|
+
this.initFailed = true;
|
|
1358
|
+
this.logger.warn('services: cannot retrieve postauth catalog');
|
|
1359
|
+
});
|
|
1354
1360
|
}
|
|
1355
1361
|
|
|
1356
1362
|
// Return a resolved promise for consistent return value.
|
|
@@ -1438,16 +1444,14 @@ const Services = WebexPlugin.extend({
|
|
|
1438
1444
|
const {supertoken} = this.webex.credentials;
|
|
1439
1445
|
// Validate if the supertoken exists.
|
|
1440
1446
|
if (supertoken && supertoken.access_token) {
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
.
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
);
|
|
1450
|
-
});
|
|
1447
|
+
// `initServiceCatalogs` marks the catalog ready internally once the
|
|
1448
|
+
// postauth catalog is collected.
|
|
1449
|
+
this.initServiceCatalogs().catch((error) => {
|
|
1450
|
+
this.initFailed = true;
|
|
1451
|
+
this.logger.error(
|
|
1452
|
+
`services: failed to init initial services when credentials available, ${error?.message}`
|
|
1453
|
+
);
|
|
1454
|
+
});
|
|
1451
1455
|
} else {
|
|
1452
1456
|
const {email} = this.webex.config;
|
|
1453
1457
|
|
|
@@ -1487,20 +1491,22 @@ const Services = WebexPlugin.extend({
|
|
|
1487
1491
|
|
|
1488
1492
|
// Race init against a hard timeout so a hung request never leaves
|
|
1489
1493
|
// `services.ready` false forever - that would stall `webex.ready` and
|
|
1490
|
-
// leave
|
|
1491
|
-
|
|
1494
|
+
// leave consumers waiting on it indefinitely. Timeout is configurable via
|
|
1495
|
+
// `config.services.catalogInitTimeout` (defaults to 15s in config).
|
|
1496
|
+
const initTimeoutMs = this.webex.config?.services?.catalogInitTimeout;
|
|
1497
|
+
const initServiceCatalogsTimeout = new Promise<never>((_, reject) => {
|
|
1492
1498
|
setTimeout(
|
|
1493
|
-
() => reject(new Error(`services: init timed out after ${
|
|
1494
|
-
|
|
1499
|
+
() => reject(new Error(`services: init timed out after ${initTimeoutMs}ms`)),
|
|
1500
|
+
initTimeoutMs
|
|
1495
1501
|
);
|
|
1496
1502
|
});
|
|
1497
1503
|
|
|
1498
1504
|
// Validate if the supertoken exists.
|
|
1499
1505
|
if (supertoken && supertoken.access_token) {
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1506
|
+
// `initServiceCatalogs` marks the catalog ready internally once the
|
|
1507
|
+
// postauth catalog is collected - even if it loses the timeout race
|
|
1508
|
+
// above, so a slow fetch still eventually flips `catalog.isReady`.
|
|
1509
|
+
Promise.race([this.initServiceCatalogs(), initServiceCatalogsTimeout])
|
|
1504
1510
|
.catch((error) => {
|
|
1505
1511
|
this.initFailed = true;
|
|
1506
1512
|
this.logger.error(
|
|
@@ -1511,7 +1517,10 @@ const Services = WebexPlugin.extend({
|
|
|
1511
1517
|
} else {
|
|
1512
1518
|
const {email} = this.webex.config;
|
|
1513
1519
|
|
|
1514
|
-
Promise.race([
|
|
1520
|
+
Promise.race([
|
|
1521
|
+
this.collectPreauthCatalog(email ? {email} : undefined),
|
|
1522
|
+
initServiceCatalogsTimeout,
|
|
1523
|
+
])
|
|
1515
1524
|
.catch((error) => {
|
|
1516
1525
|
this.initFailed = true;
|
|
1517
1526
|
this.logger.error(
|
|
@@ -1524,6 +1533,7 @@ const Services = WebexPlugin.extend({
|
|
|
1524
1533
|
// for `canAuthorize` flipping true and then collect the postauth catalog.
|
|
1525
1534
|
this.listenToOnce(this.webex, 'change:canAuthorize', () => {
|
|
1526
1535
|
if (this.webex.canAuthorize && !catalog.status.postauth.ready) {
|
|
1536
|
+
// `initServiceCatalogs` marks the catalog ready internally.
|
|
1527
1537
|
this.initServiceCatalogs().catch((error) => {
|
|
1528
1538
|
this.logger.error(
|
|
1529
1539
|
`services: failed to init service catalogs after auth, ${error?.message}`
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import uuid from 'uuid';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Generates a unique test email for user-activation/validation specs.
|
|
9
|
+
*
|
|
10
|
+
* The local part is kept short on purpose: for a brand-new self-signup user the
|
|
11
|
+
* backend derives the "given name" from the email local part, and self-signup
|
|
12
|
+
* orgs cap the given name at 50 characters. A full UUID would push the local
|
|
13
|
+
* part to 59 chars and fail with errorCode 100018, so we use 20 hex characters
|
|
14
|
+
* of entropy (local part = 43 chars).
|
|
15
|
+
*
|
|
16
|
+
* @returns {string} e.g. `Collabctg+webex-js-sdk-1a2b3c4d5e6f7a8b9c0d@gmail.com`
|
|
17
|
+
*/
|
|
18
|
+
export function createActivationEmail(): string {
|
|
19
|
+
return `Collabctg+webex-js-sdk-${uuid.v4().replace(/-/g, '').slice(0, 20)}@gmail.com`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default createActivationEmail;
|
|
@@ -23,12 +23,13 @@ describe('webex-core', () => {
|
|
|
23
23
|
.then(
|
|
24
24
|
([user]) =>
|
|
25
25
|
new Promise((resolve) => {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
setTimeout(() => {
|
|
27
|
+
webexUser = user;
|
|
28
|
+
webex = new WebexCore({credentials: user.token});
|
|
29
|
+
services = webex.internal.services;
|
|
30
|
+
catalog = services._getCatalog();
|
|
31
|
+
resolve();
|
|
32
|
+
}, 1000);
|
|
32
33
|
})
|
|
33
34
|
)
|
|
34
35
|
.then(() => webex.internal.device.register())
|
|
@@ -20,6 +20,7 @@ import WebexCore, {
|
|
|
20
20
|
import testUsers from '@webex/test-helper-test-users';
|
|
21
21
|
import uuid from 'uuid';
|
|
22
22
|
import sinon from 'sinon';
|
|
23
|
+
import {createActivationEmail} from '../../../fixtures/activation-email';
|
|
23
24
|
|
|
24
25
|
/* eslint-disable no-underscore-dangle */
|
|
25
26
|
describe('webex-core', () => {
|
|
@@ -408,11 +409,13 @@ describe('webex-core', () => {
|
|
|
408
409
|
services._loadCatalogFromCache = sinon.stub().resolves(false);
|
|
409
410
|
services.initServiceCatalogs = sinon.stub().resolves();
|
|
410
411
|
services.initialize();
|
|
412
|
+
// The mode-specific ('ready'/'loaded') listener is registered inside the
|
|
413
|
+
// change:config handler, so fire change:config first, then 'ready'.
|
|
414
|
+
webex.trigger('change:config');
|
|
411
415
|
webex.trigger('ready');
|
|
412
416
|
// Wait for the async 'ready' handler to complete
|
|
413
417
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
414
418
|
assert.called(services.initServiceCatalogs);
|
|
415
|
-
assert.isTrue(catalog.isReady);
|
|
416
419
|
});
|
|
417
420
|
|
|
418
421
|
it('should collect different catalogs based on OrgId region', () =>
|
|
@@ -925,7 +928,7 @@ describe('webex-core', () => {
|
|
|
925
928
|
|
|
926
929
|
it('validates a non-existing user', () =>
|
|
927
930
|
unauthServices
|
|
928
|
-
.validateUser({email:
|
|
931
|
+
.validateUser({email: createActivationEmail()})
|
|
929
932
|
.then((r) => {
|
|
930
933
|
assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
|
|
931
934
|
assert.equal(r.activated, false);
|
|
@@ -938,7 +941,7 @@ describe('webex-core', () => {
|
|
|
938
941
|
it('validates new user with activationOptions suppressEmail false', () =>
|
|
939
942
|
unauthServices
|
|
940
943
|
.validateUser({
|
|
941
|
-
email:
|
|
944
|
+
email: createActivationEmail(),
|
|
942
945
|
activationOptions: {suppressEmail: false},
|
|
943
946
|
})
|
|
944
947
|
.then((r) => {
|
|
@@ -954,7 +957,7 @@ describe('webex-core', () => {
|
|
|
954
957
|
it('validates new user with activationOptions suppressEmail true', () =>
|
|
955
958
|
unauthServices
|
|
956
959
|
.validateUser({
|
|
957
|
-
email:
|
|
960
|
+
email: createActivationEmail(),
|
|
958
961
|
activationOptions: {suppressEmail: true},
|
|
959
962
|
})
|
|
960
963
|
.then((r) => {
|
|
@@ -1010,7 +1013,7 @@ describe('webex-core', () => {
|
|
|
1010
1013
|
|
|
1011
1014
|
return unauthServices
|
|
1012
1015
|
.validateUser({
|
|
1013
|
-
email:
|
|
1016
|
+
email: createActivationEmail(),
|
|
1014
1017
|
activationOptions: {suppressEmail: true},
|
|
1015
1018
|
})
|
|
1016
1019
|
.then(() => {
|
|
@@ -1024,7 +1027,7 @@ describe('webex-core', () => {
|
|
|
1024
1027
|
|
|
1025
1028
|
return unauthServices
|
|
1026
1029
|
.validateUser({
|
|
1027
|
-
email:
|
|
1030
|
+
email: createActivationEmail(),
|
|
1028
1031
|
activationOptions: {suppressEmail: true},
|
|
1029
1032
|
preloginUserId,
|
|
1030
1033
|
})
|
|
@@ -1041,7 +1044,7 @@ describe('webex-core', () => {
|
|
|
1041
1044
|
|
|
1042
1045
|
return unauthServices
|
|
1043
1046
|
.validateUser({
|
|
1044
|
-
email:
|
|
1047
|
+
email: createActivationEmail(),
|
|
1045
1048
|
activationOptions: {suppressEmail: true},
|
|
1046
1049
|
})
|
|
1047
1050
|
.then(() => {
|
|
@@ -1064,7 +1067,7 @@ describe('webex-core', () => {
|
|
|
1064
1067
|
|
|
1065
1068
|
return userOnboardingServices
|
|
1066
1069
|
.validateUser({
|
|
1067
|
-
email:
|
|
1070
|
+
email: createActivationEmail(),
|
|
1068
1071
|
activationOptions: {suppressEmail: true},
|
|
1069
1072
|
})
|
|
1070
1073
|
.then(() => {
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
formattedServiceHostmapEntryTest,
|
|
26
26
|
serviceHostmapV2,
|
|
27
27
|
} from '../../../fixtures/host-catalog-v2';
|
|
28
|
+
import {createActivationEmail} from '../../../fixtures/activation-email';
|
|
28
29
|
|
|
29
30
|
// /* eslint-disable no-underscore-dangle */
|
|
30
31
|
describe('webex-core', () => {
|
|
@@ -320,11 +321,13 @@ describe('webex-core', () => {
|
|
|
320
321
|
services._loadCatalogFromCache = sinon.stub().resolves(false);
|
|
321
322
|
services.initServiceCatalogs = sinon.stub().resolves();
|
|
322
323
|
services.initialize();
|
|
324
|
+
// The mode-specific ('ready'/'loaded') listener is registered inside the
|
|
325
|
+
// change:config handler, so fire change:config first, then 'ready'.
|
|
326
|
+
webex.trigger('change:config');
|
|
323
327
|
webex.trigger('ready');
|
|
324
328
|
// Wait for the async 'ready' handler to complete
|
|
325
329
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
326
330
|
assert.called(services.initServiceCatalogs);
|
|
327
|
-
assert.isTrue(catalog.isReady);
|
|
328
331
|
});
|
|
329
332
|
|
|
330
333
|
it('should collect different catalogs based on OrgId region', () =>
|
|
@@ -848,7 +851,7 @@ describe('webex-core', () => {
|
|
|
848
851
|
|
|
849
852
|
it('validates a non-existing user', () =>
|
|
850
853
|
unauthServices
|
|
851
|
-
.validateUser({email:
|
|
854
|
+
.validateUser({email: createActivationEmail()})
|
|
852
855
|
.then((r) => {
|
|
853
856
|
assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
|
|
854
857
|
assert.equal(r.activated, false);
|
|
@@ -858,7 +861,7 @@ describe('webex-core', () => {
|
|
|
858
861
|
it('validates new user with activationOptions suppressEmail false', () =>
|
|
859
862
|
unauthServices
|
|
860
863
|
.validateUser({
|
|
861
|
-
email:
|
|
864
|
+
email: createActivationEmail(),
|
|
862
865
|
activationOptions: {suppressEmail: false},
|
|
863
866
|
})
|
|
864
867
|
.then((r) => {
|
|
@@ -871,7 +874,7 @@ describe('webex-core', () => {
|
|
|
871
874
|
it('validates new user with activationOptions suppressEmail true', () =>
|
|
872
875
|
unauthServices
|
|
873
876
|
.validateUser({
|
|
874
|
-
email:
|
|
877
|
+
email: createActivationEmail(),
|
|
875
878
|
activationOptions: {suppressEmail: true},
|
|
876
879
|
})
|
|
877
880
|
.then((r) => {
|
|
@@ -915,7 +918,7 @@ describe('webex-core', () => {
|
|
|
915
918
|
|
|
916
919
|
return unauthServices
|
|
917
920
|
.validateUser({
|
|
918
|
-
email:
|
|
921
|
+
email: createActivationEmail(),
|
|
919
922
|
activationOptions: {suppressEmail: true},
|
|
920
923
|
})
|
|
921
924
|
.then(() => {
|
|
@@ -929,7 +932,7 @@ describe('webex-core', () => {
|
|
|
929
932
|
|
|
930
933
|
return unauthServices
|
|
931
934
|
.validateUser({
|
|
932
|
-
email:
|
|
935
|
+
email: createActivationEmail(),
|
|
933
936
|
activationOptions: {suppressEmail: true},
|
|
934
937
|
preloginUserId,
|
|
935
938
|
})
|
|
@@ -946,7 +949,7 @@ describe('webex-core', () => {
|
|
|
946
949
|
|
|
947
950
|
return unauthServices
|
|
948
951
|
.validateUser({
|
|
949
|
-
email:
|
|
952
|
+
email: createActivationEmail(),
|
|
950
953
|
activationOptions: {suppressEmail: true},
|
|
951
954
|
})
|
|
952
955
|
.then(() => {
|
|
@@ -969,7 +972,7 @@ describe('webex-core', () => {
|
|
|
969
972
|
|
|
970
973
|
return userOnboardingServices
|
|
971
974
|
.validateUser({
|
|
972
|
-
email:
|
|
975
|
+
email: createActivationEmail(),
|
|
973
976
|
activationOptions: {suppressEmail: true},
|
|
974
977
|
})
|
|
975
978
|
.then(() => {
|