@webex/webex-core 3.10.0 → 3.11.0

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 (118) hide show
  1. package/dist/config.js +14 -0
  2. package/dist/config.js.map +1 -1
  3. package/dist/credentials-config.js.map +1 -1
  4. package/dist/index.js +1 -2
  5. package/dist/index.js.map +1 -1
  6. package/dist/interceptors/auth.js +6 -8
  7. package/dist/interceptors/auth.js.map +1 -1
  8. package/dist/interceptors/default-options.js +6 -8
  9. package/dist/interceptors/default-options.js.map +1 -1
  10. package/dist/interceptors/embargo.js +6 -8
  11. package/dist/interceptors/embargo.js.map +1 -1
  12. package/dist/interceptors/network-timing.js +6 -8
  13. package/dist/interceptors/network-timing.js.map +1 -1
  14. package/dist/interceptors/payload-transformer.js +6 -8
  15. package/dist/interceptors/payload-transformer.js.map +1 -1
  16. package/dist/interceptors/proxy.js +7 -10
  17. package/dist/interceptors/proxy.js.map +1 -1
  18. package/dist/interceptors/rate-limit.js +7 -10
  19. package/dist/interceptors/rate-limit.js.map +1 -1
  20. package/dist/interceptors/redirect.js +9 -8
  21. package/dist/interceptors/redirect.js.map +1 -1
  22. package/dist/interceptors/request-event.js +6 -8
  23. package/dist/interceptors/request-event.js.map +1 -1
  24. package/dist/interceptors/request-logger.js +6 -8
  25. package/dist/interceptors/request-logger.js.map +1 -1
  26. package/dist/interceptors/request-timing.js +6 -8
  27. package/dist/interceptors/request-timing.js.map +1 -1
  28. package/dist/interceptors/response-logger.js +6 -8
  29. package/dist/interceptors/response-logger.js.map +1 -1
  30. package/dist/interceptors/user-agent.js +8 -11
  31. package/dist/interceptors/user-agent.js.map +1 -1
  32. package/dist/interceptors/webex-tracking-id.js +6 -8
  33. package/dist/interceptors/webex-tracking-id.js.map +1 -1
  34. package/dist/interceptors/webex-user-agent.js +7 -10
  35. package/dist/interceptors/webex-user-agent.js.map +1 -1
  36. package/dist/lib/batcher.js +1 -1
  37. package/dist/lib/batcher.js.map +1 -1
  38. package/dist/lib/constants.js.map +1 -1
  39. package/dist/lib/credentials/credentials.js +4 -6
  40. package/dist/lib/credentials/credentials.js.map +1 -1
  41. package/dist/lib/credentials/grant-errors.js +18 -26
  42. package/dist/lib/credentials/grant-errors.js.map +1 -1
  43. package/dist/lib/credentials/index.js.map +1 -1
  44. package/dist/lib/credentials/scope.js.map +1 -1
  45. package/dist/lib/credentials/token-collection.js.map +1 -1
  46. package/dist/lib/credentials/token.js +5 -5
  47. package/dist/lib/credentials/token.js.map +1 -1
  48. package/dist/lib/interceptors/hostmap.js +6 -8
  49. package/dist/lib/interceptors/hostmap.js.map +1 -1
  50. package/dist/lib/interceptors/server-error.js +6 -8
  51. package/dist/lib/interceptors/server-error.js.map +1 -1
  52. package/dist/lib/interceptors/service.js +6 -8
  53. package/dist/lib/interceptors/service.js.map +1 -1
  54. package/dist/lib/metrics.js.map +1 -1
  55. package/dist/lib/page.js +5 -6
  56. package/dist/lib/page.js.map +1 -1
  57. package/dist/lib/services/index.js.map +1 -1
  58. package/dist/lib/services/service-catalog.js +3 -3
  59. package/dist/lib/services/service-catalog.js.map +1 -1
  60. package/dist/lib/services/service-fed-ramp.js.map +1 -1
  61. package/dist/lib/services/service-host.js +1 -2
  62. package/dist/lib/services/service-host.js.map +1 -1
  63. package/dist/lib/services/service-registry.js +1 -2
  64. package/dist/lib/services/service-registry.js.map +1 -1
  65. package/dist/lib/services/service-state.js +1 -2
  66. package/dist/lib/services/service-state.js.map +1 -1
  67. package/dist/lib/services/service-url.js +11 -1
  68. package/dist/lib/services/service-url.js.map +1 -1
  69. package/dist/lib/services/services.js +485 -127
  70. package/dist/lib/services/services.js.map +1 -1
  71. package/dist/lib/services-v2/index.js.map +1 -1
  72. package/dist/lib/services-v2/metrics.js.map +1 -1
  73. package/dist/lib/services-v2/service-catalog.js +7 -7
  74. package/dist/lib/services-v2/service-catalog.js.map +1 -1
  75. package/dist/lib/services-v2/service-detail.js.map +1 -1
  76. package/dist/lib/services-v2/service-fed-ramp.js.map +1 -1
  77. package/dist/lib/services-v2/services-v2.js +379 -51
  78. package/dist/lib/services-v2/services-v2.js.map +1 -1
  79. package/dist/lib/services-v2/types.js.map +1 -1
  80. package/dist/lib/stateless-webex-plugin.js +3 -4
  81. package/dist/lib/stateless-webex-plugin.js.map +1 -1
  82. package/dist/lib/storage/decorators.js.map +1 -1
  83. package/dist/lib/storage/errors.js +7 -9
  84. package/dist/lib/storage/errors.js.map +1 -1
  85. package/dist/lib/storage/index.js.map +1 -1
  86. package/dist/lib/storage/make-webex-plugin-store.js +14 -5
  87. package/dist/lib/storage/make-webex-plugin-store.js.map +1 -1
  88. package/dist/lib/storage/make-webex-store.js +13 -5
  89. package/dist/lib/storage/make-webex-store.js.map +1 -1
  90. package/dist/lib/storage/memory-store-adapter.js.map +1 -1
  91. package/dist/lib/webex-core-plugin-mixin.js.map +1 -1
  92. package/dist/lib/webex-http-error.js +8 -11
  93. package/dist/lib/webex-http-error.js.map +1 -1
  94. package/dist/lib/webex-internal-core-plugin-mixin.js.map +1 -1
  95. package/dist/lib/webex-plugin.js.map +1 -1
  96. package/dist/plugins/logger.js +1 -1
  97. package/dist/plugins/logger.js.map +1 -1
  98. package/dist/webex-core.js +11 -11
  99. package/dist/webex-core.js.map +1 -1
  100. package/dist/webex-internal-core.js.map +1 -1
  101. package/package.json +13 -13
  102. package/src/config.js +15 -0
  103. package/src/interceptors/redirect.js +4 -0
  104. package/src/lib/services/service-url.js +9 -1
  105. package/src/lib/services/services.js +315 -7
  106. package/src/lib/services-v2/index.ts +0 -1
  107. package/src/lib/services-v2/service-catalog.ts +4 -4
  108. package/src/lib/services-v2/services-v2.ts +307 -7
  109. package/src/lib/services-v2/types.ts +13 -0
  110. package/test/fixtures/host-catalog-v2.ts +1 -1
  111. package/test/integration/spec/services/service-catalog.js +10 -4
  112. package/test/integration/spec/services/services.js +65 -9
  113. package/test/integration/spec/services-v2/service-catalog.js +2 -2
  114. package/test/integration/spec/services-v2/services-v2.js +56 -6
  115. package/test/unit/spec/interceptors/redirect.js +98 -0
  116. package/test/unit/spec/services/service-url.js +110 -0
  117. package/test/unit/spec/services/services.js +411 -2
  118. package/test/unit/spec/services-v2/services-v2.ts +316 -0
@@ -20,6 +20,8 @@ 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
23
25
 
24
26
  /* eslint-disable no-underscore-dangle */
25
27
  /**
@@ -61,6 +63,9 @@ const Services = WebexPlugin.extend({
61
63
 
62
64
  _hostCatalog: null,
63
65
 
66
+ // Map of active cluster ids per service, e.g. { wdm: 'urn:TEAM:ap-southeast-2_m:wdm' }
67
+ _activeServices: {},
68
+
64
69
  /**
65
70
  * Get the registry associated with this webex instance.
66
71
  *
@@ -93,6 +98,49 @@ const Services = WebexPlugin.extend({
93
98
  return this._catalogs.get(this.webex);
94
99
  },
95
100
 
101
+ /**
102
+ * Safely access localStorage if available; returns the Storage or null.
103
+ * @returns {Storage|null}
104
+ */
105
+ _getLocalStorageSafe() {
106
+ if (typeof window !== 'undefined' && window.localStorage) {
107
+ return window.localStorage;
108
+ }
109
+
110
+ return null;
111
+ },
112
+
113
+ /**
114
+ * Determine the intended preauth selection based on the current context.
115
+ * @param {string|undefined} currentOrgId
116
+ * @returns {{selectionType: string, selectionValue: string}}
117
+ */
118
+ getIntendedPreauthSelection(currentOrgId) {
119
+ if (this.webex.credentials?.canAuthorize) {
120
+ if (currentOrgId) {
121
+ return {
122
+ selectionType: 'orgId',
123
+ selectionValue: currentOrgId,
124
+ };
125
+ }
126
+ }
127
+
128
+ const emailConfig = this.webex.config && this.webex.config.email;
129
+
130
+ if (typeof emailConfig === 'string' && emailConfig.trim()) {
131
+ return {
132
+ selectionType: 'emailhash',
133
+ selectionValue: sha256(emailConfig.toLowerCase()).toString(),
134
+ };
135
+ }
136
+
137
+ // fall back to proximity mode when no orgId or email available
138
+ return {
139
+ selectionType: 'mode',
140
+ selectionValue: 'DEFAULT_BY_PROXIMITY',
141
+ };
142
+ },
143
+
96
144
  /**
97
145
  * Get a service url from the current services list by name
98
146
  * from the associated instance catalog.
@@ -160,6 +208,50 @@ const Services = WebexPlugin.extend({
160
208
  return catalog.markFailedUrl(url, noPriorityHosts);
161
209
  },
162
210
 
211
+ /**
212
+ * Get all Mobius cluster host entries from the legacy host catalog.
213
+ * @returns {Array<{host: string, id: string, ttl: number, priority: number}>}
214
+ */
215
+ getMobiusClusters() {
216
+ this.logger.info('services: fetching mobius clusters');
217
+ const clusters = [];
218
+ const hostCatalog = this._hostCatalog || {};
219
+
220
+ Object.entries(hostCatalog).forEach(([host, entries]) => {
221
+ (entries || []).forEach((entry) => {
222
+ if (typeof entry?.id === 'string' && entry.id.endsWith(':mobius')) {
223
+ // Ensure host is included; prefer entry.host if present, else use the map key
224
+ const withHost = entry.host ? entry.host : host;
225
+ // Skip duplicates for the same host
226
+ if (!clusters.find((c) => c && c.host === withHost)) {
227
+ clusters.push({...entry, host: withHost});
228
+ }
229
+ }
230
+ });
231
+ });
232
+
233
+ return clusters;
234
+ },
235
+
236
+ /**
237
+ * Check is valid host from the legacy host catalog.
238
+ * @param {string} host
239
+ * @returns {Boolean}
240
+ */
241
+ isValidHost(host) {
242
+ const hostCatalog = this._hostCatalog || {};
243
+
244
+ return !!hostCatalog[host]?.length;
245
+ },
246
+ /**
247
+ * Merge provided active cluster mappings into current state.
248
+ * @param {Record<string,string>} activeServices
249
+ * @returns {void}
250
+ */
251
+ _updateActiveServices(activeServices) {
252
+ this._activeServices = {...this._activeServices, ...activeServices};
253
+ },
254
+
163
255
  /**
164
256
  * saves all the services from the pre and post catalog service
165
257
  * @param {Object} serviceUrls
@@ -191,7 +283,7 @@ const Services = WebexPlugin.extend({
191
283
  * @param {string} [param.token] - used for signin catalog
192
284
  * @returns {Promise<object>}
193
285
  */
194
- updateServices({from, query, token, forceRefresh} = {}) {
286
+ async updateServices({from, query, token, forceRefresh} = {}) {
195
287
  const catalog = this._getCatalog();
196
288
  let formattedQuery;
197
289
  let serviceGroup;
@@ -245,7 +337,20 @@ const Services = WebexPlugin.extend({
245
337
  forceRefresh,
246
338
  })
247
339
  .then((serviceHostMap) => {
248
- catalog.updateServiceUrls(serviceGroup, serviceHostMap);
340
+ const formattedServiceHostMap = this._formatReceivedHostmap(serviceHostMap);
341
+ // Build selection metadata for caching discrimination
342
+ let selectionMeta;
343
+ if (serviceGroup === 'preauth' || serviceGroup === 'signin') {
344
+ const key = formattedQuery && Object.keys(formattedQuery || {})[0];
345
+ if (key) {
346
+ selectionMeta = {
347
+ selectionType: key,
348
+ selectionValue: formattedQuery[key],
349
+ };
350
+ }
351
+ }
352
+ this._cacheCatalog(serviceGroup, serviceHostMap, selectionMeta);
353
+ catalog.updateServiceUrls(serviceGroup, formattedServiceHostMap);
249
354
  this.updateCredentialsConfig();
250
355
  catalog.status[serviceGroup].collecting = false;
251
356
  })
@@ -473,10 +578,14 @@ const Services = WebexPlugin.extend({
473
578
  ({countryCode, timezone} = clientRegionInfo);
474
579
  }
475
580
 
476
- // Send the user activation request to the **License** service.
581
+ // Send the user activation request.
582
+ // Use user-onboarding service if configured, otherwise use license service.
583
+ const useUserOnboarding =
584
+ this.webex.config.services?.useUserOnboardingServiceForActivations;
585
+
477
586
  return this.request({
478
- service: 'license',
479
- resource: 'users/activations',
587
+ service: useUserOnboarding ? 'user-onboarding' : 'license',
588
+ resource: useUserOnboarding ? 'api/v1/users/activations' : 'users/activations',
480
589
  method: 'POST',
481
590
  headers: {
482
591
  accept: 'application/json',
@@ -958,7 +1067,197 @@ const Services = WebexPlugin.extend({
958
1067
 
959
1068
  return this.webex.internal.newMetrics.callDiagnosticLatencies
960
1069
  .measureLatency(() => this.request(requestObject), 'internal.get.u2c.time')
961
- .then(({body}) => this._formatReceivedHostmap(body));
1070
+ .then(({body}) => body);
1071
+ },
1072
+
1073
+ /**
1074
+ * Cache the catalog in the bounded storage.
1075
+ * @param {string} serviceGroup - preauth, signin, postauth
1076
+ * @param {object} hostMap - The hostmap to cache
1077
+ * @param {object} [meta] - Optional selection metadata used to validate cache reuse
1078
+ * @returns {Promise<void>}
1079
+ *
1080
+ */
1081
+ async _cacheCatalog(serviceGroup, hostMap, meta) {
1082
+ let current = {};
1083
+ let orgId;
1084
+ try {
1085
+ // Respect calling.cacheU2C toggle; if disabled, skip writing cache
1086
+ if (!this.webex.config?.calling?.cacheU2C) {
1087
+ this.logger.info(`services: skipping cache write for ${serviceGroup} as per the config`);
1088
+
1089
+ return;
1090
+ }
1091
+
1092
+ // Persist to localStorage to survive browser refresh
1093
+ try {
1094
+ const ls = this._getLocalStorageSafe();
1095
+ const cachedJson = ls ? ls.getItem(CATALOG_CACHE_KEY_V1) : null;
1096
+ current = cachedJson ? JSON.parse(cachedJson) : {};
1097
+ } catch (e) {
1098
+ current = {};
1099
+ }
1100
+
1101
+ try {
1102
+ const {credentials} = this.webex;
1103
+ orgId = credentials.getOrgId();
1104
+ } catch (e) {
1105
+ orgId = current.orgId;
1106
+ }
1107
+
1108
+ // Capture environment fingerprint to invalidate cache across env changes
1109
+ let {env} = current;
1110
+ const fedramp = !!this.webex?.config?.fedramp;
1111
+ const u2cDiscoveryUrl = this.webex?.config?.services?.discovery?.u2c;
1112
+ env = {fedramp, u2cDiscoveryUrl};
1113
+
1114
+ const updated = {
1115
+ ...current,
1116
+ orgId: orgId || current.orgId,
1117
+ env: env || current.env,
1118
+ // When selection meta is provided, store as an object; otherwise keep legacy shape
1119
+ [serviceGroup]: meta ? {hostMap, meta} : hostMap,
1120
+ cachedAt: Date.now(),
1121
+ };
1122
+
1123
+ const ls = this._getLocalStorageSafe();
1124
+ if (ls) {
1125
+ ls.setItem(CATALOG_CACHE_KEY_V1, JSON.stringify(updated));
1126
+ }
1127
+ } catch (error) {
1128
+ this.logger.warn('services: error caching catalog', error);
1129
+ }
1130
+ },
1131
+
1132
+ /**
1133
+ * Load the catalog from cache and hydrate the in-memory ServiceCatalog.
1134
+ * @returns {Promise<boolean>} true if cache was loaded, false otherwise
1135
+ */
1136
+ async _loadCatalogFromCache() {
1137
+ let currentOrgId;
1138
+ try {
1139
+ // Respect calling.cacheU2C toggle; if disabled, skip using cache
1140
+ if (!this.webex.config?.calling?.cacheU2C) {
1141
+ this.logger.info('services: skipping cache warm-up as per the cache config');
1142
+
1143
+ return false;
1144
+ }
1145
+
1146
+ const ls = this._getLocalStorageSafe();
1147
+ if (!ls) {
1148
+ this.logger.info('services: skipping cache warm-up as no localStorage is available');
1149
+
1150
+ return false;
1151
+ }
1152
+ const cachedJson = ls.getItem(CATALOG_CACHE_KEY_V1);
1153
+ const cached = cachedJson ? JSON.parse(cachedJson) : undefined;
1154
+ if (!cached) {
1155
+ return false;
1156
+ }
1157
+ // TTL enforcement: clear if older than 24 hours
1158
+ const cachedAt = Number(cached.cachedAt) || 0;
1159
+ if (!cachedAt || Date.now() - cachedAt > CATALOG_TTL_MS) {
1160
+ this.clearCatalogCache();
1161
+
1162
+ return false;
1163
+ }
1164
+
1165
+ // If authorized, ensure cached org matches
1166
+ try {
1167
+ if (this.webex.credentials?.canAuthorize) {
1168
+ const {credentials} = this.webex;
1169
+ currentOrgId = credentials.getOrgId();
1170
+ if (cached.orgId && cached.orgId !== currentOrgId) {
1171
+ return false;
1172
+ }
1173
+ }
1174
+ } catch (e) {
1175
+ this.logger.warn('services: error checking orgId', e);
1176
+ }
1177
+
1178
+ // Ensure cached environment matches current environment
1179
+
1180
+ const fedramp = !!this.webex.config?.fedramp;
1181
+ const u2cDiscoveryUrl = this.webex.config?.services?.discovery?.u2c;
1182
+ const currentEnv = {fedramp, u2cDiscoveryUrl};
1183
+ if (cached.env) {
1184
+ const sameEnv =
1185
+ cached.env.fedramp === currentEnv.fedramp &&
1186
+ cached.env.u2cDiscoveryUrl === currentEnv.u2cDiscoveryUrl;
1187
+ if (!sameEnv) {
1188
+ this.logger.info('services: skipping cache warm due to environment mismatch');
1189
+
1190
+ return false;
1191
+ }
1192
+ }
1193
+
1194
+ const catalog = this._getCatalog();
1195
+
1196
+ // Apply any cached groups (with preauth selection validation if available)
1197
+ const groups = ['preauth', 'signin', 'postauth'];
1198
+ groups.forEach((serviceGroup) => {
1199
+ const cachedGroup = cached[serviceGroup];
1200
+ if (!cachedGroup) {
1201
+ return;
1202
+ }
1203
+
1204
+ // Support legacy (hostMap) and new ({hostMap, meta}) shapes
1205
+ const hostMap = cachedGroup && cachedGroup.hostMap ? cachedGroup.hostMap : cachedGroup;
1206
+ const meta = cachedGroup?.meta;
1207
+
1208
+ if (serviceGroup === 'preauth' && meta) {
1209
+ // For proximity-based selection, always fetch fresh to respect IP/region changes
1210
+ if (meta.selectionType === 'mode') {
1211
+ this.logger.info('services: skipping preauth cache warm for proximity mode');
1212
+
1213
+ return;
1214
+ }
1215
+
1216
+ const intended = this.getIntendedPreauthSelection(currentOrgId);
1217
+ const matches =
1218
+ intended &&
1219
+ intended.selectionType === meta.selectionType &&
1220
+ intended.selectionValue === meta.selectionValue;
1221
+
1222
+ if (!matches) {
1223
+ this.logger.info('services: skipping preauth cache warm due to selection mismatch');
1224
+
1225
+ return;
1226
+ }
1227
+ }
1228
+
1229
+ if (hostMap) {
1230
+ const formatted = this._formatReceivedHostmap(hostMap);
1231
+ catalog.updateServiceUrls(serviceGroup, formatted);
1232
+ }
1233
+ });
1234
+
1235
+ // Align credentials against warmed catalog
1236
+ this.updateCredentialsConfig();
1237
+
1238
+ return true;
1239
+ } catch (e) {
1240
+ this.logger.warn('services: error loading catalog from cache', e);
1241
+
1242
+ return false;
1243
+ }
1244
+ },
1245
+
1246
+ /**
1247
+ * Clear the catalog cache from the bounded storage.
1248
+ * @returns {Promise<void>}
1249
+ */
1250
+ clearCatalogCache() {
1251
+ try {
1252
+ const ls = this._getLocalStorageSafe();
1253
+ if (ls) {
1254
+ ls.removeItem(CATALOG_CACHE_KEY_V1);
1255
+ }
1256
+ } catch (e) {
1257
+ this.logger.warn('services: error clearing catalog cache', e);
1258
+ }
1259
+
1260
+ return Promise.resolve();
962
1261
  },
963
1262
 
964
1263
  /**
@@ -1038,6 +1337,7 @@ const Services = WebexPlugin.extend({
1038
1337
  // Validate if the token is authorized.
1039
1338
  if (credentials.canAuthorize) {
1040
1339
  // Attempt to collect the postauth catalog.
1340
+
1041
1341
  return this.updateServices().catch(() => {
1042
1342
  this.initFailed = true;
1043
1343
  this.logger.warn('services: cannot retrieve postauth catalog');
@@ -1073,7 +1373,15 @@ const Services = WebexPlugin.extend({
1073
1373
 
1074
1374
  // wait for webex instance to be ready before attempting
1075
1375
  // to update the service catalogs
1076
- this.listenToOnce(this.webex, 'ready', () => {
1376
+ // this can cause a race condition because credentials may
1377
+ // not be valid when services is initialized
1378
+ this.listenToOnce(this.webex, 'ready', async () => {
1379
+ const cachedCatalog = await this._loadCatalogFromCache();
1380
+ if (cachedCatalog) {
1381
+ catalog.isReady = true;
1382
+
1383
+ return; // skip initServiceCatalogs() on reload when cache exists
1384
+ }
1077
1385
  const {supertoken} = this.webex.credentials;
1078
1386
  // Validate if the supertoken exists.
1079
1387
  if (supertoken && supertoken.access_token) {
@@ -1,7 +1,6 @@
1
1
  /*!
2
2
  * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
3
3
  */
4
-
5
4
  export {default as ServicesV2} from './services-v2';
6
5
  export {default as ServiceCatalogV2} from './service-catalog';
7
6
  export {default as ServiceDetail} from './service-detail';
@@ -119,7 +119,7 @@ const ServiceCatalog = AmpState.extend({
119
119
  // declare namespaces outside of loop
120
120
  let existingService: IServiceDetail | undefined;
121
121
 
122
- serviceDetails.forEach((service) => {
122
+ serviceDetails?.forEach((service) => {
123
123
  existingService = this._getServiceDetail(service.id, serviceGroup);
124
124
 
125
125
  if (existingService) {
@@ -311,13 +311,13 @@ const ServiceCatalog = AmpState.extend({
311
311
  ) {
312
312
  const currentServiceDetails = this.serviceGroups[serviceGroup];
313
313
 
314
- const unusedServicesDetails = currentServiceDetails.filter((serviceDetail) =>
315
- serviceDetails.every(({id}) => id !== serviceDetail.id)
314
+ const unusedServicesDetails = currentServiceDetails?.filter((serviceDetail) =>
315
+ serviceDetails?.every(({id}) => id !== serviceDetail.id)
316
316
  );
317
317
 
318
318
  this._unloadServiceDetails(serviceGroup, unusedServicesDetails);
319
319
 
320
- serviceDetails.forEach((serviceObj) => {
320
+ serviceDetails?.forEach((serviceObj) => {
321
321
  const serviceDetail = this._getServiceDetail(serviceObj.id, serviceGroup);
322
322
  serviceObj?.serviceUrls?.sort((a, b) => {
323
323
  if (a.priority < 0 && b.priority < 0) return 0;