@webex/webex-core 3.12.0-task-refactor.1 → 3.12.0-webex-services-ready.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 (41) hide show
  1. package/dist/config.js +7 -0
  2. package/dist/config.js.map +1 -1
  3. package/dist/credentials-config.js +12 -0
  4. package/dist/credentials-config.js.map +1 -1
  5. package/dist/interceptors/redirect.js +1 -1
  6. package/dist/interceptors/redirect.js.map +1 -1
  7. package/dist/lib/batcher.js +23 -7
  8. package/dist/lib/batcher.js.map +1 -1
  9. package/dist/lib/credentials/credentials.js +48 -4
  10. package/dist/lib/credentials/credentials.js.map +1 -1
  11. package/dist/lib/credentials/token.js +1 -1
  12. package/dist/lib/services/service-url.js +11 -1
  13. package/dist/lib/services/service-url.js.map +1 -1
  14. package/dist/lib/services/services.js +501 -96
  15. package/dist/lib/services/services.js.map +1 -1
  16. package/dist/lib/services-v2/services-v2.js +426 -42
  17. package/dist/lib/services-v2/services-v2.js.map +1 -1
  18. package/dist/lib/services-v2/types.js.map +1 -1
  19. package/dist/plugins/logger.js +1 -1
  20. package/dist/webex-core.js +2 -2
  21. package/dist/webex-core.js.map +1 -1
  22. package/package.json +13 -13
  23. package/src/config.js +7 -0
  24. package/src/credentials-config.js +13 -0
  25. package/src/interceptors/redirect.js +4 -1
  26. package/src/lib/batcher.js +25 -10
  27. package/src/lib/credentials/credentials.js +50 -3
  28. package/src/lib/services/service-url.js +9 -1
  29. package/src/lib/services/services.js +368 -14
  30. package/src/lib/services-v2/services-v2.ts +353 -10
  31. package/src/lib/services-v2/types.ts +5 -0
  32. package/test/integration/spec/services/service-catalog.js +16 -11
  33. package/test/integration/spec/services/services.js +38 -11
  34. package/test/integration/spec/services-v2/services-v2.js +29 -8
  35. package/test/unit/spec/credentials/credentials.js +133 -2
  36. package/test/unit/spec/lib/batcher.js +56 -0
  37. package/test/unit/spec/services/service-url.js +110 -0
  38. package/test/unit/spec/services/services.js +680 -80
  39. package/test/unit/spec/services-v2/services-v2.ts +484 -62
  40. package/test/unit/spec/webex-core.js +2 -0
  41. package/test/unit/spec/webex-internal-core.js +2 -0
@@ -11,12 +11,16 @@ _Object$defineProperty(exports, "__esModule", {
11
11
  value: true
12
12
  });
13
13
  exports.default = exports.DEFAULT_CLUSTER_SERVICE = exports.DEFAULT_CLUSTER = void 0;
14
+ var _regenerator = _interopRequireDefault(require("@babel/runtime-corejs2/regenerator"));
15
+ var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/asyncToGenerator"));
14
16
  var _defineProperty2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/defineProperty"));
15
17
  var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/slicedToArray"));
16
18
  var _weakMap = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/weak-map"));
17
19
  var _entries = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/object/entries"));
18
20
  var _keys = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/object/keys"));
19
21
  var _promise = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/promise"));
22
+ var _now = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/date/now"));
23
+ var _stringify = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/json/stringify"));
20
24
  var _sha = _interopRequireDefault(require("crypto-js/sha256"));
21
25
  var _lodash = require("lodash");
22
26
  var _webexPlugin = _interopRequireDefault(require("../webex-plugin"));
@@ -27,7 +31,7 @@ var _serviceState = _interopRequireDefault(require("./service-state"));
27
31
  var _serviceFedRamp = _interopRequireDefault(require("./service-fed-ramp"));
28
32
  var _constants = require("../constants");
29
33
  function ownKeys(e, r) { var t = _Object$keys2(e); if (_Object$getOwnPropertySymbols) { var o = _Object$getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return _Object$getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
30
- function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : _Object$getOwnPropertyDescriptors ? _Object$defineProperties(e, _Object$getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { _Object$defineProperty(e, r, _Object$getOwnPropertyDescriptor(t, r)); }); } return e; }
34
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : _Object$getOwnPropertyDescriptors ? _Object$defineProperties(e, _Object$getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { _Object$defineProperty(e, r, _Object$getOwnPropertyDescriptor(t, r)); }); } return e; } /* eslint-disable no-console */
31
35
  var trailingSlashes = /(?:^\/)|(?:\/$)/;
32
36
 
33
37
  // The default cluster when one is not provided (usually as 'US' from hydra)
@@ -36,6 +40,14 @@ var DEFAULT_CLUSTER = exports.DEFAULT_CLUSTER = 'urn:TEAM:us-east-2_a';
36
40
  var DEFAULT_CLUSTER_SERVICE = exports.DEFAULT_CLUSTER_SERVICE = 'identityLookup';
37
41
  var CLUSTER_SERVICE = process.env.WEBEX_CONVERSATION_CLUSTER_SERVICE || DEFAULT_CLUSTER_SERVICE;
38
42
  var DEFAULT_CLUSTER_IDENTIFIER = process.env.WEBEX_CONVERSATION_DEFAULT_CLUSTER || "".concat(DEFAULT_CLUSTER, ":").concat(CLUSTER_SERVICE);
43
+ var CATALOG_CACHE_KEY_V1 = 'services.v1.u2cHostMap';
44
+ var CATALOG_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
45
+
46
+ // Maximum time we will wait for the initial catalog collection before letting
47
+ // `services.ready` (and therefore `webex.ready`) fire anyway. A hung request
48
+ // must never leave the app on a permanent spinner - past this point downstream
49
+ // consumers must fall through to their normal error/login paths.
50
+ var SERVICES_INIT_TIMEOUT_MS = 15000;
39
51
 
40
52
  /* eslint-disable no-underscore-dangle */
41
53
  /**
@@ -67,6 +79,19 @@ var Services = _webexPlugin.default.extend({
67
79
  validateDomains: ['boolean', false, true],
68
80
  initFailed: ['boolean', false, false]
69
81
  },
82
+ session: {
83
+ /**
84
+ * Becomes `true` once service catalog initialization has completed.
85
+ * Blocks `webex.ready` until services are initialized.
86
+ * @instance
87
+ * @memberof Services
88
+ * @type {boolean}
89
+ */
90
+ ready: {
91
+ default: false,
92
+ type: 'boolean'
93
+ }
94
+ },
70
95
  _catalogs: new _weakMap.default(),
71
96
  _serviceUrls: null,
72
97
  _hostCatalog: null,
@@ -101,6 +126,45 @@ var Services = _webexPlugin.default.extend({
101
126
  _getCatalog: function _getCatalog() {
102
127
  return this._catalogs.get(this.webex);
103
128
  },
129
+ /**
130
+ * Safely access localStorage if available; returns the Storage or null.
131
+ * @returns {Storage|null}
132
+ */
133
+ _getLocalStorageSafe: function _getLocalStorageSafe() {
134
+ if (typeof window !== 'undefined' && window.localStorage) {
135
+ return window.localStorage;
136
+ }
137
+ return null;
138
+ },
139
+ /**
140
+ * Determine the intended preauth selection based on the current context.
141
+ * @param {string|undefined} currentOrgId
142
+ * @returns {{selectionType: string, selectionValue: string}}
143
+ */
144
+ getIntendedPreauthSelection: function getIntendedPreauthSelection(currentOrgId) {
145
+ var _this$webex$credentia;
146
+ if ((_this$webex$credentia = this.webex.credentials) !== null && _this$webex$credentia !== void 0 && _this$webex$credentia.canAuthorize) {
147
+ if (currentOrgId) {
148
+ return {
149
+ selectionType: 'orgId',
150
+ selectionValue: currentOrgId
151
+ };
152
+ }
153
+ }
154
+ var emailConfig = this.webex.config && this.webex.config.email;
155
+ if (typeof emailConfig === 'string' && emailConfig.trim()) {
156
+ return {
157
+ selectionType: 'emailhash',
158
+ selectionValue: (0, _sha.default)(emailConfig.toLowerCase()).toString()
159
+ };
160
+ }
161
+
162
+ // fall back to proximity mode when no orgId or email available
163
+ return {
164
+ selectionType: 'mode',
165
+ selectionValue: 'DEFAULT_BY_PROXIMITY'
166
+ };
167
+ },
104
168
  /**
105
169
  * Get a service url from the current services list by name
106
170
  * from the associated instance catalog.
@@ -161,9 +225,10 @@ var Services = _webexPlugin.default.extend({
161
225
  },
162
226
  /**
163
227
  * Get all Mobius cluster host entries from the legacy host catalog.
164
- * @returns {Array<Object>}
228
+ * @returns {Array<{host: string, id: string, ttl: number, priority: number}>}
165
229
  */
166
230
  getMobiusClusters: function getMobiusClusters() {
231
+ this.logger.info('services: fetching mobius clusters');
167
232
  var clusters = [];
168
233
  var hostCatalog = this._hostCatalog || {};
169
234
  (0, _entries.default)(hostCatalog).forEach(function (_ref) {
@@ -197,6 +262,24 @@ var Services = _webexPlugin.default.extend({
197
262
  var hostCatalog = this._hostCatalog || {};
198
263
  return !!((_hostCatalog$host = hostCatalog[host]) !== null && _hostCatalog$host !== void 0 && _hostCatalog$host.length);
199
264
  },
265
+ /**
266
+ * Checks if the current environment is an integration (INT) environment
267
+ * by examining the u2c discovery URL from webex config.
268
+ * INT environments use discovery URLs containing 'intb' (e.g., u2c-intb.ciscospark.com).
269
+ * @returns {boolean} True if INT environment, false otherwise
270
+ */
271
+ isIntegrationEnvironment: function isIntegrationEnvironment() {
272
+ try {
273
+ var _this$webex, _this$webex$config, _this$webex$config$se, _this$webex$config$se2;
274
+ var u2cUrl = ((_this$webex = this.webex) === null || _this$webex === void 0 ? void 0 : (_this$webex$config = _this$webex.config) === null || _this$webex$config === void 0 ? void 0 : (_this$webex$config$se = _this$webex$config.services) === null || _this$webex$config$se === void 0 ? void 0 : (_this$webex$config$se2 = _this$webex$config$se.discovery) === null || _this$webex$config$se2 === void 0 ? void 0 : _this$webex$config$se2.u2c) || '';
275
+ var isInt = u2cUrl.includes('intb');
276
+ this.logger.info("services: isIntegrationEnvironment: ".concat(isInt));
277
+ return isInt;
278
+ } catch (error) {
279
+ this.logger.error('services: failed to determine integration environment', error);
280
+ return false;
281
+ }
282
+ },
200
283
  /**
201
284
  * Merge provided active cluster mappings into current state.
202
285
  * @param {Record<string,string>} activeServices
@@ -235,63 +318,88 @@ var Services = _webexPlugin.default.extend({
235
318
  * @returns {Promise<object>}
236
319
  */
237
320
  updateServices: function updateServices() {
238
- var _this = this;
239
- var _ref3 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
240
- from = _ref3.from,
241
- query = _ref3.query,
242
- token = _ref3.token,
243
- forceRefresh = _ref3.forceRefresh;
244
- var catalog = this._getCatalog();
245
- var formattedQuery;
246
- var serviceGroup;
247
-
248
- // map catalog name to service group name.
249
- switch (from) {
250
- case 'limited':
251
- serviceGroup = 'preauth';
252
- break;
253
- case 'signin':
254
- serviceGroup = 'signin';
255
- break;
256
- default:
257
- serviceGroup = 'postauth';
258
- break;
259
- }
260
-
261
- // confirm catalog update for group is not in progress.
262
- if (catalog.status[serviceGroup].collecting) {
263
- return this.waitForCatalog(serviceGroup);
264
- }
265
- catalog.status[serviceGroup].collecting = true;
266
- if (serviceGroup === 'preauth') {
267
- var queryKey = query && (0, _keys.default)(query)[0];
268
- if (!['email', 'emailhash', 'userId', 'orgId', 'mode'].includes(queryKey)) {
269
- return _promise.default.reject(new Error('a query param of email, emailhash, userId, orgId, or mode is required'));
270
- }
271
- }
272
- // encode email when query key is email
273
- if (serviceGroup === 'preauth' || serviceGroup === 'signin') {
274
- var _queryKey = (0, _keys.default)(query)[0];
275
- formattedQuery = {};
276
- if (_queryKey === 'email' && query.email) {
277
- formattedQuery.emailhash = (0, _sha.default)(query.email.toLowerCase()).toString();
278
- } else {
279
- formattedQuery[_queryKey] = query[_queryKey];
280
- }
281
- }
282
- return this._fetchNewServiceHostmap({
283
- from: from,
284
- token: token,
285
- query: formattedQuery,
286
- forceRefresh: forceRefresh
287
- }).then(function (serviceHostMap) {
288
- catalog.updateServiceUrls(serviceGroup, serviceHostMap);
289
- _this.updateCredentialsConfig();
290
- catalog.status[serviceGroup].collecting = false;
291
- }).catch(function (error) {
292
- catalog.status[serviceGroup].collecting = false;
293
- return _promise.default.reject(error);
294
- });
321
+ var _arguments = arguments,
322
+ _this = this;
323
+ return (0, _asyncToGenerator2.default)(/*#__PURE__*/_regenerator.default.mark(function _callee() {
324
+ var _ref3, from, query, token, forceRefresh, catalog, formattedQuery, serviceGroup, queryKey, _queryKey, _t;
325
+ return _regenerator.default.wrap(function (_context) {
326
+ while (1) switch (_context.prev = _context.next) {
327
+ case 0:
328
+ _ref3 = _arguments.length > 0 && _arguments[0] !== undefined ? _arguments[0] : {}, from = _ref3.from, query = _ref3.query, token = _ref3.token, forceRefresh = _ref3.forceRefresh;
329
+ catalog = _this._getCatalog();
330
+ _t = from;
331
+ _context.next = _t === 'limited' ? 1 : _t === 'signin' ? 2 : 3;
332
+ break;
333
+ case 1:
334
+ serviceGroup = 'preauth';
335
+ return _context.abrupt("continue", 4);
336
+ case 2:
337
+ serviceGroup = 'signin';
338
+ return _context.abrupt("continue", 4);
339
+ case 3:
340
+ serviceGroup = 'postauth';
341
+ return _context.abrupt("continue", 4);
342
+ case 4:
343
+ if (!catalog.status[serviceGroup].collecting) {
344
+ _context.next = 5;
345
+ break;
346
+ }
347
+ return _context.abrupt("return", _this.waitForCatalog(serviceGroup));
348
+ case 5:
349
+ catalog.status[serviceGroup].collecting = true;
350
+ if (!(serviceGroup === 'preauth')) {
351
+ _context.next = 6;
352
+ break;
353
+ }
354
+ queryKey = query && (0, _keys.default)(query)[0];
355
+ if (['email', 'emailhash', 'userId', 'orgId', 'mode'].includes(queryKey)) {
356
+ _context.next = 6;
357
+ break;
358
+ }
359
+ return _context.abrupt("return", _promise.default.reject(new Error('a query param of email, emailhash, userId, orgId, or mode is required')));
360
+ case 6:
361
+ // encode email when query key is email
362
+ if (serviceGroup === 'preauth' || serviceGroup === 'signin') {
363
+ _queryKey = (0, _keys.default)(query)[0];
364
+ formattedQuery = {};
365
+ if (_queryKey === 'email' && query.email) {
366
+ formattedQuery.emailhash = (0, _sha.default)(query.email.toLowerCase()).toString();
367
+ } else {
368
+ formattedQuery[_queryKey] = query[_queryKey];
369
+ }
370
+ }
371
+ return _context.abrupt("return", _this._fetchNewServiceHostmap({
372
+ from: from,
373
+ token: token,
374
+ query: formattedQuery,
375
+ forceRefresh: forceRefresh
376
+ }).then(function (serviceHostMap) {
377
+ var formattedServiceHostMap = _this._formatReceivedHostmap(serviceHostMap);
378
+ // Build selection metadata for caching discrimination
379
+ var selectionMeta;
380
+ if (serviceGroup === 'preauth' || serviceGroup === 'signin') {
381
+ var key = formattedQuery && (0, _keys.default)(formattedQuery || {})[0];
382
+ if (key) {
383
+ selectionMeta = {
384
+ selectionType: key,
385
+ selectionValue: formattedQuery[key]
386
+ };
387
+ }
388
+ }
389
+ _this._cacheCatalog(serviceGroup, serviceHostMap, selectionMeta);
390
+ catalog.updateServiceUrls(serviceGroup, formattedServiceHostMap);
391
+ _this.updateCredentialsConfig();
392
+ catalog.status[serviceGroup].collecting = false;
393
+ }).catch(function (error) {
394
+ catalog.status[serviceGroup].collecting = false;
395
+ return _promise.default.reject(error);
396
+ }));
397
+ case 7:
398
+ case "end":
399
+ return _context.stop();
400
+ }
401
+ }, _callee);
402
+ }))();
295
403
  },
296
404
  /**
297
405
  * User validation parameter transfer object for {@link validateUser}.
@@ -362,9 +470,9 @@ var Services = _webexPlugin.default.extend({
362
470
 
363
471
  // Destructure the client authorization details.
364
472
  /* eslint-disable camelcase */
365
- var _this$webex$credentia = this.webex.credentials.config,
366
- client_id = _this$webex$credentia.client_id,
367
- client_secret = _this$webex$credentia.client_secret;
473
+ var _this$webex$credentia2 = this.webex.credentials.config,
474
+ client_id = _this$webex$credentia2.client_id,
475
+ client_secret = _this$webex$credentia2.client_secret;
368
476
 
369
477
  // Validate that client authentication details exist.
370
478
  if (!client_id || !client_secret) {
@@ -975,9 +1083,223 @@ var Services = _webexPlugin.default.extend({
975
1083
  return _this7.request(requestObject);
976
1084
  }, 'internal.get.u2c.time').then(function (_ref12) {
977
1085
  var body = _ref12.body;
978
- return _this7._formatReceivedHostmap(body);
1086
+ return body;
979
1087
  });
980
1088
  },
1089
+ /**
1090
+ * Cache the catalog in the bounded storage.
1091
+ * @param {string} serviceGroup - preauth, signin, postauth
1092
+ * @param {object} hostMap - The hostmap to cache
1093
+ * @param {object} [meta] - Optional selection metadata used to validate cache reuse
1094
+ * @returns {Promise<void>}
1095
+ *
1096
+ */
1097
+ _cacheCatalog: function _cacheCatalog(serviceGroup, hostMap, meta) {
1098
+ var _this8 = this;
1099
+ return (0, _asyncToGenerator2.default)(/*#__PURE__*/_regenerator.default.mark(function _callee2() {
1100
+ var current, orgId, _this8$webex$config, _this8$webex$config$c, _this8$webex, _this8$webex$config2, _this8$webex2, _this8$webex2$config, _this8$webex2$config$, _this8$webex2$config$2, _ls, cachedJson, credentials, _current, env, fedramp, u2cDiscoveryUrl, updated, ls, _t2;
1101
+ return _regenerator.default.wrap(function (_context2) {
1102
+ while (1) switch (_context2.prev = _context2.next) {
1103
+ case 0:
1104
+ current = {};
1105
+ _context2.prev = 1;
1106
+ if ((_this8$webex$config = _this8.webex.config) !== null && _this8$webex$config !== void 0 && (_this8$webex$config$c = _this8$webex$config.calling) !== null && _this8$webex$config$c !== void 0 && _this8$webex$config$c.cacheU2C) {
1107
+ _context2.next = 2;
1108
+ break;
1109
+ }
1110
+ _this8.logger.info("services: skipping cache write for ".concat(serviceGroup, " as per the config"));
1111
+ return _context2.abrupt("return");
1112
+ case 2:
1113
+ // Persist to localStorage to survive browser refresh
1114
+ try {
1115
+ _ls = _this8._getLocalStorageSafe();
1116
+ cachedJson = _ls ? _ls.getItem(CATALOG_CACHE_KEY_V1) : null;
1117
+ current = cachedJson ? JSON.parse(cachedJson) : {};
1118
+ } catch (e) {
1119
+ current = {};
1120
+ }
1121
+ try {
1122
+ credentials = _this8.webex.credentials;
1123
+ orgId = credentials.getOrgId();
1124
+ } catch (e) {
1125
+ orgId = current.orgId;
1126
+ }
1127
+
1128
+ // Capture environment fingerprint to invalidate cache across env changes
1129
+ _current = current, env = _current.env;
1130
+ fedramp = !!((_this8$webex = _this8.webex) !== null && _this8$webex !== void 0 && (_this8$webex$config2 = _this8$webex.config) !== null && _this8$webex$config2 !== void 0 && _this8$webex$config2.fedramp);
1131
+ u2cDiscoveryUrl = (_this8$webex2 = _this8.webex) === null || _this8$webex2 === void 0 ? void 0 : (_this8$webex2$config = _this8$webex2.config) === null || _this8$webex2$config === void 0 ? void 0 : (_this8$webex2$config$ = _this8$webex2$config.services) === null || _this8$webex2$config$ === void 0 ? void 0 : (_this8$webex2$config$2 = _this8$webex2$config$.discovery) === null || _this8$webex2$config$2 === void 0 ? void 0 : _this8$webex2$config$2.u2c;
1132
+ env = {
1133
+ fedramp: fedramp,
1134
+ u2cDiscoveryUrl: u2cDiscoveryUrl
1135
+ };
1136
+ updated = _objectSpread(_objectSpread({}, current), {}, (0, _defineProperty2.default)((0, _defineProperty2.default)({
1137
+ orgId: orgId || current.orgId,
1138
+ env: env || current.env
1139
+ }, serviceGroup, meta ? {
1140
+ hostMap: hostMap,
1141
+ meta: meta
1142
+ } : hostMap), "cachedAt", (0, _now.default)()));
1143
+ ls = _this8._getLocalStorageSafe();
1144
+ if (ls) {
1145
+ ls.setItem(CATALOG_CACHE_KEY_V1, (0, _stringify.default)(updated));
1146
+ }
1147
+ _context2.next = 4;
1148
+ break;
1149
+ case 3:
1150
+ _context2.prev = 3;
1151
+ _t2 = _context2["catch"](1);
1152
+ _this8.logger.warn('services: error caching catalog', _t2);
1153
+ case 4:
1154
+ case "end":
1155
+ return _context2.stop();
1156
+ }
1157
+ }, _callee2, null, [[1, 3]]);
1158
+ }))();
1159
+ },
1160
+ /**
1161
+ * Load the catalog from cache and hydrate the in-memory ServiceCatalog.
1162
+ * @returns {Promise<boolean>} true if cache was loaded, false otherwise
1163
+ */
1164
+ _loadCatalogFromCache: function _loadCatalogFromCache() {
1165
+ var _this9 = this;
1166
+ return (0, _asyncToGenerator2.default)(/*#__PURE__*/_regenerator.default.mark(function _callee3() {
1167
+ var currentOrgId, _this9$webex$config, _this9$webex$config$c, _this9$webex$config2, _this9$webex$config3, _this9$webex$config3$, _this9$webex$config3$2, ls, cachedJson, cached, cachedAt, _this9$webex$credenti, credentials, fedramp, u2cDiscoveryUrl, currentEnv, sameEnv, catalog, groups, _t3, _t4;
1168
+ return _regenerator.default.wrap(function (_context3) {
1169
+ while (1) switch (_context3.prev = _context3.next) {
1170
+ case 0:
1171
+ _context3.prev = 0;
1172
+ if ((_this9$webex$config = _this9.webex.config) !== null && _this9$webex$config !== void 0 && (_this9$webex$config$c = _this9$webex$config.calling) !== null && _this9$webex$config$c !== void 0 && _this9$webex$config$c.cacheU2C) {
1173
+ _context3.next = 1;
1174
+ break;
1175
+ }
1176
+ _this9.logger.info('services: skipping cache warm-up as per the cache config');
1177
+ return _context3.abrupt("return", false);
1178
+ case 1:
1179
+ ls = _this9._getLocalStorageSafe();
1180
+ if (ls) {
1181
+ _context3.next = 2;
1182
+ break;
1183
+ }
1184
+ _this9.logger.info('services: skipping cache warm-up as no localStorage is available');
1185
+ return _context3.abrupt("return", false);
1186
+ case 2:
1187
+ cachedJson = ls.getItem(CATALOG_CACHE_KEY_V1);
1188
+ cached = cachedJson ? JSON.parse(cachedJson) : undefined;
1189
+ if (cached) {
1190
+ _context3.next = 3;
1191
+ break;
1192
+ }
1193
+ return _context3.abrupt("return", false);
1194
+ case 3:
1195
+ // TTL enforcement: clear if older than 24 hours
1196
+ cachedAt = Number(cached.cachedAt) || 0;
1197
+ if (!(!cachedAt || (0, _now.default)() - cachedAt > CATALOG_TTL_MS)) {
1198
+ _context3.next = 4;
1199
+ break;
1200
+ }
1201
+ _this9.clearCatalogCache();
1202
+ return _context3.abrupt("return", false);
1203
+ case 4:
1204
+ _context3.prev = 4;
1205
+ if (!((_this9$webex$credenti = _this9.webex.credentials) !== null && _this9$webex$credenti !== void 0 && _this9$webex$credenti.canAuthorize)) {
1206
+ _context3.next = 5;
1207
+ break;
1208
+ }
1209
+ credentials = _this9.webex.credentials;
1210
+ currentOrgId = credentials.getOrgId();
1211
+ if (!(cached.orgId && cached.orgId !== currentOrgId)) {
1212
+ _context3.next = 5;
1213
+ break;
1214
+ }
1215
+ return _context3.abrupt("return", false);
1216
+ case 5:
1217
+ _context3.next = 7;
1218
+ break;
1219
+ case 6:
1220
+ _context3.prev = 6;
1221
+ _t3 = _context3["catch"](4);
1222
+ _this9.logger.warn('services: error checking orgId', _t3);
1223
+ case 7:
1224
+ // Ensure cached environment matches current environment
1225
+ fedramp = !!((_this9$webex$config2 = _this9.webex.config) !== null && _this9$webex$config2 !== void 0 && _this9$webex$config2.fedramp);
1226
+ u2cDiscoveryUrl = (_this9$webex$config3 = _this9.webex.config) === null || _this9$webex$config3 === void 0 ? void 0 : (_this9$webex$config3$ = _this9$webex$config3.services) === null || _this9$webex$config3$ === void 0 ? void 0 : (_this9$webex$config3$2 = _this9$webex$config3$.discovery) === null || _this9$webex$config3$2 === void 0 ? void 0 : _this9$webex$config3$2.u2c;
1227
+ currentEnv = {
1228
+ fedramp: fedramp,
1229
+ u2cDiscoveryUrl: u2cDiscoveryUrl
1230
+ };
1231
+ if (!cached.env) {
1232
+ _context3.next = 8;
1233
+ break;
1234
+ }
1235
+ sameEnv = cached.env.fedramp === currentEnv.fedramp && cached.env.u2cDiscoveryUrl === currentEnv.u2cDiscoveryUrl;
1236
+ if (sameEnv) {
1237
+ _context3.next = 8;
1238
+ break;
1239
+ }
1240
+ _this9.logger.info('services: skipping cache warm due to environment mismatch');
1241
+ return _context3.abrupt("return", false);
1242
+ case 8:
1243
+ catalog = _this9._getCatalog(); // Apply any cached groups (with preauth selection validation if available)
1244
+ groups = ['preauth', 'signin', 'postauth'];
1245
+ groups.forEach(function (serviceGroup) {
1246
+ var cachedGroup = cached[serviceGroup];
1247
+ if (!cachedGroup) {
1248
+ return;
1249
+ }
1250
+
1251
+ // Support legacy (hostMap) and new ({hostMap, meta}) shapes
1252
+ var hostMap = cachedGroup && cachedGroup.hostMap ? cachedGroup.hostMap : cachedGroup;
1253
+ var meta = cachedGroup === null || cachedGroup === void 0 ? void 0 : cachedGroup.meta;
1254
+ if (serviceGroup === 'preauth' && meta) {
1255
+ // For proximity-based selection, always fetch fresh to respect IP/region changes
1256
+ if (meta.selectionType === 'mode') {
1257
+ _this9.logger.info('services: skipping preauth cache warm for proximity mode');
1258
+ return;
1259
+ }
1260
+ var intended = _this9.getIntendedPreauthSelection(currentOrgId);
1261
+ var matches = intended && intended.selectionType === meta.selectionType && intended.selectionValue === meta.selectionValue;
1262
+ if (!matches) {
1263
+ _this9.logger.info('services: skipping preauth cache warm due to selection mismatch');
1264
+ return;
1265
+ }
1266
+ }
1267
+ if (hostMap) {
1268
+ var formatted = _this9._formatReceivedHostmap(hostMap);
1269
+ catalog.updateServiceUrls(serviceGroup, formatted);
1270
+ }
1271
+ });
1272
+
1273
+ // Align credentials against warmed catalog
1274
+ _this9.updateCredentialsConfig();
1275
+ return _context3.abrupt("return", true);
1276
+ case 9:
1277
+ _context3.prev = 9;
1278
+ _t4 = _context3["catch"](0);
1279
+ _this9.logger.warn('services: error loading catalog from cache', _t4);
1280
+ return _context3.abrupt("return", false);
1281
+ case 10:
1282
+ case "end":
1283
+ return _context3.stop();
1284
+ }
1285
+ }, _callee3, null, [[0, 9], [4, 6]]);
1286
+ }))();
1287
+ },
1288
+ /**
1289
+ * Clear the catalog cache from the bounded storage.
1290
+ * @returns {Promise<void>}
1291
+ */
1292
+ clearCatalogCache: function clearCatalogCache() {
1293
+ try {
1294
+ var ls = this._getLocalStorageSafe();
1295
+ if (ls) {
1296
+ ls.removeItem(CATALOG_CACHE_KEY_V1);
1297
+ }
1298
+ } catch (e) {
1299
+ this.logger.warn('services: error clearing catalog cache', e);
1300
+ }
1301
+ return _promise.default.resolve();
1302
+ },
981
1303
  /**
982
1304
  * Initialize the discovery services and the whitelisted services.
983
1305
  *
@@ -986,9 +1308,9 @@ var Services = _webexPlugin.default.extend({
986
1308
  initConfig: function initConfig() {
987
1309
  // Get the catalog and destructure the services config.
988
1310
  var catalog = this._getCatalog();
989
- var _this$webex$config = this.webex.config,
990
- services = _this$webex$config.services,
991
- fedramp = _this$webex$config.fedramp;
1311
+ var _this$webex$config2 = this.webex.config,
1312
+ services = _this$webex$config2.services,
1313
+ fedramp = _this$webex$config2.fedramp;
992
1314
 
993
1315
  // Validate that the services configuration exists.
994
1316
  if (services) {
@@ -1042,7 +1364,7 @@ var Services = _webexPlugin.default.extend({
1042
1364
  * @returns {Promise<void, Error>} - Errors if the token is unavailable.
1043
1365
  */
1044
1366
  initServiceCatalogs: function initServiceCatalogs() {
1045
- var _this8 = this;
1367
+ var _this0 = this;
1046
1368
  this.logger.info('services: initializing initial service catalogs');
1047
1369
 
1048
1370
  // Destructure the credentials plugin.
@@ -1057,16 +1379,17 @@ var Services = _webexPlugin.default.extend({
1057
1379
  })
1058
1380
  // Begin collecting the preauth/limited catalog.
1059
1381
  .then(function (orgId) {
1060
- return _this8.collectPreauthCatalog({
1382
+ return _this0.collectPreauthCatalog({
1061
1383
  orgId: orgId
1062
1384
  });
1063
1385
  }).then(function () {
1064
1386
  // Validate if the token is authorized.
1065
1387
  if (credentials.canAuthorize) {
1066
1388
  // Attempt to collect the postauth catalog.
1067
- return _this8.updateServices().catch(function () {
1068
- _this8.initFailed = true;
1069
- _this8.logger.warn('services: cannot retrieve postauth catalog');
1389
+
1390
+ return _this0.updateServices().catch(function () {
1391
+ _this0.initFailed = true;
1392
+ _this0.logger.warn('services: cannot retrieve postauth catalog');
1070
1393
  });
1071
1394
  }
1072
1395
 
@@ -1074,6 +1397,43 @@ var Services = _webexPlugin.default.extend({
1074
1397
  return _promise.default.resolve();
1075
1398
  });
1076
1399
  },
1400
+ /**
1401
+ * Await any in-flight credentials refresh, then flip `services.ready` so
1402
+ * `webex.ready` can fire. Closes the parallel-refresh window: if a credential
1403
+ * refresh is in flight when initial catalog collection settles, we must not
1404
+ * signal ready until the refresh has resolved - otherwise downstream
1405
+ * consumers may observe `canAuthorize`/token state that is about to change
1406
+ * under them.
1407
+ *
1408
+ * @private
1409
+ * @returns {Promise<void>}
1410
+ */
1411
+ _finalizeReady: function _finalizeReady() {
1412
+ var _this1 = this;
1413
+ return (0, _asyncToGenerator2.default)(/*#__PURE__*/_regenerator.default.mark(function _callee4() {
1414
+ var credentials;
1415
+ return _regenerator.default.wrap(function (_context4) {
1416
+ while (1) switch (_context4.prev = _context4.next) {
1417
+ case 0:
1418
+ credentials = _this1.webex.credentials;
1419
+ if (!(credentials && credentials.isRefreshing)) {
1420
+ _context4.next = 1;
1421
+ break;
1422
+ }
1423
+ _context4.next = 1;
1424
+ return new _promise.default(function (resolve) {
1425
+ credentials.once('change:isRefreshing', resolve);
1426
+ });
1427
+ case 1:
1428
+ _this1.ready = true;
1429
+ _this1.trigger('services:initialized');
1430
+ case 2:
1431
+ case "end":
1432
+ return _context4.stop();
1433
+ }
1434
+ }, _callee4);
1435
+ }))();
1436
+ },
1077
1437
  /**
1078
1438
  * Initializer
1079
1439
  *
@@ -1082,7 +1442,7 @@ var Services = _webexPlugin.default.extend({
1082
1442
  * @returns {Services}
1083
1443
  */
1084
1444
  initialize: function initialize() {
1085
- var _this9 = this;
1445
+ var _this10 = this;
1086
1446
  var catalog = new _serviceCatalog.default();
1087
1447
  var registry = new _serviceRegistry.default();
1088
1448
  var state = new _serviceState.default();
@@ -1092,33 +1452,78 @@ var Services = _webexPlugin.default.extend({
1092
1452
 
1093
1453
  // Listen for configuration changes once.
1094
1454
  this.listenToOnce(this.webex, 'change:config', function () {
1095
- _this9.initConfig();
1455
+ _this10.initConfig();
1096
1456
  });
1097
1457
 
1098
- // wait for webex instance to be ready before attempting
1099
- // to update the service catalogs
1100
- this.listenToOnce(this.webex, 'ready', function () {
1101
- var supertoken = _this9.webex.credentials.supertoken;
1102
- // Validate if the supertoken exists.
1103
- if (supertoken && supertoken.access_token) {
1104
- _this9.initServiceCatalogs().then(function () {
1105
- catalog.isReady = true;
1106
- }).catch(function (error) {
1107
- _this9.initFailed = true;
1108
- _this9.logger.error("services: failed to init initial services when credentials available, ".concat(error === null || error === void 0 ? void 0 : error.message));
1109
- });
1110
- } else {
1111
- var email = _this9.webex.config.email;
1112
- _this9.collectPreauthCatalog(email ? {
1113
- email: email
1114
- } : undefined).catch(function (error) {
1115
- _this9.initFailed = true;
1116
- _this9.logger.error("services: failed to init initial services when no credentials available, ".concat(error === null || error === void 0 ? void 0 : error.message));
1117
- });
1118
- }
1119
- });
1458
+ // Wait for storage to be loaded before attempting to update the service catalogs.
1459
+ // We listen for 'loaded' instead of 'ready' because services.ready is a dependency
1460
+ // of webex.ready - listening to 'ready' would cause a deadlock.
1461
+ this.listenToOnce(this.webex, 'loaded', /*#__PURE__*/(0, _asyncToGenerator2.default)(/*#__PURE__*/_regenerator.default.mark(function _callee5() {
1462
+ var cachedCatalog, supertoken, timeout, email;
1463
+ return _regenerator.default.wrap(function (_context5) {
1464
+ while (1) switch (_context5.prev = _context5.next) {
1465
+ case 0:
1466
+ _context5.next = 1;
1467
+ return _this10._loadCatalogFromCache();
1468
+ case 1:
1469
+ cachedCatalog = _context5.sent;
1470
+ if (!cachedCatalog) {
1471
+ _context5.next = 3;
1472
+ break;
1473
+ }
1474
+ catalog.isReady = true;
1475
+ _context5.next = 2;
1476
+ return _this10._finalizeReady();
1477
+ case 2:
1478
+ return _context5.abrupt("return");
1479
+ case 3:
1480
+ supertoken = _this10.webex.credentials.supertoken; // Race init against a hard timeout so a hung request never leaves
1481
+ // `services.ready` false forever - that would stall `webex.ready` and
1482
+ // leave the app on a permanent spinner.
1483
+ timeout = new _promise.default(function (_, reject) {
1484
+ setTimeout(function () {
1485
+ return reject(new Error("services: init timed out after ".concat(SERVICES_INIT_TIMEOUT_MS, "ms")));
1486
+ }, SERVICES_INIT_TIMEOUT_MS);
1487
+ }); // Validate if the supertoken exists.
1488
+ if (supertoken && supertoken.access_token) {
1489
+ _promise.default.race([_this10.initServiceCatalogs(), timeout]).then(function () {
1490
+ catalog.isReady = true;
1491
+ }).catch(function (error) {
1492
+ _this10.initFailed = true;
1493
+ _this10.logger.error("services: failed to init initial services when credentials available, ".concat(error === null || error === void 0 ? void 0 : error.message));
1494
+ }).finally(function () {
1495
+ return _this10._finalizeReady();
1496
+ });
1497
+ } else {
1498
+ email = _this10.webex.config.email;
1499
+ _promise.default.race([_this10.collectPreauthCatalog(email ? {
1500
+ email: email
1501
+ } : undefined), timeout]).catch(function (error) {
1502
+ _this10.initFailed = true;
1503
+ _this10.logger.error("services: failed to init initial services when no credentials available, ".concat(error === null || error === void 0 ? void 0 : error.message));
1504
+ }).finally(function () {
1505
+ return _this10._finalizeReady();
1506
+ });
1507
+ // Listen for when credentials become available to fetch the full catalog.
1508
+ // This handles fresh login where 'loaded' fires before OAuth completes.
1509
+ _this10.listenToOnce(_this10.webex, 'change:canAuthorize', function () {
1510
+ if (_this10.webex.canAuthorize && !catalog.status.postauth.ready) {
1511
+ _this10.initServiceCatalogs().then(function () {
1512
+ catalog.isReady = true;
1513
+ }).catch(function (error) {
1514
+ _this10.logger.error("services: failed to init service catalogs after auth, ".concat(error === null || error === void 0 ? void 0 : error.message));
1515
+ });
1516
+ }
1517
+ });
1518
+ }
1519
+ case 4:
1520
+ case "end":
1521
+ return _context5.stop();
1522
+ }
1523
+ }, _callee5);
1524
+ })));
1120
1525
  },
1121
- version: "3.12.0-task-refactor.1"
1526
+ version: "3.12.0-webex-services-ready.1"
1122
1527
  });
1123
1528
  /* eslint-enable no-underscore-dangle */
1124
1529
  var _default = exports.default = Services;