@webex/webex-core 3.12.0-next.4 → 3.12.0-next.40
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/credentials-config.js +12 -0
- package/dist/credentials-config.js.map +1 -1
- package/dist/lib/batcher.js +23 -7
- package/dist/lib/batcher.js.map +1 -1
- package/dist/lib/credentials/credentials.js +48 -4
- package/dist/lib/credentials/credentials.js.map +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 +196 -28
- 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 +203 -30
- 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/dist/webex-core.js.map +1 -1
- package/package.json +13 -13
- package/src/config.js +17 -0
- package/src/credentials-config.js +13 -0
- package/src/lib/batcher.js +25 -10
- package/src/lib/credentials/credentials.js +50 -3
- package/src/lib/domains.ts +94 -0
- package/src/lib/services/service-catalog.js +6 -10
- package/src/lib/services/services.js +170 -17
- package/src/lib/services-v2/service-catalog.ts +6 -11
- package/src/lib/services-v2/services-v2.ts +174 -17
- package/test/fixtures/activation-email.ts +22 -0
- package/test/integration/spec/services/services.js +44 -8
- package/test/integration/spec/services-v2/services-v2.js +44 -8
- package/test/unit/spec/credentials/credentials.js +133 -2
- package/test/unit/spec/interceptors/auth.js +56 -0
- package/test/unit/spec/lib/batcher.js +56 -0
- package/test/unit/spec/services/service-catalog.js +93 -11
- package/test/unit/spec/services/services.js +307 -2
- package/test/unit/spec/services-v2/service-catalog.ts +93 -11
- package/test/unit/spec/services-v2/services-v2.ts +311 -0
|
@@ -57,6 +57,22 @@ const Services = WebexPlugin.extend({
|
|
|
57
57
|
initFailed: ['boolean', false, false],
|
|
58
58
|
},
|
|
59
59
|
|
|
60
|
+
session: {
|
|
61
|
+
/**
|
|
62
|
+
* Becomes `true` once the initial catalog collection has completed
|
|
63
|
+
* (successfully or otherwise) and any in-flight credentials refresh has
|
|
64
|
+
* settled. Blocks `webex.ready` so consumers can rely on `webex.ready`
|
|
65
|
+
* implying "catalogs populated AND credential state stable".
|
|
66
|
+
* @instance
|
|
67
|
+
* @memberof Services
|
|
68
|
+
* @type {boolean}
|
|
69
|
+
*/
|
|
70
|
+
ready: {
|
|
71
|
+
default: false,
|
|
72
|
+
type: 'boolean',
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
|
|
60
76
|
_catalogs: new WeakMap(),
|
|
61
77
|
|
|
62
78
|
_serviceUrls: null,
|
|
@@ -1346,6 +1362,7 @@ const Services = WebexPlugin.extend({
|
|
|
1346
1362
|
|
|
1347
1363
|
// Destructure the credentials plugin.
|
|
1348
1364
|
const {credentials} = this.webex;
|
|
1365
|
+
const catalog = this._getCatalog();
|
|
1349
1366
|
|
|
1350
1367
|
// Init a promise chain. Must be done as a Promise.resolve() to allow
|
|
1351
1368
|
// credentials#getOrgId() to properly throw.
|
|
@@ -1358,12 +1375,18 @@ const Services = WebexPlugin.extend({
|
|
|
1358
1375
|
.then(() => {
|
|
1359
1376
|
// Validate if the token is authorized.
|
|
1360
1377
|
if (credentials.canAuthorize) {
|
|
1361
|
-
// Attempt to collect the postauth catalog
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
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
|
+
});
|
|
1367
1390
|
}
|
|
1368
1391
|
|
|
1369
1392
|
// Return a resolved promise for consistent return value.
|
|
@@ -1372,6 +1395,29 @@ const Services = WebexPlugin.extend({
|
|
|
1372
1395
|
);
|
|
1373
1396
|
},
|
|
1374
1397
|
|
|
1398
|
+
/**
|
|
1399
|
+
* Await any in-flight credentials refresh, then flip `services.ready` so
|
|
1400
|
+
* `webex.ready` can fire. Closes the parallel-refresh window: if a credential
|
|
1401
|
+
* refresh is in flight when initial catalog collection settles, we must not
|
|
1402
|
+
* signal ready until the refresh has resolved - otherwise downstream
|
|
1403
|
+
* consumers may observe `canAuthorize`/token state that is about to change
|
|
1404
|
+
* under them.
|
|
1405
|
+
*
|
|
1406
|
+
* @private
|
|
1407
|
+
* @returns {Promise<void>}
|
|
1408
|
+
*/
|
|
1409
|
+
async _finalizeReady() {
|
|
1410
|
+
const {credentials} = this.webex;
|
|
1411
|
+
|
|
1412
|
+
if (credentials && credentials.isRefreshing) {
|
|
1413
|
+
await new Promise((resolve) => {
|
|
1414
|
+
credentials.once('change:isRefreshing', resolve);
|
|
1415
|
+
});
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
this.ready = true;
|
|
1419
|
+
},
|
|
1420
|
+
|
|
1375
1421
|
/**
|
|
1376
1422
|
* Initializer
|
|
1377
1423
|
*
|
|
@@ -1388,11 +1434,39 @@ const Services = WebexPlugin.extend({
|
|
|
1388
1434
|
this.registries.set(this.webex, registry);
|
|
1389
1435
|
this.states.set(this.webex, state);
|
|
1390
1436
|
|
|
1391
|
-
// Listen for configuration changes once.
|
|
1437
|
+
// Listen for configuration changes once. The config is not populated on the
|
|
1438
|
+
// webex instance until the `change:config` event fires, so any decision that
|
|
1439
|
+
// depends on config values (such as the gated-vs-ungated init below) must be
|
|
1440
|
+
// made from within this handler rather than synchronously in `initialize()`.
|
|
1392
1441
|
this.listenToOnce(this.webex, 'change:config', () => {
|
|
1393
1442
|
this.initConfig();
|
|
1443
|
+
|
|
1444
|
+
// Feature flag: when enabled, `webex.ready` is blocked until the initial
|
|
1445
|
+
// catalog collection has settled AND any in-flight credentials refresh has
|
|
1446
|
+
// completed. When disabled (the default), preserves the pre-existing
|
|
1447
|
+
// behavior where `webex.ready` fires as soon as `webex.loaded` does and
|
|
1448
|
+
// the catalog is collected out-of-band.
|
|
1449
|
+
const waitForCatalogInit = this.webex.config?.services?.waitForCatalogInit === true;
|
|
1450
|
+
|
|
1451
|
+
if (waitForCatalogInit) {
|
|
1452
|
+
this._initializeCatalogsGated(catalog);
|
|
1453
|
+
} else {
|
|
1454
|
+
// Not gating - immediately mark ready so we do not block webex.ready.
|
|
1455
|
+
this.ready = true;
|
|
1456
|
+
this._initializeCatalogsUngated(catalog);
|
|
1457
|
+
}
|
|
1394
1458
|
});
|
|
1459
|
+
},
|
|
1395
1460
|
|
|
1461
|
+
/**
|
|
1462
|
+
* Original (pre-verified-ready) initialization path. Runs on `webex.ready`
|
|
1463
|
+
* and collects catalogs opportunistically without blocking anything.
|
|
1464
|
+
*
|
|
1465
|
+
* @private
|
|
1466
|
+
* @param {ServiceCatalog} catalog
|
|
1467
|
+
* @returns {void}
|
|
1468
|
+
*/
|
|
1469
|
+
_initializeCatalogsUngated(catalog) {
|
|
1396
1470
|
// wait for webex instance to be ready before attempting
|
|
1397
1471
|
// to update the service catalogs
|
|
1398
1472
|
// this can cause a race condition because credentials may
|
|
@@ -1407,16 +1481,14 @@ const Services = WebexPlugin.extend({
|
|
|
1407
1481
|
const {supertoken} = this.webex.credentials;
|
|
1408
1482
|
// Validate if the supertoken exists.
|
|
1409
1483
|
if (supertoken && supertoken.access_token) {
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
.
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
);
|
|
1419
|
-
});
|
|
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
|
+
});
|
|
1420
1492
|
} else {
|
|
1421
1493
|
const {email} = this.webex.config;
|
|
1422
1494
|
|
|
@@ -1429,6 +1501,87 @@ const Services = WebexPlugin.extend({
|
|
|
1429
1501
|
}
|
|
1430
1502
|
});
|
|
1431
1503
|
},
|
|
1504
|
+
|
|
1505
|
+
/**
|
|
1506
|
+
* Verified-ready initialization path. Blocks `webex.ready` until the initial
|
|
1507
|
+
* catalog fetch has settled (or timed out) AND any in-flight credentials
|
|
1508
|
+
* refresh has completed. Also handles the fresh-login case where OAuth
|
|
1509
|
+
* completes after `loaded` fires.
|
|
1510
|
+
*
|
|
1511
|
+
* @private
|
|
1512
|
+
* @param {ServiceCatalog} catalog
|
|
1513
|
+
* @returns {void}
|
|
1514
|
+
*/
|
|
1515
|
+
_initializeCatalogsGated(catalog) {
|
|
1516
|
+
// Wait for storage to be loaded before attempting to update the service
|
|
1517
|
+
// catalogs. We listen for 'loaded' instead of 'ready' because `services.ready`
|
|
1518
|
+
// now blocks `webex.ready` - listening to 'ready' would deadlock.
|
|
1519
|
+
this.listenToOnce(this.webex, 'loaded', async () => {
|
|
1520
|
+
const cachedCatalog = await this._loadCatalogFromCache();
|
|
1521
|
+
if (cachedCatalog) {
|
|
1522
|
+
catalog.isReady = true;
|
|
1523
|
+
await this._finalizeReady();
|
|
1524
|
+
|
|
1525
|
+
return; // skip initServiceCatalogs() on reload when cache exists
|
|
1526
|
+
}
|
|
1527
|
+
const {supertoken} = this.webex.credentials;
|
|
1528
|
+
|
|
1529
|
+
// Race init against a hard timeout so a hung request never leaves
|
|
1530
|
+
// `services.ready` false forever - that would stall `webex.ready` and
|
|
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) => {
|
|
1536
|
+
setTimeout(
|
|
1537
|
+
() => reject(new Error(`services: init timed out after ${initTimeoutMs}ms`)),
|
|
1538
|
+
initTimeoutMs
|
|
1539
|
+
);
|
|
1540
|
+
});
|
|
1541
|
+
|
|
1542
|
+
// Validate if the supertoken exists.
|
|
1543
|
+
if (supertoken && supertoken.access_token) {
|
|
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])
|
|
1548
|
+
.catch((error) => {
|
|
1549
|
+
this.initFailed = true;
|
|
1550
|
+
this.logger.error(
|
|
1551
|
+
`services: failed to init initial services when credentials available, ${error?.message}`
|
|
1552
|
+
);
|
|
1553
|
+
})
|
|
1554
|
+
.finally(() => this._finalizeReady());
|
|
1555
|
+
} else {
|
|
1556
|
+
const {email} = this.webex.config;
|
|
1557
|
+
|
|
1558
|
+
Promise.race([
|
|
1559
|
+
this.collectPreauthCatalog(email ? {email} : undefined),
|
|
1560
|
+
initServiceCatalogsTimeout,
|
|
1561
|
+
])
|
|
1562
|
+
.catch((error) => {
|
|
1563
|
+
this.initFailed = true;
|
|
1564
|
+
this.logger.error(
|
|
1565
|
+
`services: failed to init initial services when no credentials available, ${error?.message}`
|
|
1566
|
+
);
|
|
1567
|
+
})
|
|
1568
|
+
.finally(() => this._finalizeReady());
|
|
1569
|
+
|
|
1570
|
+
// Handle fresh login: 'loaded' fires before OAuth completes, so listen
|
|
1571
|
+
// for `canAuthorize` flipping true and then collect the postauth catalog.
|
|
1572
|
+
this.listenToOnce(this.webex, 'change:canAuthorize', () => {
|
|
1573
|
+
if (this.webex.canAuthorize && !catalog.status.postauth.ready) {
|
|
1574
|
+
// `initServiceCatalogs` marks the catalog ready internally.
|
|
1575
|
+
this.initServiceCatalogs().catch((error) => {
|
|
1576
|
+
this.logger.error(
|
|
1577
|
+
`services: failed to init service catalogs after auth, ${error?.message}`
|
|
1578
|
+
);
|
|
1579
|
+
});
|
|
1580
|
+
}
|
|
1581
|
+
});
|
|
1582
|
+
}
|
|
1583
|
+
});
|
|
1584
|
+
},
|
|
1432
1585
|
});
|
|
1433
1586
|
/* eslint-enable no-underscore-dangle */
|
|
1434
1587
|
|
|
@@ -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
|
/**
|
|
@@ -44,6 +44,22 @@ const Services = WebexPlugin.extend({
|
|
|
44
44
|
initFailed: ['boolean', false, false],
|
|
45
45
|
},
|
|
46
46
|
|
|
47
|
+
session: {
|
|
48
|
+
/**
|
|
49
|
+
* Becomes `true` once the initial catalog collection has completed
|
|
50
|
+
* (successfully or otherwise) and any in-flight credentials refresh has
|
|
51
|
+
* settled. Blocks `webex.ready` so consumers can rely on `webex.ready`
|
|
52
|
+
* implying "catalogs populated AND credential state stable".
|
|
53
|
+
* @instance
|
|
54
|
+
* @memberof Services
|
|
55
|
+
* @type {boolean}
|
|
56
|
+
*/
|
|
57
|
+
ready: {
|
|
58
|
+
default: false,
|
|
59
|
+
type: 'boolean',
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
|
|
47
63
|
_catalogs: new WeakMap(),
|
|
48
64
|
|
|
49
65
|
_activeServices: {},
|
|
@@ -1219,7 +1235,11 @@ const Services = WebexPlugin.extend({
|
|
|
1219
1235
|
): Promise<object> {
|
|
1220
1236
|
const service = 'u2c';
|
|
1221
1237
|
const resource = from ? `/${from}/catalog` : '/catalog';
|
|
1222
|
-
const qs = {
|
|
1238
|
+
const qs = {
|
|
1239
|
+
...(query || {}),
|
|
1240
|
+
format: 'U2CV2',
|
|
1241
|
+
...(this.webex.config?.services?.useCatalogOverride && {useCatalogOverride: true}),
|
|
1242
|
+
};
|
|
1223
1243
|
|
|
1224
1244
|
if (forceRefresh) {
|
|
1225
1245
|
qs.timestamp = new Date().getTime();
|
|
@@ -1312,6 +1332,7 @@ const Services = WebexPlugin.extend({
|
|
|
1312
1332
|
|
|
1313
1333
|
// Destructure the credentials plugin.
|
|
1314
1334
|
const {credentials} = this.webex;
|
|
1335
|
+
const catalog = this._getCatalog();
|
|
1315
1336
|
|
|
1316
1337
|
// Init a promise chain. Must be done as a Promise.resolve() to allow
|
|
1317
1338
|
// credentials#getOrgId() to properly throw.
|
|
@@ -1324,11 +1345,18 @@ const Services = WebexPlugin.extend({
|
|
|
1324
1345
|
.then(() => {
|
|
1325
1346
|
// Validate if the token is authorized.
|
|
1326
1347
|
if (credentials.canAuthorize) {
|
|
1327
|
-
// Attempt to collect the postauth catalog
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
})
|
|
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
|
+
});
|
|
1332
1360
|
}
|
|
1333
1361
|
|
|
1334
1362
|
// Return a resolved promise for consistent return value.
|
|
@@ -1337,6 +1365,29 @@ const Services = WebexPlugin.extend({
|
|
|
1337
1365
|
);
|
|
1338
1366
|
},
|
|
1339
1367
|
|
|
1368
|
+
/**
|
|
1369
|
+
* Await any in-flight credentials refresh, then flip `services.ready` so
|
|
1370
|
+
* `webex.ready` can fire. Closes the parallel-refresh window: if a credential
|
|
1371
|
+
* refresh is in flight when initial catalog collection settles, we must not
|
|
1372
|
+
* signal ready until the refresh has resolved - otherwise downstream
|
|
1373
|
+
* consumers may observe `canAuthorize`/token state that is about to change
|
|
1374
|
+
* under them.
|
|
1375
|
+
*
|
|
1376
|
+
* @private
|
|
1377
|
+
* @returns {Promise<void>}
|
|
1378
|
+
*/
|
|
1379
|
+
async _finalizeReady(): Promise<void> {
|
|
1380
|
+
const {credentials} = this.webex;
|
|
1381
|
+
|
|
1382
|
+
if (credentials && credentials.isRefreshing) {
|
|
1383
|
+
await new Promise<void>((resolve) => {
|
|
1384
|
+
credentials.once('change:isRefreshing', resolve);
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
this.ready = true;
|
|
1389
|
+
},
|
|
1390
|
+
|
|
1340
1391
|
/**
|
|
1341
1392
|
* Initializer
|
|
1342
1393
|
*
|
|
@@ -1348,11 +1399,39 @@ const Services = WebexPlugin.extend({
|
|
|
1348
1399
|
const catalog = new ServiceCatalog();
|
|
1349
1400
|
this._catalogs.set(this.webex, catalog);
|
|
1350
1401
|
|
|
1351
|
-
// Listen for configuration changes once.
|
|
1402
|
+
// Listen for configuration changes once. The config is not populated on the
|
|
1403
|
+
// webex instance until the `change:config` event fires, so any decision that
|
|
1404
|
+
// depends on config values (such as the gated-vs-ungated init below) must be
|
|
1405
|
+
// made from within this handler rather than synchronously in `initialize()`.
|
|
1352
1406
|
this.listenToOnce(this.webex, 'change:config', () => {
|
|
1353
1407
|
this.initConfig();
|
|
1408
|
+
|
|
1409
|
+
// Feature flag: when enabled, `webex.ready` is blocked until the initial
|
|
1410
|
+
// catalog collection has settled AND any in-flight credentials refresh has
|
|
1411
|
+
// completed. When disabled (the default), preserves the pre-existing
|
|
1412
|
+
// behavior where `webex.ready` fires as soon as `webex.loaded` does and
|
|
1413
|
+
// the catalog is collected out-of-band.
|
|
1414
|
+
const waitForCatalogInit = this.webex.config?.services?.waitForCatalogInit === true;
|
|
1415
|
+
|
|
1416
|
+
if (waitForCatalogInit) {
|
|
1417
|
+
this._initializeCatalogsGated(catalog);
|
|
1418
|
+
} else {
|
|
1419
|
+
// Not gating - immediately mark ready so we do not block webex.ready.
|
|
1420
|
+
this.ready = true;
|
|
1421
|
+
this._initializeCatalogsUngated(catalog);
|
|
1422
|
+
}
|
|
1354
1423
|
});
|
|
1424
|
+
},
|
|
1355
1425
|
|
|
1426
|
+
/**
|
|
1427
|
+
* Original (pre-verified-ready) initialization path. Runs on `webex.ready`
|
|
1428
|
+
* and collects catalogs opportunistically without blocking anything.
|
|
1429
|
+
*
|
|
1430
|
+
* @private
|
|
1431
|
+
* @param {ServiceCatalog} catalog
|
|
1432
|
+
* @returns {void}
|
|
1433
|
+
*/
|
|
1434
|
+
_initializeCatalogsUngated(catalog: ServiceCatalog): void {
|
|
1356
1435
|
// wait for webex instance to be ready before attempting
|
|
1357
1436
|
// to update the service catalogs
|
|
1358
1437
|
this.listenToOnce(this.webex, 'ready', async () => {
|
|
@@ -1365,16 +1444,14 @@ const Services = WebexPlugin.extend({
|
|
|
1365
1444
|
const {supertoken} = this.webex.credentials;
|
|
1366
1445
|
// Validate if the supertoken exists.
|
|
1367
1446
|
if (supertoken && supertoken.access_token) {
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
.
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
);
|
|
1377
|
-
});
|
|
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
|
+
});
|
|
1378
1455
|
} else {
|
|
1379
1456
|
const {email} = this.webex.config;
|
|
1380
1457
|
|
|
@@ -1387,6 +1464,86 @@ const Services = WebexPlugin.extend({
|
|
|
1387
1464
|
}
|
|
1388
1465
|
});
|
|
1389
1466
|
},
|
|
1467
|
+
|
|
1468
|
+
/**
|
|
1469
|
+
* Verified-ready initialization path. Blocks `webex.ready` until the initial
|
|
1470
|
+
* catalog fetch has settled (or timed out) AND any in-flight credentials
|
|
1471
|
+
* refresh has completed. Also handles the fresh-login case where OAuth
|
|
1472
|
+
* completes after `loaded` fires.
|
|
1473
|
+
*
|
|
1474
|
+
* @private
|
|
1475
|
+
* @param {ServiceCatalog} catalog
|
|
1476
|
+
* @returns {void}
|
|
1477
|
+
*/
|
|
1478
|
+
_initializeCatalogsGated(catalog: ServiceCatalog): void {
|
|
1479
|
+
// Wait for storage to be loaded before attempting to update the service
|
|
1480
|
+
// catalogs. We listen for 'loaded' instead of 'ready' because `services.ready`
|
|
1481
|
+
// now blocks `webex.ready` - listening to 'ready' would deadlock.
|
|
1482
|
+
this.listenToOnce(this.webex, 'loaded', async () => {
|
|
1483
|
+
const warmed = await this._loadCatalogFromCache();
|
|
1484
|
+
if (warmed) {
|
|
1485
|
+
catalog.isReady = true;
|
|
1486
|
+
await this._finalizeReady();
|
|
1487
|
+
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
const {supertoken} = this.webex.credentials;
|
|
1491
|
+
|
|
1492
|
+
// Race init against a hard timeout so a hung request never leaves
|
|
1493
|
+
// `services.ready` false forever - that would stall `webex.ready` and
|
|
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) => {
|
|
1498
|
+
setTimeout(
|
|
1499
|
+
() => reject(new Error(`services: init timed out after ${initTimeoutMs}ms`)),
|
|
1500
|
+
initTimeoutMs
|
|
1501
|
+
);
|
|
1502
|
+
});
|
|
1503
|
+
|
|
1504
|
+
// Validate if the supertoken exists.
|
|
1505
|
+
if (supertoken && supertoken.access_token) {
|
|
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])
|
|
1510
|
+
.catch((error) => {
|
|
1511
|
+
this.initFailed = true;
|
|
1512
|
+
this.logger.error(
|
|
1513
|
+
`services: failed to init initial services when credentials available, ${error?.message}`
|
|
1514
|
+
);
|
|
1515
|
+
})
|
|
1516
|
+
.finally(() => this._finalizeReady());
|
|
1517
|
+
} else {
|
|
1518
|
+
const {email} = this.webex.config;
|
|
1519
|
+
|
|
1520
|
+
Promise.race([
|
|
1521
|
+
this.collectPreauthCatalog(email ? {email} : undefined),
|
|
1522
|
+
initServiceCatalogsTimeout,
|
|
1523
|
+
])
|
|
1524
|
+
.catch((error) => {
|
|
1525
|
+
this.initFailed = true;
|
|
1526
|
+
this.logger.error(
|
|
1527
|
+
`services: failed to init initial services when no credentials available, ${error?.message}`
|
|
1528
|
+
);
|
|
1529
|
+
})
|
|
1530
|
+
.finally(() => this._finalizeReady());
|
|
1531
|
+
|
|
1532
|
+
// Handle fresh login: 'loaded' fires before OAuth completes, so listen
|
|
1533
|
+
// for `canAuthorize` flipping true and then collect the postauth catalog.
|
|
1534
|
+
this.listenToOnce(this.webex, 'change:canAuthorize', () => {
|
|
1535
|
+
if (this.webex.canAuthorize && !catalog.status.postauth.ready) {
|
|
1536
|
+
// `initServiceCatalogs` marks the catalog ready internally.
|
|
1537
|
+
this.initServiceCatalogs().catch((error) => {
|
|
1538
|
+
this.logger.error(
|
|
1539
|
+
`services: failed to init service catalogs after auth, ${error?.message}`
|
|
1540
|
+
);
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
});
|
|
1544
|
+
}
|
|
1545
|
+
});
|
|
1546
|
+
},
|
|
1390
1547
|
});
|
|
1391
1548
|
/* eslint-enable no-underscore-dangle */
|
|
1392
1549
|
|
|
@@ -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;
|
|
@@ -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', () =>
|
|
@@ -430,6 +433,39 @@ describe('webex-core', () => {
|
|
|
430
433
|
done();
|
|
431
434
|
}, 2000);
|
|
432
435
|
});
|
|
436
|
+
|
|
437
|
+
it('blocks webex.ready until services.ready flips when waitForCatalogInit is enabled', async () => {
|
|
438
|
+
const gatedWebex = new WebexCore({
|
|
439
|
+
credentials: {supertoken: webexUser.token},
|
|
440
|
+
config: {services: {waitForCatalogInit: true}},
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
// Before init settles, webex.ready must be false because services.ready
|
|
444
|
+
// is a dependency and starts false in the gated path.
|
|
445
|
+
assert.isFalse(gatedWebex.internal.services.ready, 'services.ready should start false');
|
|
446
|
+
assert.isFalse(gatedWebex.ready, 'webex.ready should not fire while services.ready is false');
|
|
447
|
+
|
|
448
|
+
// Wait up to 30s for services init to complete and flip ready.
|
|
449
|
+
await new Promise((resolve, reject) => {
|
|
450
|
+
if (gatedWebex.internal.services.ready) {
|
|
451
|
+
resolve();
|
|
452
|
+
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
const timer = setTimeout(
|
|
456
|
+
() => reject(new Error('timed out waiting for services.ready')),
|
|
457
|
+
30_000
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
gatedWebex.internal.services.once('change:ready', () => {
|
|
461
|
+
clearTimeout(timer);
|
|
462
|
+
resolve();
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
assert.isTrue(gatedWebex.internal.services.ready, 'services.ready should flip true after init settles');
|
|
467
|
+
assert.isTrue(gatedWebex.ready, 'webex.ready should fire once services.ready flips');
|
|
468
|
+
});
|
|
433
469
|
});
|
|
434
470
|
|
|
435
471
|
describe('#initServiceCatalogs()', () => {
|
|
@@ -892,7 +928,7 @@ describe('webex-core', () => {
|
|
|
892
928
|
|
|
893
929
|
it('validates a non-existing user', () =>
|
|
894
930
|
unauthServices
|
|
895
|
-
.validateUser({email:
|
|
931
|
+
.validateUser({email: createActivationEmail()})
|
|
896
932
|
.then((r) => {
|
|
897
933
|
assert.hasAllKeys(r, ['activated', 'exists', 'user', 'details']);
|
|
898
934
|
assert.equal(r.activated, false);
|
|
@@ -905,7 +941,7 @@ describe('webex-core', () => {
|
|
|
905
941
|
it('validates new user with activationOptions suppressEmail false', () =>
|
|
906
942
|
unauthServices
|
|
907
943
|
.validateUser({
|
|
908
|
-
email:
|
|
944
|
+
email: createActivationEmail(),
|
|
909
945
|
activationOptions: {suppressEmail: false},
|
|
910
946
|
})
|
|
911
947
|
.then((r) => {
|
|
@@ -921,7 +957,7 @@ describe('webex-core', () => {
|
|
|
921
957
|
it('validates new user with activationOptions suppressEmail true', () =>
|
|
922
958
|
unauthServices
|
|
923
959
|
.validateUser({
|
|
924
|
-
email:
|
|
960
|
+
email: createActivationEmail(),
|
|
925
961
|
activationOptions: {suppressEmail: true},
|
|
926
962
|
})
|
|
927
963
|
.then((r) => {
|
|
@@ -977,7 +1013,7 @@ describe('webex-core', () => {
|
|
|
977
1013
|
|
|
978
1014
|
return unauthServices
|
|
979
1015
|
.validateUser({
|
|
980
|
-
email:
|
|
1016
|
+
email: createActivationEmail(),
|
|
981
1017
|
activationOptions: {suppressEmail: true},
|
|
982
1018
|
})
|
|
983
1019
|
.then(() => {
|
|
@@ -991,7 +1027,7 @@ describe('webex-core', () => {
|
|
|
991
1027
|
|
|
992
1028
|
return unauthServices
|
|
993
1029
|
.validateUser({
|
|
994
|
-
email:
|
|
1030
|
+
email: createActivationEmail(),
|
|
995
1031
|
activationOptions: {suppressEmail: true},
|
|
996
1032
|
preloginUserId,
|
|
997
1033
|
})
|
|
@@ -1008,7 +1044,7 @@ describe('webex-core', () => {
|
|
|
1008
1044
|
|
|
1009
1045
|
return unauthServices
|
|
1010
1046
|
.validateUser({
|
|
1011
|
-
email:
|
|
1047
|
+
email: createActivationEmail(),
|
|
1012
1048
|
activationOptions: {suppressEmail: true},
|
|
1013
1049
|
})
|
|
1014
1050
|
.then(() => {
|
|
@@ -1031,7 +1067,7 @@ describe('webex-core', () => {
|
|
|
1031
1067
|
|
|
1032
1068
|
return userOnboardingServices
|
|
1033
1069
|
.validateUser({
|
|
1034
|
-
email:
|
|
1070
|
+
email: createActivationEmail(),
|
|
1035
1071
|
activationOptions: {suppressEmail: true},
|
|
1036
1072
|
})
|
|
1037
1073
|
.then(() => {
|