@webex/webex-core 3.11.0-webex-services-ready.1 → 3.12.0-llmrefactor.1
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/interceptors/redirect.js +1 -1
- package/dist/interceptors/redirect.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 +208 -51
- 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 +207 -46
- 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/interceptors/redirect.js +4 -1
- 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 +174 -37
- package/src/lib/services-v2/service-catalog.ts +6 -11
- package/src/lib/services-v2/services-v2.ts +173 -33
- 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 +49 -24
- package/test/integration/spec/services-v2/services-v2.js +49 -24
- 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 +458 -322
- package/test/unit/spec/services-v2/service-catalog.ts +93 -11
- package/test/unit/spec/services-v2/services-v2.ts +403 -214
- package/test/unit/spec/webex-core.js +0 -2
- package/test/unit/spec/webex-internal-core.js +0 -2
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/* eslint-disable no-console */
|
|
2
1
|
import sha256 from 'crypto-js/sha256';
|
|
3
2
|
|
|
4
3
|
import {union, forEach} from 'lodash';
|
|
@@ -60,8 +59,10 @@ const Services = WebexPlugin.extend({
|
|
|
60
59
|
|
|
61
60
|
session: {
|
|
62
61
|
/**
|
|
63
|
-
* Becomes `true` once
|
|
64
|
-
*
|
|
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".
|
|
65
66
|
* @instance
|
|
66
67
|
* @memberof Services
|
|
67
68
|
* @type {boolean}
|
|
@@ -258,6 +259,28 @@ const Services = WebexPlugin.extend({
|
|
|
258
259
|
|
|
259
260
|
return !!hostCatalog[host]?.length;
|
|
260
261
|
},
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Checks if the current environment is an integration (INT) environment
|
|
265
|
+
* by examining the u2c discovery URL from webex config.
|
|
266
|
+
* INT environments use discovery URLs containing 'intb' (e.g., u2c-intb.ciscospark.com).
|
|
267
|
+
* @returns {boolean} True if INT environment, false otherwise
|
|
268
|
+
*/
|
|
269
|
+
isIntegrationEnvironment() {
|
|
270
|
+
try {
|
|
271
|
+
const u2cUrl = this.webex?.config?.services?.discovery?.u2c || '';
|
|
272
|
+
const isInt = u2cUrl.includes('intb');
|
|
273
|
+
|
|
274
|
+
this.logger.info(`services: isIntegrationEnvironment: ${isInt}`);
|
|
275
|
+
|
|
276
|
+
return isInt;
|
|
277
|
+
} catch (error) {
|
|
278
|
+
this.logger.error('services: failed to determine integration environment', error);
|
|
279
|
+
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
|
|
261
284
|
/**
|
|
262
285
|
* Merge provided active cluster mappings into current state.
|
|
263
286
|
* @param {Record<string,string>} activeServices
|
|
@@ -1339,6 +1362,7 @@ const Services = WebexPlugin.extend({
|
|
|
1339
1362
|
|
|
1340
1363
|
// Destructure the credentials plugin.
|
|
1341
1364
|
const {credentials} = this.webex;
|
|
1365
|
+
const catalog = this._getCatalog();
|
|
1342
1366
|
|
|
1343
1367
|
// Init a promise chain. Must be done as a Promise.resolve() to allow
|
|
1344
1368
|
// credentials#getOrgId() to properly throw.
|
|
@@ -1351,12 +1375,18 @@ const Services = WebexPlugin.extend({
|
|
|
1351
1375
|
.then(() => {
|
|
1352
1376
|
// Validate if the token is authorized.
|
|
1353
1377
|
if (credentials.canAuthorize) {
|
|
1354
|
-
// Attempt to collect the postauth catalog
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
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
|
+
});
|
|
1360
1390
|
}
|
|
1361
1391
|
|
|
1362
1392
|
// Return a resolved promise for consistent return value.
|
|
@@ -1365,6 +1395,29 @@ const Services = WebexPlugin.extend({
|
|
|
1365
1395
|
);
|
|
1366
1396
|
},
|
|
1367
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
|
+
|
|
1368
1421
|
/**
|
|
1369
1422
|
* Initializer
|
|
1370
1423
|
*
|
|
@@ -1381,65 +1434,149 @@ const Services = WebexPlugin.extend({
|
|
|
1381
1434
|
this.registries.set(this.webex, registry);
|
|
1382
1435
|
this.states.set(this.webex, state);
|
|
1383
1436
|
|
|
1384
|
-
// 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()`.
|
|
1385
1441
|
this.listenToOnce(this.webex, 'change:config', () => {
|
|
1386
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
|
+
}
|
|
1387
1458
|
});
|
|
1459
|
+
},
|
|
1388
1460
|
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
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) {
|
|
1470
|
+
// wait for webex instance to be ready before attempting
|
|
1471
|
+
// to update the service catalogs
|
|
1472
|
+
// this can cause a race condition because credentials may
|
|
1473
|
+
// not be valid when services is initialized
|
|
1474
|
+
this.listenToOnce(this.webex, 'ready', async () => {
|
|
1475
|
+
const cachedCatalog = await this._loadCatalogFromCache();
|
|
1476
|
+
if (cachedCatalog) {
|
|
1477
|
+
catalog.isReady = true;
|
|
1478
|
+
|
|
1479
|
+
return; // skip initServiceCatalogs() on reload when cache exists
|
|
1480
|
+
}
|
|
1481
|
+
const {supertoken} = this.webex.credentials;
|
|
1482
|
+
// Validate if the supertoken exists.
|
|
1483
|
+
if (supertoken && supertoken.access_token) {
|
|
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
|
+
});
|
|
1492
|
+
} else {
|
|
1493
|
+
const {email} = this.webex.config;
|
|
1494
|
+
|
|
1495
|
+
this.collectPreauthCatalog(email ? {email} : undefined).catch((error) => {
|
|
1496
|
+
this.initFailed = true;
|
|
1497
|
+
this.logger.error(
|
|
1498
|
+
`services: failed to init initial services when no credentials available, ${error?.message}`
|
|
1499
|
+
);
|
|
1500
|
+
});
|
|
1501
|
+
}
|
|
1502
|
+
});
|
|
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.
|
|
1392
1519
|
this.listenToOnce(this.webex, 'loaded', async () => {
|
|
1393
1520
|
const cachedCatalog = await this._loadCatalogFromCache();
|
|
1394
1521
|
if (cachedCatalog) {
|
|
1395
1522
|
catalog.isReady = true;
|
|
1523
|
+
await this._finalizeReady();
|
|
1396
1524
|
|
|
1397
1525
|
return; // skip initServiceCatalogs() on reload when cache exists
|
|
1398
1526
|
}
|
|
1399
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
|
+
|
|
1400
1542
|
// Validate if the supertoken exists.
|
|
1401
1543
|
if (supertoken && supertoken.access_token) {
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
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])
|
|
1406
1548
|
.catch((error) => {
|
|
1407
1549
|
this.initFailed = true;
|
|
1408
1550
|
this.logger.error(
|
|
1409
1551
|
`services: failed to init initial services when credentials available, ${error?.message}`
|
|
1410
1552
|
);
|
|
1411
1553
|
})
|
|
1412
|
-
.finally(() =>
|
|
1413
|
-
this.ready = true;
|
|
1414
|
-
this.trigger('services:initialized');
|
|
1415
|
-
});
|
|
1554
|
+
.finally(() => this._finalizeReady());
|
|
1416
1555
|
} else {
|
|
1417
1556
|
const {email} = this.webex.config;
|
|
1418
1557
|
|
|
1419
|
-
|
|
1558
|
+
Promise.race([
|
|
1559
|
+
this.collectPreauthCatalog(email ? {email} : undefined),
|
|
1560
|
+
initServiceCatalogsTimeout,
|
|
1561
|
+
])
|
|
1420
1562
|
.catch((error) => {
|
|
1421
1563
|
this.initFailed = true;
|
|
1422
1564
|
this.logger.error(
|
|
1423
1565
|
`services: failed to init initial services when no credentials available, ${error?.message}`
|
|
1424
1566
|
);
|
|
1425
1567
|
})
|
|
1426
|
-
.finally(() =>
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
// Listen for when credentials become available to fetch the full catalog.
|
|
1431
|
-
// This handles fresh login where 'loaded' fires before OAuth completes.
|
|
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.
|
|
1432
1572
|
this.listenToOnce(this.webex, 'change:canAuthorize', () => {
|
|
1433
1573
|
if (this.webex.canAuthorize && !catalog.status.postauth.ready) {
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
`services: failed to init service catalogs after auth, ${error?.message}`
|
|
1441
|
-
);
|
|
1442
|
-
});
|
|
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
|
+
});
|
|
1443
1580
|
}
|
|
1444
1581
|
});
|
|
1445
1582
|
}
|
|
@@ -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
|
/**
|
|
@@ -46,8 +46,10 @@ const Services = WebexPlugin.extend({
|
|
|
46
46
|
|
|
47
47
|
session: {
|
|
48
48
|
/**
|
|
49
|
-
* Becomes `true` once
|
|
50
|
-
*
|
|
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".
|
|
51
53
|
* @instance
|
|
52
54
|
* @memberof Services
|
|
53
55
|
* @type {boolean}
|
|
@@ -212,6 +214,27 @@ const Services = WebexPlugin.extend({
|
|
|
212
214
|
});
|
|
213
215
|
});
|
|
214
216
|
},
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Checks if the current environment is an integration (INT) environment
|
|
220
|
+
* by examining the u2c discovery URL from webex config.
|
|
221
|
+
* INT environments use discovery URLs containing 'intb' (e.g., u2c-intb.ciscospark.com).
|
|
222
|
+
* @returns {boolean} True if INT environment, false otherwise
|
|
223
|
+
*/
|
|
224
|
+
isIntegrationEnvironment(): boolean {
|
|
225
|
+
try {
|
|
226
|
+
const u2cUrl = this.webex?.config?.services?.discovery?.u2c || '';
|
|
227
|
+
const isInt = u2cUrl.includes('intb');
|
|
228
|
+
|
|
229
|
+
this.logger.info(`services: isIntegrationEnvironment: ${isInt}`);
|
|
230
|
+
|
|
231
|
+
return isInt;
|
|
232
|
+
} catch (error) {
|
|
233
|
+
this.logger.error('services: failed to determine integration environment', error);
|
|
234
|
+
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
},
|
|
215
238
|
/**
|
|
216
239
|
* saves all the services from the pre and post catalog service
|
|
217
240
|
* @param {ActiveServices} activeServices
|
|
@@ -1212,7 +1235,11 @@ const Services = WebexPlugin.extend({
|
|
|
1212
1235
|
): Promise<object> {
|
|
1213
1236
|
const service = 'u2c';
|
|
1214
1237
|
const resource = from ? `/${from}/catalog` : '/catalog';
|
|
1215
|
-
const qs = {
|
|
1238
|
+
const qs = {
|
|
1239
|
+
...(query || {}),
|
|
1240
|
+
format: 'U2CV2',
|
|
1241
|
+
...(this.webex.config?.services?.useCatalogOverride && {useCatalogOverride: true}),
|
|
1242
|
+
};
|
|
1216
1243
|
|
|
1217
1244
|
if (forceRefresh) {
|
|
1218
1245
|
qs.timestamp = new Date().getTime();
|
|
@@ -1305,6 +1332,7 @@ const Services = WebexPlugin.extend({
|
|
|
1305
1332
|
|
|
1306
1333
|
// Destructure the credentials plugin.
|
|
1307
1334
|
const {credentials} = this.webex;
|
|
1335
|
+
const catalog = this._getCatalog();
|
|
1308
1336
|
|
|
1309
1337
|
// Init a promise chain. Must be done as a Promise.resolve() to allow
|
|
1310
1338
|
// credentials#getOrgId() to properly throw.
|
|
@@ -1317,11 +1345,18 @@ const Services = WebexPlugin.extend({
|
|
|
1317
1345
|
.then(() => {
|
|
1318
1346
|
// Validate if the token is authorized.
|
|
1319
1347
|
if (credentials.canAuthorize) {
|
|
1320
|
-
// Attempt to collect the postauth catalog
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
})
|
|
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
|
+
});
|
|
1325
1360
|
}
|
|
1326
1361
|
|
|
1327
1362
|
// Return a resolved promise for consistent return value.
|
|
@@ -1330,6 +1365,29 @@ const Services = WebexPlugin.extend({
|
|
|
1330
1365
|
);
|
|
1331
1366
|
},
|
|
1332
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
|
+
|
|
1333
1391
|
/**
|
|
1334
1392
|
* Initializer
|
|
1335
1393
|
*
|
|
@@ -1341,64 +1399,146 @@ const Services = WebexPlugin.extend({
|
|
|
1341
1399
|
const catalog = new ServiceCatalog();
|
|
1342
1400
|
this._catalogs.set(this.webex, catalog);
|
|
1343
1401
|
|
|
1344
|
-
// 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()`.
|
|
1345
1406
|
this.listenToOnce(this.webex, 'change:config', () => {
|
|
1346
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
|
+
}
|
|
1347
1423
|
});
|
|
1424
|
+
},
|
|
1348
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 {
|
|
1349
1435
|
// wait for webex instance to be ready before attempting
|
|
1350
1436
|
// to update the service catalogs
|
|
1437
|
+
this.listenToOnce(this.webex, 'ready', async () => {
|
|
1438
|
+
const warmed = await this._loadCatalogFromCache();
|
|
1439
|
+
if (warmed) {
|
|
1440
|
+
catalog.isReady = true;
|
|
1441
|
+
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
1444
|
+
const {supertoken} = this.webex.credentials;
|
|
1445
|
+
// Validate if the supertoken exists.
|
|
1446
|
+
if (supertoken && supertoken.access_token) {
|
|
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
|
+
});
|
|
1455
|
+
} else {
|
|
1456
|
+
const {email} = this.webex.config;
|
|
1457
|
+
|
|
1458
|
+
this.collectPreauthCatalog(email ? {email} : undefined).catch((error) => {
|
|
1459
|
+
this.initFailed = true;
|
|
1460
|
+
this.logger.error(
|
|
1461
|
+
`services: failed to init initial services when no credentials available, ${error?.message}`
|
|
1462
|
+
);
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
});
|
|
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.
|
|
1351
1482
|
this.listenToOnce(this.webex, 'loaded', async () => {
|
|
1352
1483
|
const warmed = await this._loadCatalogFromCache();
|
|
1353
1484
|
if (warmed) {
|
|
1354
1485
|
catalog.isReady = true;
|
|
1486
|
+
await this._finalizeReady();
|
|
1355
1487
|
|
|
1356
1488
|
return;
|
|
1357
1489
|
}
|
|
1358
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
|
+
|
|
1359
1504
|
// Validate if the supertoken exists.
|
|
1360
1505
|
if (supertoken && supertoken.access_token) {
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
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])
|
|
1365
1510
|
.catch((error) => {
|
|
1366
1511
|
this.initFailed = true;
|
|
1367
1512
|
this.logger.error(
|
|
1368
1513
|
`services: failed to init initial services when credentials available, ${error?.message}`
|
|
1369
1514
|
);
|
|
1370
1515
|
})
|
|
1371
|
-
.finally(() =>
|
|
1372
|
-
this.ready = true;
|
|
1373
|
-
this.trigger('services:initialized');
|
|
1374
|
-
});
|
|
1516
|
+
.finally(() => this._finalizeReady());
|
|
1375
1517
|
} else {
|
|
1376
1518
|
const {email} = this.webex.config;
|
|
1377
1519
|
|
|
1378
|
-
|
|
1520
|
+
Promise.race([
|
|
1521
|
+
this.collectPreauthCatalog(email ? {email} : undefined),
|
|
1522
|
+
initServiceCatalogsTimeout,
|
|
1523
|
+
])
|
|
1379
1524
|
.catch((error) => {
|
|
1380
1525
|
this.initFailed = true;
|
|
1381
1526
|
this.logger.error(
|
|
1382
1527
|
`services: failed to init initial services when no credentials available, ${error?.message}`
|
|
1383
1528
|
);
|
|
1384
1529
|
})
|
|
1385
|
-
.finally(() =>
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
// Listen for when credentials become available to fetch the full catalog.
|
|
1390
|
-
// This handles fresh login where 'loaded' fires before OAuth completes.
|
|
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.
|
|
1391
1534
|
this.listenToOnce(this.webex, 'change:canAuthorize', () => {
|
|
1392
1535
|
if (this.webex.canAuthorize && !catalog.status.postauth.ready) {
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
`services: failed to init service catalogs after auth, ${error?.message}`
|
|
1400
|
-
);
|
|
1401
|
-
});
|
|
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
|
+
});
|
|
1402
1542
|
}
|
|
1403
1543
|
});
|
|
1404
1544
|
}
|
|
@@ -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())
|