@webex/webex-core 3.11.0-webex-services-ready.1 → 3.12.0-auth-prejoin-fetch.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 (49) hide show
  1. package/README.md +5 -2
  2. package/dist/config.js +24 -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 +244 -57
  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 +243 -52
  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 +27 -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 +218 -46
  35. package/src/lib/services-v2/service-catalog.ts +6 -11
  36. package/src/lib/services-v2/services-v2.ts +218 -42
  37. package/test/fixtures/activation-email.ts +22 -0
  38. package/test/integration/spec/services/service-catalog.js +7 -6
  39. package/test/integration/spec/services/services.js +188 -128
  40. package/test/integration/spec/services-v2/services-v2.js +174 -114
  41. package/test/unit/spec/credentials/credentials.js +133 -2
  42. package/test/unit/spec/interceptors/auth.js +56 -0
  43. package/test/unit/spec/lib/batcher.js +56 -0
  44. package/test/unit/spec/services/service-catalog.js +93 -11
  45. package/test/unit/spec/services/services.js +591 -322
  46. package/test/unit/spec/services-v2/service-catalog.ts +93 -11
  47. package/test/unit/spec/services-v2/services-v2.ts +539 -214
  48. package/test/unit/spec/webex-core.js +0 -2
  49. package/test/unit/spec/webex-internal-core.js +0 -2
@@ -46,8 +46,10 @@ const Services = WebexPlugin.extend({
46
46
 
47
47
  session: {
48
48
  /**
49
- * Becomes `true` once services initialization has completed.
50
- * This blocks `webex.ready` until services are initialized.
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 = {...(query || {}), format: 'U2CV2'};
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();
@@ -1230,9 +1257,16 @@ const Services = WebexPlugin.extend({
1230
1257
  requestObject.headers = {authorization: token};
1231
1258
  }
1232
1259
 
1233
- return this.webex.internal.newMetrics.callDiagnosticLatencies
1234
- .measureLatency(() => this.request(requestObject), 'internal.get.u2c.time')
1235
- .then(({body}) => this._formatReceivedHostmap(body || {}));
1260
+ const sendRequest = () => this.request(requestObject);
1261
+
1262
+ const responsePromise = this.webex.internal.newMetrics
1263
+ ? this.webex.internal.newMetrics.callDiagnosticLatencies.measureLatency(
1264
+ sendRequest,
1265
+ 'internal.get.u2c.time'
1266
+ )
1267
+ : sendRequest();
1268
+
1269
+ return responsePromise.then(({body}) => this._formatReceivedHostmap(body || {}));
1236
1270
  },
1237
1271
 
1238
1272
  /**
@@ -1305,6 +1339,7 @@ const Services = WebexPlugin.extend({
1305
1339
 
1306
1340
  // Destructure the credentials plugin.
1307
1341
  const {credentials} = this.webex;
1342
+ const catalog = this._getCatalog();
1308
1343
 
1309
1344
  // Init a promise chain. Must be done as a Promise.resolve() to allow
1310
1345
  // credentials#getOrgId() to properly throw.
@@ -1317,11 +1352,18 @@ const Services = WebexPlugin.extend({
1317
1352
  .then(() => {
1318
1353
  // Validate if the token is authorized.
1319
1354
  if (credentials.canAuthorize) {
1320
- // Attempt to collect the postauth catalog.
1321
- return this.updateServices({forceRefresh: refresh}).catch(() => {
1322
- this.initFailed = true;
1323
- this.logger.warn('services: cannot retrieve postauth catalog');
1324
- });
1355
+ // Attempt to collect the postauth catalog, then mark the catalog
1356
+ // ready. Setting `isReady` here - rather than only in the init
1357
+ // callers - means a slow postauth fetch that loses the gated-init
1358
+ // timeout race still marks the catalog ready once it completes.
1359
+ return this.updateServices({forceRefresh: refresh})
1360
+ .then(() => {
1361
+ catalog.isReady = true;
1362
+ })
1363
+ .catch(() => {
1364
+ this.initFailed = true;
1365
+ this.logger.warn('services: cannot retrieve postauth catalog');
1366
+ });
1325
1367
  }
1326
1368
 
1327
1369
  // Return a resolved promise for consistent return value.
@@ -1330,6 +1372,51 @@ const Services = WebexPlugin.extend({
1330
1372
  );
1331
1373
  },
1332
1374
 
1375
+ /**
1376
+ * Await any in-flight credentials refresh, then flip `services.ready` so
1377
+ * `webex.ready` can fire. Closes the parallel-refresh window: if a credential
1378
+ * refresh is in flight when initial catalog collection settles, we must not
1379
+ * signal ready until the refresh has resolved - otherwise downstream
1380
+ * consumers may observe `canAuthorize`/token state that is about to change
1381
+ * under them.
1382
+ *
1383
+ * @private
1384
+ * @returns {Promise<void>}
1385
+ */
1386
+ async _finalizeReady(): Promise<void> {
1387
+ const {credentials} = this.webex;
1388
+
1389
+ if (credentials && credentials.isRefreshing) {
1390
+ await new Promise<void>((resolve) => {
1391
+ credentials.once('change:isRefreshing', resolve);
1392
+ });
1393
+ }
1394
+
1395
+ this.ready = true;
1396
+ },
1397
+
1398
+ /**
1399
+ * Build a promise that rejects once the catalog init timeout elapses. Race
1400
+ * this against catalog collection so a hung request never leaves
1401
+ * `services.ready` false forever - that would stall `webex.ready` and leave
1402
+ * consumers waiting on it indefinitely. Timeout is configurable via
1403
+ * `config.services.catalogInitTimeout` (defaults to 15s in config). Created
1404
+ * lazily so paths that skip catalog collection never schedule a stray timer.
1405
+ *
1406
+ * @private
1407
+ * @returns {Promise<never>}
1408
+ */
1409
+ _makeInitTimeout(): Promise<never> {
1410
+ const initTimeoutMs = this.webex.config?.services?.catalogInitTimeout;
1411
+
1412
+ return new Promise<never>((_, reject) => {
1413
+ setTimeout(
1414
+ () => reject(new Error(`services: init timed out after ${initTimeoutMs}ms`)),
1415
+ initTimeoutMs
1416
+ );
1417
+ });
1418
+ },
1419
+
1333
1420
  /**
1334
1421
  * Initializer
1335
1422
  *
@@ -1341,66 +1428,155 @@ const Services = WebexPlugin.extend({
1341
1428
  const catalog = new ServiceCatalog();
1342
1429
  this._catalogs.set(this.webex, catalog);
1343
1430
 
1344
- // Listen for configuration changes once.
1431
+ // Listen for configuration changes once. The config is not populated on the
1432
+ // webex instance until the `change:config` event fires, so any decision that
1433
+ // depends on config values (such as the gated-vs-ungated init below) must be
1434
+ // made from within this handler rather than synchronously in `initialize()`.
1345
1435
  this.listenToOnce(this.webex, 'change:config', () => {
1346
1436
  this.initConfig();
1437
+
1438
+ // Feature flag: when enabled, `webex.ready` is blocked until the initial
1439
+ // catalog collection has settled AND any in-flight credentials refresh has
1440
+ // completed. When disabled (the default), preserves the pre-existing
1441
+ // behavior where `webex.ready` fires as soon as `webex.loaded` does and
1442
+ // the catalog is collected out-of-band.
1443
+ const waitForCatalogInit = this.webex.config?.services?.waitForCatalogInit === true;
1444
+
1445
+ if (waitForCatalogInit) {
1446
+ this._initializeCatalogsGated(catalog);
1447
+ } else {
1448
+ // Not gating - immediately mark ready so we do not block webex.ready.
1449
+ this.ready = true;
1450
+ this._initializeCatalogsUngated(catalog);
1451
+ }
1347
1452
  });
1453
+ },
1348
1454
 
1455
+ /**
1456
+ * Original (pre-verified-ready) initialization path. Runs on `webex.ready`
1457
+ * and collects catalogs opportunistically without blocking anything.
1458
+ *
1459
+ * @private
1460
+ * @param {ServiceCatalog} catalog
1461
+ * @returns {void}
1462
+ */
1463
+ _initializeCatalogsUngated(catalog: ServiceCatalog): void {
1349
1464
  // wait for webex instance to be ready before attempting
1350
1465
  // to update the service catalogs
1466
+ this.listenToOnce(this.webex, 'ready', async () => {
1467
+ const warmed = await this._loadCatalogFromCache();
1468
+ if (warmed) {
1469
+ catalog.isReady = true;
1470
+
1471
+ return;
1472
+ }
1473
+ const {supertoken} = this.webex.credentials;
1474
+ // Validate if the supertoken exists.
1475
+ if (supertoken && supertoken.access_token) {
1476
+ // `initServiceCatalogs` marks the catalog ready internally once the
1477
+ // postauth catalog is collected.
1478
+ this.initServiceCatalogs().catch((error) => {
1479
+ this.initFailed = true;
1480
+ this.logger.error(
1481
+ `services: failed to init initial services when credentials available, ${error?.message}`
1482
+ );
1483
+ });
1484
+ } else {
1485
+ const {email} = this.webex.config;
1486
+
1487
+ if (this.webex.config?.services?.skipPreauthCatalogOnUnauthenticated === true) {
1488
+ this.logger.info(
1489
+ 'services: skipping preauth catalog collection while unauthenticated as per the config'
1490
+ );
1491
+
1492
+ return;
1493
+ }
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: ServiceCatalog): void {
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.
1351
1519
  this.listenToOnce(this.webex, 'loaded', async () => {
1352
1520
  const warmed = await this._loadCatalogFromCache();
1353
1521
  if (warmed) {
1354
1522
  catalog.isReady = true;
1523
+ await this._finalizeReady();
1355
1524
 
1356
1525
  return;
1357
1526
  }
1358
1527
  const {supertoken} = this.webex.credentials;
1528
+
1359
1529
  // Validate if the supertoken exists.
1360
1530
  if (supertoken && supertoken.access_token) {
1361
- this.initServiceCatalogs()
1362
- .then(() => {
1363
- catalog.isReady = true;
1364
- })
1531
+ // `initServiceCatalogs` marks the catalog ready internally once the
1532
+ // postauth catalog is collected - even if it loses the timeout race
1533
+ // below, so a slow fetch still eventually flips `catalog.isReady`.
1534
+ Promise.race([this.initServiceCatalogs(), this._makeInitTimeout()])
1365
1535
  .catch((error) => {
1366
1536
  this.initFailed = true;
1367
1537
  this.logger.error(
1368
1538
  `services: failed to init initial services when credentials available, ${error?.message}`
1369
1539
  );
1370
1540
  })
1371
- .finally(() => {
1372
- this.ready = true;
1373
- this.trigger('services:initialized');
1374
- });
1541
+ .finally(() => this._finalizeReady());
1375
1542
  } else {
1376
1543
  const {email} = this.webex.config;
1377
1544
 
1378
- this.collectPreauthCatalog(email ? {email} : undefined)
1379
- .catch((error) => {
1380
- this.initFailed = true;
1381
- this.logger.error(
1382
- `services: failed to init initial services when no credentials available, ${error?.message}`
1383
- );
1384
- })
1385
- .finally(() => {
1386
- this.ready = true;
1387
- this.trigger('services:initialized');
1388
- });
1389
- // Listen for when credentials become available to fetch the full catalog.
1390
- // This handles fresh login where 'loaded' fires before OAuth completes.
1545
+ // Handle fresh login: 'loaded' fires before OAuth completes, so listen
1546
+ // for `canAuthorize` flipping true and then collect the postauth catalog.
1391
1547
  this.listenToOnce(this.webex, 'change:canAuthorize', () => {
1392
1548
  if (this.webex.canAuthorize && !catalog.status.postauth.ready) {
1393
- this.initServiceCatalogs()
1394
- .then(() => {
1395
- catalog.isReady = true;
1396
- })
1397
- .catch((error) => {
1398
- this.logger.error(
1399
- `services: failed to init service catalogs after auth, ${error?.message}`
1400
- );
1401
- });
1549
+ // `initServiceCatalogs` marks the catalog ready internally.
1550
+ this.initServiceCatalogs().catch((error) => {
1551
+ this.logger.error(
1552
+ `services: failed to init service catalogs after auth, ${error?.message}`
1553
+ );
1554
+ });
1402
1555
  }
1403
1556
  });
1557
+
1558
+ if (this.webex.config?.services?.skipPreauthCatalogOnUnauthenticated === true) {
1559
+ // Skip the preauth catalog fetch (it will be collected manually
1560
+ // later), but still finalize `services.ready` so `webex.ready` is not
1561
+ // stalled while unauthenticated. No timeout is created here so there
1562
+ // is no stray timer or unhandled rejection.
1563
+ this.logger.info(
1564
+ 'services: skipping preauth catalog collection while unauthenticated as per the config'
1565
+ );
1566
+ this._finalizeReady();
1567
+ } else {
1568
+ Promise.race([
1569
+ this.collectPreauthCatalog(email ? {email} : undefined),
1570
+ this._makeInitTimeout(),
1571
+ ])
1572
+ .catch((error) => {
1573
+ this.initFailed = true;
1574
+ this.logger.error(
1575
+ `services: failed to init initial services when no credentials available, ${error?.message}`
1576
+ );
1577
+ })
1578
+ .finally(() => this._finalizeReady());
1579
+ }
1404
1580
  }
1405
1581
  });
1406
1582
  },
@@ -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
- webexUser = user;
27
- webex = new WebexCore({credentials: user.token});
28
- services = webex.internal.services;
29
- catalog = services._getCatalog();
30
- // Wait for webex ready event before registering device to ensure newMetrics.callDiagnosticMetrics is initialized
31
- webex.once('ready', resolve);
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())