@webex/webex-core 3.12.0-task-refactor.1 → 3.12.0-webex-services-ready.2

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 +583 -94
  15. package/dist/lib/services/services.js.map +1 -1
  16. package/dist/lib/services-v2/services-v2.js +507 -41
  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 +433 -6
  30. package/src/lib/services-v2/services-v2.ts +417 -2
  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 +58 -9
  34. package/test/integration/spec/services-v2/services-v2.js +49 -6
  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 +692 -12
  39. package/test/unit/spec/services-v2/services-v2.ts +539 -0
  40. package/test/unit/spec/webex-core.js +2 -0
  41. package/test/unit/spec/webex-internal-core.js +2 -0
@@ -11,6 +11,8 @@ _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 _slicedToArray2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/slicedToArray"));
15
17
  var _defineProperty2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/defineProperty"));
16
18
  var _weakMap = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/weak-map"));
@@ -18,6 +20,8 @@ var _isArray = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/ar
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"));
20
22
  var _values = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/object/values"));
23
+ var _now = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/date/now"));
24
+ var _stringify = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/json/stringify"));
21
25
  var _sha = _interopRequireDefault(require("crypto-js/sha256"));
22
26
  var _lodash = require("lodash");
23
27
  var _webexPlugin = _interopRequireDefault(require("../webex-plugin"));
@@ -35,6 +39,14 @@ var DEFAULT_CLUSTER = exports.DEFAULT_CLUSTER = 'urn:TEAM:us-east-2_a';
35
39
  var DEFAULT_CLUSTER_SERVICE = exports.DEFAULT_CLUSTER_SERVICE = 'identityLookup';
36
40
  var CLUSTER_SERVICE = process.env.WEBEX_CONVERSATION_CLUSTER_SERVICE || DEFAULT_CLUSTER_SERVICE;
37
41
  var DEFAULT_CLUSTER_IDENTIFIER = process.env.WEBEX_CONVERSATION_DEFAULT_CLUSTER || "".concat(DEFAULT_CLUSTER, ":").concat(CLUSTER_SERVICE);
42
+ var CATALOG_CACHE_KEY_V2 = 'services.v2.u2cHostMap';
43
+ var CATALOG_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
44
+
45
+ // Maximum time we will wait for the initial catalog collection before letting
46
+ // `services.ready` (and therefore `webex.ready`) fire anyway. A hung request
47
+ // must never leave the app on a permanent spinner - past this point downstream
48
+ // consumers must fall through to their normal error/login paths.
49
+ var SERVICES_INIT_TIMEOUT_MS = 15000;
38
50
 
39
51
  /* eslint-disable no-underscore-dangle */
40
52
  /**
@@ -46,6 +58,21 @@ var Services = _webexPlugin.default.extend({
46
58
  validateDomains: ['boolean', false, true],
47
59
  initFailed: ['boolean', false, false]
48
60
  },
61
+ session: {
62
+ /**
63
+ * Becomes `true` once the initial catalog collection has completed
64
+ * (successfully or otherwise) and any in-flight credentials refresh has
65
+ * settled. Blocks `webex.ready` so consumers can rely on `webex.ready`
66
+ * implying "catalogs populated AND credential state stable".
67
+ * @instance
68
+ * @memberof Services
69
+ * @type {boolean}
70
+ */
71
+ ready: {
72
+ default: false,
73
+ type: 'boolean'
74
+ }
75
+ },
49
76
  _catalogs: new _weakMap.default(),
50
77
  _activeServices: {},
51
78
  _services: [],
@@ -58,6 +85,45 @@ var Services = _webexPlugin.default.extend({
58
85
  _getCatalog: function _getCatalog() {
59
86
  return this._catalogs.get(this.webex);
60
87
  },
88
+ /**
89
+ * Safely access localStorage if available; returns the Storage or null.
90
+ * @returns {Storage | null}
91
+ */
92
+ _getLocalStorageSafe: function _getLocalStorageSafe() {
93
+ if (typeof window !== 'undefined' && window.localStorage) {
94
+ return window.localStorage;
95
+ }
96
+ return null;
97
+ },
98
+ /**
99
+ * Determine the intended preauth selection based on the current context.
100
+ * @param {string} [currentOrgId]
101
+ * @returns {{selectionType: string, selectionValue: string}}
102
+ */
103
+ getIntendedPreauthSelection: function getIntendedPreauthSelection(currentOrgId) {
104
+ var _this$webex$credentia;
105
+ if ((_this$webex$credentia = this.webex.credentials) !== null && _this$webex$credentia !== void 0 && _this$webex$credentia.canAuthorize) {
106
+ if (currentOrgId) {
107
+ return {
108
+ selectionType: 'orgId',
109
+ selectionValue: currentOrgId
110
+ };
111
+ }
112
+ }
113
+ var emailConfig = this.webex.config && this.webex.config.email;
114
+ if (typeof emailConfig === 'string' && emailConfig.trim()) {
115
+ return {
116
+ selectionType: 'emailhash',
117
+ selectionValue: (0, _sha.default)(emailConfig.toLowerCase()).toString()
118
+ };
119
+ }
120
+
121
+ // fall back to proximity mode when no orgId or email available
122
+ return {
123
+ selectionType: 'mode',
124
+ selectionValue: 'DEFAULT_BY_PROXIMITY'
125
+ };
126
+ },
61
127
  /**
62
128
  * Get a service url from the current services list by name
63
129
  * from the associated instance catalog.
@@ -104,6 +170,7 @@ var Services = _webexPlugin.default.extend({
104
170
  * @returns {Array<ServiceHost>} - An array of `ServiceHost` objects.
105
171
  */
106
172
  getMobiusClusters: function getMobiusClusters() {
173
+ this.logger.info('services: fetching mobius clusters');
107
174
  var clusters = [];
108
175
  var services = this._services || [];
109
176
  services.filter(function (service) {
@@ -140,6 +207,24 @@ var Services = _webexPlugin.default.extend({
140
207
  });
141
208
  });
142
209
  },
210
+ /**
211
+ * Checks if the current environment is an integration (INT) environment
212
+ * by examining the u2c discovery URL from webex config.
213
+ * INT environments use discovery URLs containing 'intb' (e.g., u2c-intb.ciscospark.com).
214
+ * @returns {boolean} True if INT environment, false otherwise
215
+ */
216
+ isIntegrationEnvironment: function isIntegrationEnvironment() {
217
+ try {
218
+ var _this$webex, _this$webex$config, _this$webex$config$se, _this$webex$config$se2;
219
+ 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) || '';
220
+ var isInt = u2cUrl.includes('intb');
221
+ this.logger.info("services: isIntegrationEnvironment: ".concat(isInt));
222
+ return isInt;
223
+ } catch (error) {
224
+ this.logger.error('services: failed to determine integration environment', error);
225
+ return false;
226
+ }
227
+ },
143
228
  /**
144
229
  * saves all the services from the pre and post catalog service
145
230
  * @param {ActiveServices} activeServices
@@ -222,6 +307,18 @@ var Services = _webexPlugin.default.extend({
222
307
  forceRefresh: forceRefresh
223
308
  }).then(function (serviceHostMap) {
224
309
  catalog.updateServiceGroups(serviceGroup, serviceHostMap === null || serviceHostMap === void 0 ? void 0 : serviceHostMap.services, serviceHostMap === null || serviceHostMap === void 0 ? void 0 : serviceHostMap.timestamp);
310
+ // Build selection metadata for caching discrimination (preauth/signin)
311
+ var selectionMeta;
312
+ if (serviceGroup === 'preauth' || serviceGroup === 'signin') {
313
+ var key = formattedQuery && (0, _keys.default)(formattedQuery || {})[0];
314
+ if (key) {
315
+ selectionMeta = {
316
+ selectionType: key,
317
+ selectionValue: formattedQuery[key]
318
+ };
319
+ }
320
+ }
321
+ _this._cacheCatalog(serviceGroup, serviceHostMap, selectionMeta);
225
322
  _this.updateCredentialsConfig();
226
323
  catalog.status[serviceGroup].collecting = false;
227
324
  }).catch(function (error) {
@@ -298,9 +395,9 @@ var Services = _webexPlugin.default.extend({
298
395
 
299
396
  // Destructure the client authorization details.
300
397
  /* eslint-disable camelcase */
301
- var _this$webex$credentia = this.webex.credentials.config,
302
- client_id = _this$webex$credentia.client_id,
303
- client_secret = _this$webex$credentia.client_secret;
398
+ var _this$webex$credentia2 = this.webex.credentials.config,
399
+ client_id = _this$webex$credentia2.client_id,
400
+ client_secret = _this$webex$credentia2.client_secret;
304
401
 
305
402
  // Validate that client authentication details exist.
306
403
  if (!client_id || !client_secret) {
@@ -876,6 +973,213 @@ var Services = _webexPlugin.default.extend({
876
973
  }
877
974
  return url.replace(data.defaultUrl, data.priorityUrl);
878
975
  },
976
+ /**
977
+ * @private
978
+ * Cache the catalog in the bounded storage.
979
+ * @param {ServiceGroup} serviceGroup - preauth, signin, postauth
980
+ * @param {ServiceHostmap} hostMap - The hostmap to cache
981
+ * @param {object} [meta] - Optional selection metadata for cache discrimination
982
+ * @returns {Promise<void>}
983
+ */
984
+ _cacheCatalog: function _cacheCatalog(serviceGroup, hostMap, meta) {
985
+ var _this9 = this;
986
+ return (0, _asyncToGenerator2.default)(/*#__PURE__*/_regenerator.default.mark(function _callee() {
987
+ var current, orgId, _this9$webex$config, _this9$webex$config$c, _this9$webex, _this9$webex$config2, _this9$webex2, _this9$webex2$config, _this9$webex2$config$, _this9$webex2$config$2, _ls, cachedJson, credentials, _current, env, fedramp, u2cDiscoveryUrl, updated, ls, _t;
988
+ return _regenerator.default.wrap(function (_context) {
989
+ while (1) switch (_context.prev = _context.next) {
990
+ case 0:
991
+ current = {};
992
+ _context.prev = 1;
993
+ 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) {
994
+ _context.next = 2;
995
+ break;
996
+ }
997
+ _this9.logger.info("services: skipping cache write for ".concat(serviceGroup, " as per the config"));
998
+ return _context.abrupt("return");
999
+ case 2:
1000
+ try {
1001
+ _ls = _this9._getLocalStorageSafe();
1002
+ cachedJson = _ls ? _ls.getItem(CATALOG_CACHE_KEY_V2) : null;
1003
+ current = cachedJson ? JSON.parse(cachedJson) : {};
1004
+ } catch (_unused2) {
1005
+ current = {};
1006
+ }
1007
+ try {
1008
+ credentials = _this9.webex.credentials;
1009
+ orgId = credentials.getOrgId();
1010
+ } catch (_unused3) {
1011
+ orgId = current.orgId;
1012
+ }
1013
+
1014
+ // Capture environment fingerprint to invalidate cache across env changes
1015
+ _current = current, env = _current.env;
1016
+ fedramp = !!((_this9$webex = _this9.webex) !== null && _this9$webex !== void 0 && (_this9$webex$config2 = _this9$webex.config) !== null && _this9$webex$config2 !== void 0 && _this9$webex$config2.fedramp);
1017
+ u2cDiscoveryUrl = (_this9$webex2 = _this9.webex) === null || _this9$webex2 === void 0 ? void 0 : (_this9$webex2$config = _this9$webex2.config) === null || _this9$webex2$config === void 0 ? void 0 : (_this9$webex2$config$ = _this9$webex2$config.services) === null || _this9$webex2$config$ === void 0 ? void 0 : (_this9$webex2$config$2 = _this9$webex2$config$.discovery) === null || _this9$webex2$config$2 === void 0 ? void 0 : _this9$webex2$config$2.u2c;
1018
+ env = {
1019
+ fedramp: fedramp,
1020
+ u2cDiscoveryUrl: u2cDiscoveryUrl
1021
+ };
1022
+ updated = _objectSpread(_objectSpread({}, current), {}, (0, _defineProperty2.default)((0, _defineProperty2.default)({
1023
+ orgId: orgId || current.orgId,
1024
+ env: env || current.env
1025
+ }, serviceGroup, meta ? {
1026
+ hostMap: hostMap,
1027
+ meta: meta
1028
+ } : hostMap), "cachedAt", (0, _now.default)()));
1029
+ ls = _this9._getLocalStorageSafe();
1030
+ if (ls) {
1031
+ ls.setItem(CATALOG_CACHE_KEY_V2, (0, _stringify.default)(updated));
1032
+ }
1033
+ _context.next = 4;
1034
+ break;
1035
+ case 3:
1036
+ _context.prev = 3;
1037
+ _t = _context["catch"](1);
1038
+ _this9.logger.warn('services: error caching catalog', _t);
1039
+ case 4:
1040
+ case "end":
1041
+ return _context.stop();
1042
+ }
1043
+ }, _callee, null, [[1, 3]]);
1044
+ }))();
1045
+ },
1046
+ /**
1047
+ * @private
1048
+ * Load the catalog from cache and hydrate the in-memory ServiceCatalog.
1049
+ * @returns {Promise<boolean>} true if cache was loaded, false otherwise
1050
+ */
1051
+ _loadCatalogFromCache: function _loadCatalogFromCache() {
1052
+ var _this0 = this;
1053
+ return (0, _asyncToGenerator2.default)(/*#__PURE__*/_regenerator.default.mark(function _callee2() {
1054
+ var currentOrgId, _this0$webex$config, _this0$webex$config$c, _this0$webex$config2, _this0$webex$config3, _this0$webex$config3$, _this0$webex$config3$2, ls, cachedJson, cached, cachedAt, _this0$webex$credenti, credentials, fedramp, u2cDiscoveryUrl, currentEnv, sameEnv, catalog, groups, _t2, _t3;
1055
+ return _regenerator.default.wrap(function (_context2) {
1056
+ while (1) switch (_context2.prev = _context2.next) {
1057
+ case 0:
1058
+ _context2.prev = 0;
1059
+ if ((_this0$webex$config = _this0.webex.config) !== null && _this0$webex$config !== void 0 && (_this0$webex$config$c = _this0$webex$config.calling) !== null && _this0$webex$config$c !== void 0 && _this0$webex$config$c.cacheU2C) {
1060
+ _context2.next = 1;
1061
+ break;
1062
+ }
1063
+ _this0.logger.info('services: skipping cache warm-up as per the cache config');
1064
+ return _context2.abrupt("return", false);
1065
+ case 1:
1066
+ ls = _this0._getLocalStorageSafe();
1067
+ if (ls) {
1068
+ _context2.next = 2;
1069
+ break;
1070
+ }
1071
+ _this0.logger.info('services: skipping cache warm-up as no localStorage is available');
1072
+ return _context2.abrupt("return", false);
1073
+ case 2:
1074
+ cachedJson = ls.getItem(CATALOG_CACHE_KEY_V2);
1075
+ cached = cachedJson ? JSON.parse(cachedJson) : undefined;
1076
+ if (cached) {
1077
+ _context2.next = 3;
1078
+ break;
1079
+ }
1080
+ return _context2.abrupt("return", false);
1081
+ case 3:
1082
+ // TTL enforcement
1083
+ cachedAt = Number(cached.cachedAt) || 0;
1084
+ if (!(!cachedAt || (0, _now.default)() - cachedAt > CATALOG_TTL_MS)) {
1085
+ _context2.next = 4;
1086
+ break;
1087
+ }
1088
+ _this0.clearCatalogCache();
1089
+ return _context2.abrupt("return", false);
1090
+ case 4:
1091
+ _context2.prev = 4;
1092
+ if (!((_this0$webex$credenti = _this0.webex.credentials) !== null && _this0$webex$credenti !== void 0 && _this0$webex$credenti.canAuthorize)) {
1093
+ _context2.next = 5;
1094
+ break;
1095
+ }
1096
+ credentials = _this0.webex.credentials;
1097
+ currentOrgId = credentials.getOrgId();
1098
+ if (!(cached.orgId && cached.orgId !== currentOrgId)) {
1099
+ _context2.next = 5;
1100
+ break;
1101
+ }
1102
+ return _context2.abrupt("return", false);
1103
+ case 5:
1104
+ _context2.next = 7;
1105
+ break;
1106
+ case 6:
1107
+ _context2.prev = 6;
1108
+ _t2 = _context2["catch"](4);
1109
+ _this0.logger.warn('services: error checking orgId', _t2);
1110
+ case 7:
1111
+ // Ensure cached environment matches current environment
1112
+ fedramp = !!((_this0$webex$config2 = _this0.webex.config) !== null && _this0$webex$config2 !== void 0 && _this0$webex$config2.fedramp);
1113
+ u2cDiscoveryUrl = (_this0$webex$config3 = _this0.webex.config) === null || _this0$webex$config3 === void 0 ? void 0 : (_this0$webex$config3$ = _this0$webex$config3.services) === null || _this0$webex$config3$ === void 0 ? void 0 : (_this0$webex$config3$2 = _this0$webex$config3$.discovery) === null || _this0$webex$config3$2 === void 0 ? void 0 : _this0$webex$config3$2.u2c;
1114
+ currentEnv = {
1115
+ fedramp: fedramp,
1116
+ u2cDiscoveryUrl: u2cDiscoveryUrl
1117
+ };
1118
+ if (!cached.env) {
1119
+ _context2.next = 8;
1120
+ break;
1121
+ }
1122
+ sameEnv = cached.env.fedramp === currentEnv.fedramp && cached.env.u2cDiscoveryUrl === currentEnv.u2cDiscoveryUrl;
1123
+ if (sameEnv) {
1124
+ _context2.next = 8;
1125
+ break;
1126
+ }
1127
+ return _context2.abrupt("return", false);
1128
+ case 8:
1129
+ catalog = _this0._getCatalog();
1130
+ groups = ['preauth', 'signin', 'postauth'];
1131
+ groups.forEach(function (serviceGroup) {
1132
+ var cachedGroup = cached[serviceGroup];
1133
+ if (!cachedGroup) {
1134
+ return;
1135
+ }
1136
+
1137
+ // Support legacy (hostMap) and new ({hostMap, meta}) shapes
1138
+ var hostMap = cachedGroup && cachedGroup.hostMap ? cachedGroup.hostMap : cachedGroup;
1139
+ var meta = cachedGroup === null || cachedGroup === void 0 ? void 0 : cachedGroup.meta;
1140
+ if (serviceGroup === 'preauth' && meta) {
1141
+ // For proximity-based selection, always fetch fresh to respect IP/region changes
1142
+ if (meta.selectionType === 'mode') {
1143
+ return;
1144
+ }
1145
+ var intended = _this0.getIntendedPreauthSelection(currentOrgId);
1146
+ var matches = intended && intended.selectionType === meta.selectionType && intended.selectionValue === meta.selectionValue;
1147
+ if (!matches) {
1148
+ return;
1149
+ }
1150
+ }
1151
+ if (hostMap) {
1152
+ catalog.updateServiceGroups(serviceGroup, hostMap === null || hostMap === void 0 ? void 0 : hostMap.services, hostMap === null || hostMap === void 0 ? void 0 : hostMap.timestamp);
1153
+ }
1154
+ });
1155
+ _this0.updateCredentialsConfig();
1156
+ return _context2.abrupt("return", true);
1157
+ case 9:
1158
+ _context2.prev = 9;
1159
+ _t3 = _context2["catch"](0);
1160
+ return _context2.abrupt("return", false);
1161
+ case 10:
1162
+ case "end":
1163
+ return _context2.stop();
1164
+ }
1165
+ }, _callee2, null, [[0, 9], [4, 6]]);
1166
+ }))();
1167
+ },
1168
+ /**
1169
+ * Clear the catalog cache from the bounded storage (v2).
1170
+ * @returns {Promise<void>}
1171
+ */
1172
+ clearCatalogCache: function clearCatalogCache() {
1173
+ try {
1174
+ var ls = this._getLocalStorageSafe();
1175
+ if (ls) {
1176
+ ls.removeItem(CATALOG_CACHE_KEY_V2);
1177
+ }
1178
+ } catch (e) {
1179
+ this.logger.warn('services: error clearing catalog cache', e);
1180
+ }
1181
+ return _promise.default.resolve();
1182
+ },
879
1183
  /**
880
1184
  * @private
881
1185
  * Simplified method wrapper for sending a request to get
@@ -890,7 +1194,7 @@ var Services = _webexPlugin.default.extend({
890
1194
  * @returns {Promise<object>}
891
1195
  */
892
1196
  _fetchNewServiceHostmap: function _fetchNewServiceHostmap() {
893
- var _this9 = this;
1197
+ var _this1 = this;
894
1198
  var _ref11 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
895
1199
  from = _ref11.from,
896
1200
  query = _ref11.query,
@@ -917,10 +1221,10 @@ var Services = _webexPlugin.default.extend({
917
1221
  };
918
1222
  }
919
1223
  return this.webex.internal.newMetrics.callDiagnosticLatencies.measureLatency(function () {
920
- return _this9.request(requestObject);
1224
+ return _this1.request(requestObject);
921
1225
  }, 'internal.get.u2c.time').then(function (_ref12) {
922
1226
  var body = _ref12.body;
923
- return _this9._formatReceivedHostmap(body || {});
1227
+ return _this1._formatReceivedHostmap(body || {});
924
1228
  });
925
1229
  },
926
1230
  /**
@@ -929,12 +1233,12 @@ var Services = _webexPlugin.default.extend({
929
1233
  * @returns {void}
930
1234
  */
931
1235
  initConfig: function initConfig() {
932
- var _this0 = this;
1236
+ var _this10 = this;
933
1237
  // Get the catalog and destructure the services config.
934
1238
  var catalog = this._getCatalog();
935
- var _this$webex$config = this.webex.config,
936
- services = _this$webex$config.services,
937
- fedramp = _this$webex$config.fedramp;
1239
+ var _this$webex$config2 = this.webex.config,
1240
+ services = _this$webex$config2.services,
1241
+ fedramp = _this$webex$config2.fedramp;
938
1242
 
939
1243
  // Validate that the services configuration exists.
940
1244
  if (services) {
@@ -945,7 +1249,7 @@ var Services = _webexPlugin.default.extend({
945
1249
  if (services.discovery) {
946
1250
  // Format the discovery configuration into an injectable array.
947
1251
  var formattedDiscoveryServices = (0, _keys.default)(services.discovery).map(function (key) {
948
- return _this0._formatHostMapEntry({
1252
+ return _this10._formatHostMapEntry({
949
1253
  id: key,
950
1254
  serviceName: key,
951
1255
  serviceUrls: [{
@@ -961,7 +1265,7 @@ var Services = _webexPlugin.default.extend({
961
1265
  if (services.override) {
962
1266
  // Format the override configuration into an injectable array.
963
1267
  var formattedOverrideServices = (0, _keys.default)(services.override).map(function (key) {
964
- return _this0._formatHostMapEntry({
1268
+ return _this10._formatHostMapEntry({
965
1269
  id: key,
966
1270
  serviceName: key,
967
1271
  serviceUrls: [{
@@ -996,7 +1300,7 @@ var Services = _webexPlugin.default.extend({
996
1300
  * @returns {Promise<void, Error>} - Errors if the token is unavailable.
997
1301
  */
998
1302
  initServiceCatalogs: function initServiceCatalogs() {
999
- var _this1 = this;
1303
+ var _this11 = this;
1000
1304
  var refresh = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
1001
1305
  this.logger.info('services: initializing initial service catalogs');
1002
1306
 
@@ -1012,18 +1316,18 @@ var Services = _webexPlugin.default.extend({
1012
1316
  })
1013
1317
  // Begin collecting the preauth/limited catalog.
1014
1318
  .then(function (orgId) {
1015
- return _this1.collectPreauthCatalog({
1319
+ return _this11.collectPreauthCatalog({
1016
1320
  orgId: orgId
1017
1321
  }, refresh);
1018
1322
  }).then(function () {
1019
1323
  // Validate if the token is authorized.
1020
1324
  if (credentials.canAuthorize) {
1021
1325
  // Attempt to collect the postauth catalog.
1022
- return _this1.updateServices({
1326
+ return _this11.updateServices({
1023
1327
  forceRefresh: refresh
1024
1328
  }).catch(function () {
1025
- _this1.initFailed = true;
1026
- _this1.logger.warn('services: cannot retrieve postauth catalog');
1329
+ _this11.initFailed = true;
1330
+ _this11.logger.warn('services: cannot retrieve postauth catalog');
1027
1331
  });
1028
1332
  }
1029
1333
 
@@ -1031,6 +1335,42 @@ var Services = _webexPlugin.default.extend({
1031
1335
  return _promise.default.resolve();
1032
1336
  });
1033
1337
  },
1338
+ /**
1339
+ * Await any in-flight credentials refresh, then flip `services.ready` so
1340
+ * `webex.ready` can fire. Closes the parallel-refresh window: if a credential
1341
+ * refresh is in flight when initial catalog collection settles, we must not
1342
+ * signal ready until the refresh has resolved - otherwise downstream
1343
+ * consumers may observe `canAuthorize`/token state that is about to change
1344
+ * under them.
1345
+ *
1346
+ * @private
1347
+ * @returns {Promise<void>}
1348
+ */
1349
+ _finalizeReady: function _finalizeReady() {
1350
+ var _this12 = this;
1351
+ return (0, _asyncToGenerator2.default)(/*#__PURE__*/_regenerator.default.mark(function _callee3() {
1352
+ var credentials;
1353
+ return _regenerator.default.wrap(function (_context3) {
1354
+ while (1) switch (_context3.prev = _context3.next) {
1355
+ case 0:
1356
+ credentials = _this12.webex.credentials;
1357
+ if (!(credentials && credentials.isRefreshing)) {
1358
+ _context3.next = 1;
1359
+ break;
1360
+ }
1361
+ _context3.next = 1;
1362
+ return new _promise.default(function (resolve) {
1363
+ credentials.once('change:isRefreshing', resolve);
1364
+ });
1365
+ case 1:
1366
+ _this12.ready = true;
1367
+ case 2:
1368
+ case "end":
1369
+ return _context3.stop();
1370
+ }
1371
+ }, _callee3);
1372
+ }))();
1373
+ },
1034
1374
  /**
1035
1375
  * Initializer
1036
1376
  *
@@ -1039,39 +1379,165 @@ var Services = _webexPlugin.default.extend({
1039
1379
  * @returns {Services}
1040
1380
  */
1041
1381
  initialize: function initialize() {
1042
- var _this10 = this;
1382
+ var _this13 = this;
1043
1383
  var catalog = new _serviceCatalog.default();
1044
1384
  this._catalogs.set(this.webex, catalog);
1045
1385
 
1046
- // Listen for configuration changes once.
1386
+ // Listen for configuration changes once. The config is not populated on the
1387
+ // webex instance until the `change:config` event fires, so any decision that
1388
+ // depends on config values (such as the gated-vs-ungated init below) must be
1389
+ // made from within this handler rather than synchronously in `initialize()`.
1047
1390
  this.listenToOnce(this.webex, 'change:config', function () {
1048
- _this10.initConfig();
1049
- });
1391
+ var _this13$webex$config, _this13$webex$config$;
1392
+ _this13.initConfig();
1050
1393
 
1051
- // wait for webex instance to be ready before attempting
1052
- // to update the service catalogs
1053
- this.listenToOnce(this.webex, 'ready', function () {
1054
- var supertoken = _this10.webex.credentials.supertoken;
1055
- // Validate if the supertoken exists.
1056
- if (supertoken && supertoken.access_token) {
1057
- _this10.initServiceCatalogs().then(function () {
1058
- catalog.isReady = true;
1059
- }).catch(function (error) {
1060
- _this10.initFailed = true;
1061
- _this10.logger.error("services: failed to init initial services when credentials available, ".concat(error === null || error === void 0 ? void 0 : error.message));
1062
- });
1394
+ // Feature flag: when enabled, `webex.ready` is blocked until the initial
1395
+ // catalog collection has settled AND any in-flight credentials refresh has
1396
+ // completed. When disabled (the default), preserves the pre-existing
1397
+ // behavior where `webex.ready` fires as soon as `webex.loaded` does and
1398
+ // the catalog is collected out-of-band.
1399
+ var waitForCatalogInit = ((_this13$webex$config = _this13.webex.config) === null || _this13$webex$config === void 0 ? void 0 : (_this13$webex$config$ = _this13$webex$config.services) === null || _this13$webex$config$ === void 0 ? void 0 : _this13$webex$config$.waitForCatalogInit) === true;
1400
+ if (waitForCatalogInit) {
1401
+ _this13._initializeCatalogsGated(catalog);
1063
1402
  } else {
1064
- var email = _this10.webex.config.email;
1065
- _this10.collectPreauthCatalog(email ? {
1066
- email: email
1067
- } : undefined).catch(function (error) {
1068
- _this10.initFailed = true;
1069
- _this10.logger.error("services: failed to init initial services when no credentials available, ".concat(error === null || error === void 0 ? void 0 : error.message));
1070
- });
1403
+ // Not gating - immediately mark ready so we do not block webex.ready.
1404
+ _this13.ready = true;
1405
+ _this13._initializeCatalogsUngated(catalog);
1071
1406
  }
1072
1407
  });
1073
1408
  },
1074
- version: "3.12.0-task-refactor.1"
1409
+ /**
1410
+ * Original (pre-verified-ready) initialization path. Runs on `webex.ready`
1411
+ * and collects catalogs opportunistically without blocking anything.
1412
+ *
1413
+ * @private
1414
+ * @param {ServiceCatalog} catalog
1415
+ * @returns {void}
1416
+ */
1417
+ _initializeCatalogsUngated: function _initializeCatalogsUngated(catalog) {
1418
+ var _this14 = this;
1419
+ // wait for webex instance to be ready before attempting
1420
+ // to update the service catalogs
1421
+ this.listenToOnce(this.webex, 'ready', /*#__PURE__*/(0, _asyncToGenerator2.default)(/*#__PURE__*/_regenerator.default.mark(function _callee4() {
1422
+ var warmed, supertoken, email;
1423
+ return _regenerator.default.wrap(function (_context4) {
1424
+ while (1) switch (_context4.prev = _context4.next) {
1425
+ case 0:
1426
+ _context4.next = 1;
1427
+ return _this14._loadCatalogFromCache();
1428
+ case 1:
1429
+ warmed = _context4.sent;
1430
+ if (!warmed) {
1431
+ _context4.next = 2;
1432
+ break;
1433
+ }
1434
+ catalog.isReady = true;
1435
+ return _context4.abrupt("return");
1436
+ case 2:
1437
+ supertoken = _this14.webex.credentials.supertoken; // Validate if the supertoken exists.
1438
+ if (supertoken && supertoken.access_token) {
1439
+ _this14.initServiceCatalogs().then(function () {
1440
+ catalog.isReady = true;
1441
+ }).catch(function (error) {
1442
+ _this14.initFailed = true;
1443
+ _this14.logger.error("services: failed to init initial services when credentials available, ".concat(error === null || error === void 0 ? void 0 : error.message));
1444
+ });
1445
+ } else {
1446
+ email = _this14.webex.config.email;
1447
+ _this14.collectPreauthCatalog(email ? {
1448
+ email: email
1449
+ } : undefined).catch(function (error) {
1450
+ _this14.initFailed = true;
1451
+ _this14.logger.error("services: failed to init initial services when no credentials available, ".concat(error === null || error === void 0 ? void 0 : error.message));
1452
+ });
1453
+ }
1454
+ case 3:
1455
+ case "end":
1456
+ return _context4.stop();
1457
+ }
1458
+ }, _callee4);
1459
+ })));
1460
+ },
1461
+ /**
1462
+ * Verified-ready initialization path. Blocks `webex.ready` until the initial
1463
+ * catalog fetch has settled (or timed out) AND any in-flight credentials
1464
+ * refresh has completed. Also handles the fresh-login case where OAuth
1465
+ * completes after `loaded` fires.
1466
+ *
1467
+ * @private
1468
+ * @param {ServiceCatalog} catalog
1469
+ * @returns {void}
1470
+ */
1471
+ _initializeCatalogsGated: function _initializeCatalogsGated(catalog) {
1472
+ var _this15 = this;
1473
+ // Wait for storage to be loaded before attempting to update the service
1474
+ // catalogs. We listen for 'loaded' instead of 'ready' because `services.ready`
1475
+ // now blocks `webex.ready` - listening to 'ready' would deadlock.
1476
+ this.listenToOnce(this.webex, 'loaded', /*#__PURE__*/(0, _asyncToGenerator2.default)(/*#__PURE__*/_regenerator.default.mark(function _callee5() {
1477
+ var warmed, supertoken, timeout, email;
1478
+ return _regenerator.default.wrap(function (_context5) {
1479
+ while (1) switch (_context5.prev = _context5.next) {
1480
+ case 0:
1481
+ _context5.next = 1;
1482
+ return _this15._loadCatalogFromCache();
1483
+ case 1:
1484
+ warmed = _context5.sent;
1485
+ if (!warmed) {
1486
+ _context5.next = 3;
1487
+ break;
1488
+ }
1489
+ catalog.isReady = true;
1490
+ _context5.next = 2;
1491
+ return _this15._finalizeReady();
1492
+ case 2:
1493
+ return _context5.abrupt("return");
1494
+ case 3:
1495
+ supertoken = _this15.webex.credentials.supertoken; // Race init against a hard timeout so a hung request never leaves
1496
+ // `services.ready` false forever - that would stall `webex.ready` and
1497
+ // leave the app on a permanent spinner.
1498
+ timeout = new _promise.default(function (_, reject) {
1499
+ setTimeout(function () {
1500
+ return reject(new Error("services: init timed out after ".concat(SERVICES_INIT_TIMEOUT_MS, "ms")));
1501
+ }, SERVICES_INIT_TIMEOUT_MS);
1502
+ }); // Validate if the supertoken exists.
1503
+ if (supertoken && supertoken.access_token) {
1504
+ _promise.default.race([_this15.initServiceCatalogs(), timeout]).then(function () {
1505
+ catalog.isReady = true;
1506
+ }).catch(function (error) {
1507
+ _this15.initFailed = true;
1508
+ _this15.logger.error("services: failed to init initial services when credentials available, ".concat(error === null || error === void 0 ? void 0 : error.message));
1509
+ }).finally(function () {
1510
+ return _this15._finalizeReady();
1511
+ });
1512
+ } else {
1513
+ email = _this15.webex.config.email;
1514
+ _promise.default.race([_this15.collectPreauthCatalog(email ? {
1515
+ email: email
1516
+ } : undefined), timeout]).catch(function (error) {
1517
+ _this15.initFailed = true;
1518
+ _this15.logger.error("services: failed to init initial services when no credentials available, ".concat(error === null || error === void 0 ? void 0 : error.message));
1519
+ }).finally(function () {
1520
+ return _this15._finalizeReady();
1521
+ });
1522
+
1523
+ // Handle fresh login: 'loaded' fires before OAuth completes, so listen
1524
+ // for `canAuthorize` flipping true and then collect the postauth catalog.
1525
+ _this15.listenToOnce(_this15.webex, 'change:canAuthorize', function () {
1526
+ if (_this15.webex.canAuthorize && !catalog.status.postauth.ready) {
1527
+ _this15.initServiceCatalogs().catch(function (error) {
1528
+ _this15.logger.error("services: failed to init service catalogs after auth, ".concat(error === null || error === void 0 ? void 0 : error.message));
1529
+ });
1530
+ }
1531
+ });
1532
+ }
1533
+ case 4:
1534
+ case "end":
1535
+ return _context5.stop();
1536
+ }
1537
+ }, _callee5);
1538
+ })));
1539
+ },
1540
+ version: "3.12.0-webex-services-ready.2"
1075
1541
  });
1076
1542
  /* eslint-enable no-underscore-dangle */
1077
1543
  var _default = exports.default = Services;