@webex/webex-core 3.11.0 → 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.
Files changed (46) hide show
  1. package/README.md +5 -2
  2. package/dist/config.js +15 -0
  3. package/dist/config.js.map +1 -1
  4. package/dist/credentials-config.js +12 -0
  5. package/dist/credentials-config.js.map +1 -1
  6. package/dist/interceptors/redirect.js +1 -1
  7. package/dist/interceptors/redirect.js.map +1 -1
  8. package/dist/lib/batcher.js +23 -7
  9. package/dist/lib/batcher.js.map +1 -1
  10. package/dist/lib/credentials/credentials.js +48 -4
  11. package/dist/lib/credentials/credentials.js.map +1 -1
  12. package/dist/lib/credentials/token.js +1 -1
  13. package/dist/lib/domains.js +90 -0
  14. package/dist/lib/domains.js.map +1 -0
  15. package/dist/lib/services/service-catalog.js +6 -10
  16. package/dist/lib/services/service-catalog.js.map +1 -1
  17. package/dist/lib/services/services.js +217 -31
  18. package/dist/lib/services/services.js.map +1 -1
  19. package/dist/lib/services-v2/service-catalog.js +6 -12
  20. package/dist/lib/services-v2/service-catalog.js.map +1 -1
  21. package/dist/lib/services-v2/services-v2.js +221 -30
  22. package/dist/lib/services-v2/services-v2.js.map +1 -1
  23. package/dist/plugins/logger.js +1 -1
  24. package/dist/webex-core.js +2 -2
  25. package/dist/webex-core.js.map +1 -1
  26. package/package.json +13 -13
  27. package/src/config.js +17 -0
  28. package/src/credentials-config.js +13 -0
  29. package/src/interceptors/redirect.js +4 -1
  30. package/src/lib/batcher.js +25 -10
  31. package/src/lib/credentials/credentials.js +50 -3
  32. package/src/lib/domains.ts +94 -0
  33. package/src/lib/services/service-catalog.js +6 -10
  34. package/src/lib/services/services.js +192 -17
  35. package/src/lib/services-v2/service-catalog.ts +6 -11
  36. package/src/lib/services-v2/services-v2.ts +195 -17
  37. package/test/fixtures/activation-email.ts +22 -0
  38. package/test/integration/spec/services/services.js +44 -8
  39. package/test/integration/spec/services-v2/services-v2.js +44 -8
  40. package/test/unit/spec/credentials/credentials.js +133 -2
  41. package/test/unit/spec/interceptors/auth.js +56 -0
  42. package/test/unit/spec/lib/batcher.js +56 -0
  43. package/test/unit/spec/services/service-catalog.js +93 -11
  44. package/test/unit/spec/services/services.js +358 -2
  45. package/test/unit/spec/services-v2/service-catalog.ts +93 -11
  46. package/test/unit/spec/services-v2/services-v2.ts +362 -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,
@@ -243,6 +259,28 @@ const Services = WebexPlugin.extend({
243
259
 
244
260
  return !!hostCatalog[host]?.length;
245
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
+
246
284
  /**
247
285
  * Merge provided active cluster mappings into current state.
248
286
  * @param {Record<string,string>} activeServices
@@ -1324,6 +1362,7 @@ const Services = WebexPlugin.extend({
1324
1362
 
1325
1363
  // Destructure the credentials plugin.
1326
1364
  const {credentials} = this.webex;
1365
+ const catalog = this._getCatalog();
1327
1366
 
1328
1367
  // Init a promise chain. Must be done as a Promise.resolve() to allow
1329
1368
  // credentials#getOrgId() to properly throw.
@@ -1336,12 +1375,18 @@ const Services = WebexPlugin.extend({
1336
1375
  .then(() => {
1337
1376
  // Validate if the token is authorized.
1338
1377
  if (credentials.canAuthorize) {
1339
- // Attempt to collect the postauth catalog.
1340
-
1341
- return this.updateServices().catch(() => {
1342
- this.initFailed = true;
1343
- this.logger.warn('services: cannot retrieve postauth catalog');
1344
- });
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
+ });
1345
1390
  }
1346
1391
 
1347
1392
  // Return a resolved promise for consistent return value.
@@ -1350,6 +1395,29 @@ const Services = WebexPlugin.extend({
1350
1395
  );
1351
1396
  },
1352
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
+
1353
1421
  /**
1354
1422
  * Initializer
1355
1423
  *
@@ -1366,11 +1434,39 @@ const Services = WebexPlugin.extend({
1366
1434
  this.registries.set(this.webex, registry);
1367
1435
  this.states.set(this.webex, state);
1368
1436
 
1369
- // 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()`.
1370
1441
  this.listenToOnce(this.webex, 'change:config', () => {
1371
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
+ }
1372
1458
  });
1459
+ },
1373
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) {
1374
1470
  // wait for webex instance to be ready before attempting
1375
1471
  // to update the service catalogs
1376
1472
  // this can cause a race condition because credentials may
@@ -1385,16 +1481,14 @@ const Services = WebexPlugin.extend({
1385
1481
  const {supertoken} = this.webex.credentials;
1386
1482
  // Validate if the supertoken exists.
1387
1483
  if (supertoken && supertoken.access_token) {
1388
- this.initServiceCatalogs()
1389
- .then(() => {
1390
- catalog.isReady = true;
1391
- })
1392
- .catch((error) => {
1393
- this.initFailed = true;
1394
- this.logger.error(
1395
- `services: failed to init initial services when credentials available, ${error?.message}`
1396
- );
1397
- });
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
+ });
1398
1492
  } else {
1399
1493
  const {email} = this.webex.config;
1400
1494
 
@@ -1407,6 +1501,87 @@ const Services = WebexPlugin.extend({
1407
1501
  }
1408
1502
  });
1409
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
+ },
1410
1585
  });
1411
1586
  /* eslint-enable no-underscore-dangle */
1412
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
- try {
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 = [...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: {},
@@ -198,6 +214,27 @@ const Services = WebexPlugin.extend({
198
214
  });
199
215
  });
200
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
+ },
201
238
  /**
202
239
  * saves all the services from the pre and post catalog service
203
240
  * @param {ActiveServices} activeServices
@@ -1198,7 +1235,11 @@ const Services = WebexPlugin.extend({
1198
1235
  ): Promise<object> {
1199
1236
  const service = 'u2c';
1200
1237
  const resource = from ? `/${from}/catalog` : '/catalog';
1201
- const qs = {...(query || {}), format: 'U2CV2'};
1238
+ const qs = {
1239
+ ...(query || {}),
1240
+ format: 'U2CV2',
1241
+ ...(this.webex.config?.services?.useCatalogOverride && {useCatalogOverride: true}),
1242
+ };
1202
1243
 
1203
1244
  if (forceRefresh) {
1204
1245
  qs.timestamp = new Date().getTime();
@@ -1291,6 +1332,7 @@ const Services = WebexPlugin.extend({
1291
1332
 
1292
1333
  // Destructure the credentials plugin.
1293
1334
  const {credentials} = this.webex;
1335
+ const catalog = this._getCatalog();
1294
1336
 
1295
1337
  // Init a promise chain. Must be done as a Promise.resolve() to allow
1296
1338
  // credentials#getOrgId() to properly throw.
@@ -1303,11 +1345,18 @@ const Services = WebexPlugin.extend({
1303
1345
  .then(() => {
1304
1346
  // Validate if the token is authorized.
1305
1347
  if (credentials.canAuthorize) {
1306
- // Attempt to collect the postauth catalog.
1307
- return this.updateServices({forceRefresh: refresh}).catch(() => {
1308
- this.initFailed = true;
1309
- this.logger.warn('services: cannot retrieve postauth catalog');
1310
- });
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
+ });
1311
1360
  }
1312
1361
 
1313
1362
  // Return a resolved promise for consistent return value.
@@ -1316,6 +1365,29 @@ const Services = WebexPlugin.extend({
1316
1365
  );
1317
1366
  },
1318
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
+
1319
1391
  /**
1320
1392
  * Initializer
1321
1393
  *
@@ -1327,11 +1399,39 @@ const Services = WebexPlugin.extend({
1327
1399
  const catalog = new ServiceCatalog();
1328
1400
  this._catalogs.set(this.webex, catalog);
1329
1401
 
1330
- // 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()`.
1331
1406
  this.listenToOnce(this.webex, 'change:config', () => {
1332
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
+ }
1333
1423
  });
1424
+ },
1334
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 {
1335
1435
  // wait for webex instance to be ready before attempting
1336
1436
  // to update the service catalogs
1337
1437
  this.listenToOnce(this.webex, 'ready', async () => {
@@ -1344,16 +1444,14 @@ const Services = WebexPlugin.extend({
1344
1444
  const {supertoken} = this.webex.credentials;
1345
1445
  // Validate if the supertoken exists.
1346
1446
  if (supertoken && supertoken.access_token) {
1347
- this.initServiceCatalogs()
1348
- .then(() => {
1349
- catalog.isReady = true;
1350
- })
1351
- .catch((error) => {
1352
- this.initFailed = true;
1353
- this.logger.error(
1354
- `services: failed to init initial services when credentials available, ${error?.message}`
1355
- );
1356
- });
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
+ });
1357
1455
  } else {
1358
1456
  const {email} = this.webex.config;
1359
1457
 
@@ -1366,6 +1464,86 @@ const Services = WebexPlugin.extend({
1366
1464
  }
1367
1465
  });
1368
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
+ },
1369
1547
  });
1370
1548
  /* eslint-enable no-underscore-dangle */
1371
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;