@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
@@ -20,6 +20,14 @@ export const DEFAULT_CLUSTER_SERVICE = 'identityLookup';
20
20
  const CLUSTER_SERVICE = process.env.WEBEX_CONVERSATION_CLUSTER_SERVICE || DEFAULT_CLUSTER_SERVICE;
21
21
  const DEFAULT_CLUSTER_IDENTIFIER =
22
22
  process.env.WEBEX_CONVERSATION_DEFAULT_CLUSTER || `${DEFAULT_CLUSTER}:${CLUSTER_SERVICE}`;
23
+ const CATALOG_CACHE_KEY_V1 = 'services.v1.u2cHostMap';
24
+ const CATALOG_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
25
+
26
+ // Maximum time we will wait for the initial catalog collection before letting
27
+ // `services.ready` (and therefore `webex.ready`) fire anyway. A hung request
28
+ // must never leave the app on a permanent spinner - past this point downstream
29
+ // consumers must fall through to their normal error/login paths.
30
+ const SERVICES_INIT_TIMEOUT_MS = 15_000;
23
31
 
24
32
  /* eslint-disable no-underscore-dangle */
25
33
  /**
@@ -55,6 +63,22 @@ const Services = WebexPlugin.extend({
55
63
  initFailed: ['boolean', false, false],
56
64
  },
57
65
 
66
+ session: {
67
+ /**
68
+ * Becomes `true` once the initial catalog collection has completed
69
+ * (successfully or otherwise) and any in-flight credentials refresh has
70
+ * settled. Blocks `webex.ready` so consumers can rely on `webex.ready`
71
+ * implying "catalogs populated AND credential state stable".
72
+ * @instance
73
+ * @memberof Services
74
+ * @type {boolean}
75
+ */
76
+ ready: {
77
+ default: false,
78
+ type: 'boolean',
79
+ },
80
+ },
81
+
58
82
  _catalogs: new WeakMap(),
59
83
 
60
84
  _serviceUrls: null,
@@ -96,6 +120,49 @@ const Services = WebexPlugin.extend({
96
120
  return this._catalogs.get(this.webex);
97
121
  },
98
122
 
123
+ /**
124
+ * Safely access localStorage if available; returns the Storage or null.
125
+ * @returns {Storage|null}
126
+ */
127
+ _getLocalStorageSafe() {
128
+ if (typeof window !== 'undefined' && window.localStorage) {
129
+ return window.localStorage;
130
+ }
131
+
132
+ return null;
133
+ },
134
+
135
+ /**
136
+ * Determine the intended preauth selection based on the current context.
137
+ * @param {string|undefined} currentOrgId
138
+ * @returns {{selectionType: string, selectionValue: string}}
139
+ */
140
+ getIntendedPreauthSelection(currentOrgId) {
141
+ if (this.webex.credentials?.canAuthorize) {
142
+ if (currentOrgId) {
143
+ return {
144
+ selectionType: 'orgId',
145
+ selectionValue: currentOrgId,
146
+ };
147
+ }
148
+ }
149
+
150
+ const emailConfig = this.webex.config && this.webex.config.email;
151
+
152
+ if (typeof emailConfig === 'string' && emailConfig.trim()) {
153
+ return {
154
+ selectionType: 'emailhash',
155
+ selectionValue: sha256(emailConfig.toLowerCase()).toString(),
156
+ };
157
+ }
158
+
159
+ // fall back to proximity mode when no orgId or email available
160
+ return {
161
+ selectionType: 'mode',
162
+ selectionValue: 'DEFAULT_BY_PROXIMITY',
163
+ };
164
+ },
165
+
99
166
  /**
100
167
  * Get a service url from the current services list by name
101
168
  * from the associated instance catalog.
@@ -165,9 +232,10 @@ const Services = WebexPlugin.extend({
165
232
 
166
233
  /**
167
234
  * Get all Mobius cluster host entries from the legacy host catalog.
168
- * @returns {Array<Object>}
235
+ * @returns {Array<{host: string, id: string, ttl: number, priority: number}>}
169
236
  */
170
237
  getMobiusClusters() {
238
+ this.logger.info('services: fetching mobius clusters');
171
239
  const clusters = [];
172
240
  const hostCatalog = this._hostCatalog || {};
173
241
 
@@ -197,6 +265,28 @@ const Services = WebexPlugin.extend({
197
265
 
198
266
  return !!hostCatalog[host]?.length;
199
267
  },
268
+
269
+ /**
270
+ * Checks if the current environment is an integration (INT) environment
271
+ * by examining the u2c discovery URL from webex config.
272
+ * INT environments use discovery URLs containing 'intb' (e.g., u2c-intb.ciscospark.com).
273
+ * @returns {boolean} True if INT environment, false otherwise
274
+ */
275
+ isIntegrationEnvironment() {
276
+ try {
277
+ const u2cUrl = this.webex?.config?.services?.discovery?.u2c || '';
278
+ const isInt = u2cUrl.includes('intb');
279
+
280
+ this.logger.info(`services: isIntegrationEnvironment: ${isInt}`);
281
+
282
+ return isInt;
283
+ } catch (error) {
284
+ this.logger.error('services: failed to determine integration environment', error);
285
+
286
+ return false;
287
+ }
288
+ },
289
+
200
290
  /**
201
291
  * Merge provided active cluster mappings into current state.
202
292
  * @param {Record<string,string>} activeServices
@@ -237,7 +327,7 @@ const Services = WebexPlugin.extend({
237
327
  * @param {string} [param.token] - used for signin catalog
238
328
  * @returns {Promise<object>}
239
329
  */
240
- updateServices({from, query, token, forceRefresh} = {}) {
330
+ async updateServices({from, query, token, forceRefresh} = {}) {
241
331
  const catalog = this._getCatalog();
242
332
  let formattedQuery;
243
333
  let serviceGroup;
@@ -291,7 +381,20 @@ const Services = WebexPlugin.extend({
291
381
  forceRefresh,
292
382
  })
293
383
  .then((serviceHostMap) => {
294
- catalog.updateServiceUrls(serviceGroup, serviceHostMap);
384
+ const formattedServiceHostMap = this._formatReceivedHostmap(serviceHostMap);
385
+ // Build selection metadata for caching discrimination
386
+ let selectionMeta;
387
+ if (serviceGroup === 'preauth' || serviceGroup === 'signin') {
388
+ const key = formattedQuery && Object.keys(formattedQuery || {})[0];
389
+ if (key) {
390
+ selectionMeta = {
391
+ selectionType: key,
392
+ selectionValue: formattedQuery[key],
393
+ };
394
+ }
395
+ }
396
+ this._cacheCatalog(serviceGroup, serviceHostMap, selectionMeta);
397
+ catalog.updateServiceUrls(serviceGroup, formattedServiceHostMap);
295
398
  this.updateCredentialsConfig();
296
399
  catalog.status[serviceGroup].collecting = false;
297
400
  })
@@ -1008,7 +1111,197 @@ const Services = WebexPlugin.extend({
1008
1111
 
1009
1112
  return this.webex.internal.newMetrics.callDiagnosticLatencies
1010
1113
  .measureLatency(() => this.request(requestObject), 'internal.get.u2c.time')
1011
- .then(({body}) => this._formatReceivedHostmap(body));
1114
+ .then(({body}) => body);
1115
+ },
1116
+
1117
+ /**
1118
+ * Cache the catalog in the bounded storage.
1119
+ * @param {string} serviceGroup - preauth, signin, postauth
1120
+ * @param {object} hostMap - The hostmap to cache
1121
+ * @param {object} [meta] - Optional selection metadata used to validate cache reuse
1122
+ * @returns {Promise<void>}
1123
+ *
1124
+ */
1125
+ async _cacheCatalog(serviceGroup, hostMap, meta) {
1126
+ let current = {};
1127
+ let orgId;
1128
+ try {
1129
+ // Respect calling.cacheU2C toggle; if disabled, skip writing cache
1130
+ if (!this.webex.config?.calling?.cacheU2C) {
1131
+ this.logger.info(`services: skipping cache write for ${serviceGroup} as per the config`);
1132
+
1133
+ return;
1134
+ }
1135
+
1136
+ // Persist to localStorage to survive browser refresh
1137
+ try {
1138
+ const ls = this._getLocalStorageSafe();
1139
+ const cachedJson = ls ? ls.getItem(CATALOG_CACHE_KEY_V1) : null;
1140
+ current = cachedJson ? JSON.parse(cachedJson) : {};
1141
+ } catch (e) {
1142
+ current = {};
1143
+ }
1144
+
1145
+ try {
1146
+ const {credentials} = this.webex;
1147
+ orgId = credentials.getOrgId();
1148
+ } catch (e) {
1149
+ orgId = current.orgId;
1150
+ }
1151
+
1152
+ // Capture environment fingerprint to invalidate cache across env changes
1153
+ let {env} = current;
1154
+ const fedramp = !!this.webex?.config?.fedramp;
1155
+ const u2cDiscoveryUrl = this.webex?.config?.services?.discovery?.u2c;
1156
+ env = {fedramp, u2cDiscoveryUrl};
1157
+
1158
+ const updated = {
1159
+ ...current,
1160
+ orgId: orgId || current.orgId,
1161
+ env: env || current.env,
1162
+ // When selection meta is provided, store as an object; otherwise keep legacy shape
1163
+ [serviceGroup]: meta ? {hostMap, meta} : hostMap,
1164
+ cachedAt: Date.now(),
1165
+ };
1166
+
1167
+ const ls = this._getLocalStorageSafe();
1168
+ if (ls) {
1169
+ ls.setItem(CATALOG_CACHE_KEY_V1, JSON.stringify(updated));
1170
+ }
1171
+ } catch (error) {
1172
+ this.logger.warn('services: error caching catalog', error);
1173
+ }
1174
+ },
1175
+
1176
+ /**
1177
+ * Load the catalog from cache and hydrate the in-memory ServiceCatalog.
1178
+ * @returns {Promise<boolean>} true if cache was loaded, false otherwise
1179
+ */
1180
+ async _loadCatalogFromCache() {
1181
+ let currentOrgId;
1182
+ try {
1183
+ // Respect calling.cacheU2C toggle; if disabled, skip using cache
1184
+ if (!this.webex.config?.calling?.cacheU2C) {
1185
+ this.logger.info('services: skipping cache warm-up as per the cache config');
1186
+
1187
+ return false;
1188
+ }
1189
+
1190
+ const ls = this._getLocalStorageSafe();
1191
+ if (!ls) {
1192
+ this.logger.info('services: skipping cache warm-up as no localStorage is available');
1193
+
1194
+ return false;
1195
+ }
1196
+ const cachedJson = ls.getItem(CATALOG_CACHE_KEY_V1);
1197
+ const cached = cachedJson ? JSON.parse(cachedJson) : undefined;
1198
+ if (!cached) {
1199
+ return false;
1200
+ }
1201
+ // TTL enforcement: clear if older than 24 hours
1202
+ const cachedAt = Number(cached.cachedAt) || 0;
1203
+ if (!cachedAt || Date.now() - cachedAt > CATALOG_TTL_MS) {
1204
+ this.clearCatalogCache();
1205
+
1206
+ return false;
1207
+ }
1208
+
1209
+ // If authorized, ensure cached org matches
1210
+ try {
1211
+ if (this.webex.credentials?.canAuthorize) {
1212
+ const {credentials} = this.webex;
1213
+ currentOrgId = credentials.getOrgId();
1214
+ if (cached.orgId && cached.orgId !== currentOrgId) {
1215
+ return false;
1216
+ }
1217
+ }
1218
+ } catch (e) {
1219
+ this.logger.warn('services: error checking orgId', e);
1220
+ }
1221
+
1222
+ // Ensure cached environment matches current environment
1223
+
1224
+ const fedramp = !!this.webex.config?.fedramp;
1225
+ const u2cDiscoveryUrl = this.webex.config?.services?.discovery?.u2c;
1226
+ const currentEnv = {fedramp, u2cDiscoveryUrl};
1227
+ if (cached.env) {
1228
+ const sameEnv =
1229
+ cached.env.fedramp === currentEnv.fedramp &&
1230
+ cached.env.u2cDiscoveryUrl === currentEnv.u2cDiscoveryUrl;
1231
+ if (!sameEnv) {
1232
+ this.logger.info('services: skipping cache warm due to environment mismatch');
1233
+
1234
+ return false;
1235
+ }
1236
+ }
1237
+
1238
+ const catalog = this._getCatalog();
1239
+
1240
+ // Apply any cached groups (with preauth selection validation if available)
1241
+ const groups = ['preauth', 'signin', 'postauth'];
1242
+ groups.forEach((serviceGroup) => {
1243
+ const cachedGroup = cached[serviceGroup];
1244
+ if (!cachedGroup) {
1245
+ return;
1246
+ }
1247
+
1248
+ // Support legacy (hostMap) and new ({hostMap, meta}) shapes
1249
+ const hostMap = cachedGroup && cachedGroup.hostMap ? cachedGroup.hostMap : cachedGroup;
1250
+ const meta = cachedGroup?.meta;
1251
+
1252
+ if (serviceGroup === 'preauth' && meta) {
1253
+ // For proximity-based selection, always fetch fresh to respect IP/region changes
1254
+ if (meta.selectionType === 'mode') {
1255
+ this.logger.info('services: skipping preauth cache warm for proximity mode');
1256
+
1257
+ return;
1258
+ }
1259
+
1260
+ const intended = this.getIntendedPreauthSelection(currentOrgId);
1261
+ const matches =
1262
+ intended &&
1263
+ intended.selectionType === meta.selectionType &&
1264
+ intended.selectionValue === meta.selectionValue;
1265
+
1266
+ if (!matches) {
1267
+ this.logger.info('services: skipping preauth cache warm due to selection mismatch');
1268
+
1269
+ return;
1270
+ }
1271
+ }
1272
+
1273
+ if (hostMap) {
1274
+ const formatted = this._formatReceivedHostmap(hostMap);
1275
+ catalog.updateServiceUrls(serviceGroup, formatted);
1276
+ }
1277
+ });
1278
+
1279
+ // Align credentials against warmed catalog
1280
+ this.updateCredentialsConfig();
1281
+
1282
+ return true;
1283
+ } catch (e) {
1284
+ this.logger.warn('services: error loading catalog from cache', e);
1285
+
1286
+ return false;
1287
+ }
1288
+ },
1289
+
1290
+ /**
1291
+ * Clear the catalog cache from the bounded storage.
1292
+ * @returns {Promise<void>}
1293
+ */
1294
+ clearCatalogCache() {
1295
+ try {
1296
+ const ls = this._getLocalStorageSafe();
1297
+ if (ls) {
1298
+ ls.removeItem(CATALOG_CACHE_KEY_V1);
1299
+ }
1300
+ } catch (e) {
1301
+ this.logger.warn('services: error clearing catalog cache', e);
1302
+ }
1303
+
1304
+ return Promise.resolve();
1012
1305
  },
1013
1306
 
1014
1307
  /**
@@ -1088,6 +1381,7 @@ const Services = WebexPlugin.extend({
1088
1381
  // Validate if the token is authorized.
1089
1382
  if (credentials.canAuthorize) {
1090
1383
  // Attempt to collect the postauth catalog.
1384
+
1091
1385
  return this.updateServices().catch(() => {
1092
1386
  this.initFailed = true;
1093
1387
  this.logger.warn('services: cannot retrieve postauth catalog');
@@ -1100,6 +1394,29 @@ const Services = WebexPlugin.extend({
1100
1394
  );
1101
1395
  },
1102
1396
 
1397
+ /**
1398
+ * Await any in-flight credentials refresh, then flip `services.ready` so
1399
+ * `webex.ready` can fire. Closes the parallel-refresh window: if a credential
1400
+ * refresh is in flight when initial catalog collection settles, we must not
1401
+ * signal ready until the refresh has resolved - otherwise downstream
1402
+ * consumers may observe `canAuthorize`/token state that is about to change
1403
+ * under them.
1404
+ *
1405
+ * @private
1406
+ * @returns {Promise<void>}
1407
+ */
1408
+ async _finalizeReady() {
1409
+ const {credentials} = this.webex;
1410
+
1411
+ if (credentials && credentials.isRefreshing) {
1412
+ await new Promise((resolve) => {
1413
+ credentials.once('change:isRefreshing', resolve);
1414
+ });
1415
+ }
1416
+
1417
+ this.ready = true;
1418
+ },
1419
+
1103
1420
  /**
1104
1421
  * Initializer
1105
1422
  *
@@ -1116,14 +1433,50 @@ const Services = WebexPlugin.extend({
1116
1433
  this.registries.set(this.webex, registry);
1117
1434
  this.states.set(this.webex, state);
1118
1435
 
1119
- // Listen for configuration changes once.
1436
+ // Listen for configuration changes once. The config is not populated on the
1437
+ // webex instance until the `change:config` event fires, so any decision that
1438
+ // depends on config values (such as the gated-vs-ungated init below) must be
1439
+ // made from within this handler rather than synchronously in `initialize()`.
1120
1440
  this.listenToOnce(this.webex, 'change:config', () => {
1121
1441
  this.initConfig();
1442
+
1443
+ // Feature flag: when enabled, `webex.ready` is blocked until the initial
1444
+ // catalog collection has settled AND any in-flight credentials refresh has
1445
+ // completed. When disabled (the default), preserves the pre-existing
1446
+ // behavior where `webex.ready` fires as soon as `webex.loaded` does and
1447
+ // the catalog is collected out-of-band.
1448
+ const waitForCatalogInit = this.webex.config?.services?.waitForCatalogInit === true;
1449
+
1450
+ if (waitForCatalogInit) {
1451
+ this._initializeCatalogsGated(catalog);
1452
+ } else {
1453
+ // Not gating - immediately mark ready so we do not block webex.ready.
1454
+ this.ready = true;
1455
+ this._initializeCatalogsUngated(catalog);
1456
+ }
1122
1457
  });
1458
+ },
1123
1459
 
1460
+ /**
1461
+ * Original (pre-verified-ready) initialization path. Runs on `webex.ready`
1462
+ * and collects catalogs opportunistically without blocking anything.
1463
+ *
1464
+ * @private
1465
+ * @param {ServiceCatalog} catalog
1466
+ * @returns {void}
1467
+ */
1468
+ _initializeCatalogsUngated(catalog) {
1124
1469
  // wait for webex instance to be ready before attempting
1125
1470
  // to update the service catalogs
1126
- this.listenToOnce(this.webex, 'ready', () => {
1471
+ // this can cause a race condition because credentials may
1472
+ // not be valid when services is initialized
1473
+ this.listenToOnce(this.webex, 'ready', async () => {
1474
+ const cachedCatalog = await this._loadCatalogFromCache();
1475
+ if (cachedCatalog) {
1476
+ catalog.isReady = true;
1477
+
1478
+ return; // skip initServiceCatalogs() on reload when cache exists
1479
+ }
1127
1480
  const {supertoken} = this.webex.credentials;
1128
1481
  // Validate if the supertoken exists.
1129
1482
  if (supertoken && supertoken.access_token) {
@@ -1149,6 +1502,80 @@ const Services = WebexPlugin.extend({
1149
1502
  }
1150
1503
  });
1151
1504
  },
1505
+
1506
+ /**
1507
+ * Verified-ready initialization path. Blocks `webex.ready` until the initial
1508
+ * catalog fetch has settled (or timed out) AND any in-flight credentials
1509
+ * refresh has completed. Also handles the fresh-login case where OAuth
1510
+ * completes after `loaded` fires.
1511
+ *
1512
+ * @private
1513
+ * @param {ServiceCatalog} catalog
1514
+ * @returns {void}
1515
+ */
1516
+ _initializeCatalogsGated(catalog) {
1517
+ // Wait for storage to be loaded before attempting to update the service
1518
+ // catalogs. We listen for 'loaded' instead of 'ready' because `services.ready`
1519
+ // now blocks `webex.ready` - listening to 'ready' would deadlock.
1520
+ this.listenToOnce(this.webex, 'loaded', async () => {
1521
+ const cachedCatalog = await this._loadCatalogFromCache();
1522
+ if (cachedCatalog) {
1523
+ catalog.isReady = true;
1524
+ await this._finalizeReady();
1525
+
1526
+ return; // skip initServiceCatalogs() on reload when cache exists
1527
+ }
1528
+ const {supertoken} = this.webex.credentials;
1529
+
1530
+ // Race init against a hard timeout so a hung request never leaves
1531
+ // `services.ready` false forever - that would stall `webex.ready` and
1532
+ // leave the app on a permanent spinner.
1533
+ const timeout = new Promise((_, reject) => {
1534
+ setTimeout(
1535
+ () => reject(new Error(`services: init timed out after ${SERVICES_INIT_TIMEOUT_MS}ms`)),
1536
+ SERVICES_INIT_TIMEOUT_MS
1537
+ );
1538
+ });
1539
+
1540
+ // Validate if the supertoken exists.
1541
+ if (supertoken && supertoken.access_token) {
1542
+ Promise.race([this.initServiceCatalogs(), timeout])
1543
+ .then(() => {
1544
+ catalog.isReady = true;
1545
+ })
1546
+ .catch((error) => {
1547
+ this.initFailed = true;
1548
+ this.logger.error(
1549
+ `services: failed to init initial services when credentials available, ${error?.message}`
1550
+ );
1551
+ })
1552
+ .finally(() => this._finalizeReady());
1553
+ } else {
1554
+ const {email} = this.webex.config;
1555
+
1556
+ Promise.race([this.collectPreauthCatalog(email ? {email} : undefined), timeout])
1557
+ .catch((error) => {
1558
+ this.initFailed = true;
1559
+ this.logger.error(
1560
+ `services: failed to init initial services when no credentials available, ${error?.message}`
1561
+ );
1562
+ })
1563
+ .finally(() => this._finalizeReady());
1564
+
1565
+ // Handle fresh login: 'loaded' fires before OAuth completes, so listen
1566
+ // for `canAuthorize` flipping true and then collect the postauth catalog.
1567
+ this.listenToOnce(this.webex, 'change:canAuthorize', () => {
1568
+ if (this.webex.canAuthorize && !catalog.status.postauth.ready) {
1569
+ this.initServiceCatalogs().catch((error) => {
1570
+ this.logger.error(
1571
+ `services: failed to init service catalogs after auth, ${error?.message}`
1572
+ );
1573
+ });
1574
+ }
1575
+ });
1576
+ }
1577
+ });
1578
+ },
1152
1579
  });
1153
1580
  /* eslint-enable no-underscore-dangle */
1154
1581