@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
@@ -14,6 +14,8 @@ import {
14
14
  Service,
15
15
  ServiceHostmap,
16
16
  ServiceGroup,
17
+ ServiceHost,
18
+ SelectionMeta,
17
19
  } from './types';
18
20
 
19
21
  const trailingSlashes = /(?:^\/)|(?:\/$)/;
@@ -27,6 +29,9 @@ const CLUSTER_SERVICE = process.env.WEBEX_CONVERSATION_CLUSTER_SERVICE || DEFAUL
27
29
  const DEFAULT_CLUSTER_IDENTIFIER =
28
30
  process.env.WEBEX_CONVERSATION_DEFAULT_CLUSTER || `${DEFAULT_CLUSTER}:${CLUSTER_SERVICE}`;
29
31
 
32
+ const CATALOG_CACHE_KEY_V2 = 'services.v2.u2cHostMap';
33
+ const CATALOG_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
34
+
30
35
  /* eslint-disable no-underscore-dangle */
31
36
  /**
32
37
  * @class
@@ -55,6 +60,46 @@ const Services = WebexPlugin.extend({
55
60
  return this._catalogs.get(this.webex);
56
61
  },
57
62
 
63
+ /**
64
+ * Safely access localStorage if available; returns the Storage or null.
65
+ * @returns {Storage | null}
66
+ */
67
+ _getLocalStorageSafe(): Storage | null {
68
+ if (typeof window !== 'undefined' && (window as any).localStorage) {
69
+ return (window as any).localStorage as Storage;
70
+ }
71
+
72
+ return null;
73
+ },
74
+
75
+ /**
76
+ * Determine the intended preauth selection based on the current context.
77
+ * @param {string} [currentOrgId]
78
+ * @returns {{selectionType: string, selectionValue: string}}
79
+ */
80
+ getIntendedPreauthSelection(currentOrgId?: string): {
81
+ selectionType: string;
82
+ selectionValue: string;
83
+ } {
84
+ if (this.webex.credentials?.canAuthorize) {
85
+ if (currentOrgId) {
86
+ return {selectionType: 'orgId', selectionValue: currentOrgId};
87
+ }
88
+ }
89
+
90
+ const emailConfig = this.webex.config && this.webex.config.email;
91
+
92
+ if (typeof emailConfig === 'string' && emailConfig.trim()) {
93
+ return {
94
+ selectionType: 'emailhash',
95
+ selectionValue: sha256(emailConfig.toLowerCase()).toString(),
96
+ };
97
+ }
98
+
99
+ // fall back to proximity mode when no orgId or email available
100
+ return {selectionType: 'mode', selectionValue: 'DEFAULT_BY_PROXIMITY'};
101
+ },
102
+
58
103
  /**
59
104
  * Get a service url from the current services list by name
60
105
  * from the associated instance catalog.
@@ -104,6 +149,55 @@ const Services = WebexPlugin.extend({
104
149
  return catalog.markFailedServiceUrl(url);
105
150
  },
106
151
 
152
+ /**
153
+ * Get all Mobius cluster host entries from the v2 services list.
154
+ * @returns {Array<ServiceHost>} - An array of `ServiceHost` objects.
155
+ */
156
+ getMobiusClusters(): Array<ServiceHost> {
157
+ this.logger.info('services: fetching mobius clusters');
158
+ const clusters: Array<ServiceHost> = [];
159
+ const services: Array<Service> = this._services || [];
160
+
161
+ services
162
+ .filter(
163
+ (service) =>
164
+ service?.serviceName === 'mobius' &&
165
+ Array.isArray(service.serviceUrls) &&
166
+ service.serviceUrls.length > 0
167
+ )
168
+ .forEach((service) => {
169
+ service.serviceUrls.forEach((serviceUrl) => {
170
+ const modifiedHost = serviceUrl.baseUrl.replace('https://', '').replace('/api/v1', '');
171
+ if (!clusters.find((c) => c && c.host === modifiedHost)) {
172
+ clusters.push({
173
+ host: modifiedHost,
174
+ priority: serviceUrl.priority,
175
+ id: service.id,
176
+ ttl: 0,
177
+ });
178
+ }
179
+ });
180
+ });
181
+
182
+ return clusters;
183
+ },
184
+
185
+ /**
186
+ * Check is valid host from services list.
187
+ * @param {string} host
188
+ * @returns {Boolean}
189
+ */
190
+ isValidHost(host: string): boolean {
191
+ const services: Array<Service> = this._services || [];
192
+
193
+ return services.some((service) => {
194
+ return service.serviceUrls.some((serviceUrl) => {
195
+ const serviceHost = serviceUrl?.baseUrl && new URL(serviceUrl.baseUrl)?.host;
196
+
197
+ return serviceHost === host;
198
+ });
199
+ });
200
+ },
107
201
  /**
108
202
  * saves all the services from the pre and post catalog service
109
203
  * @param {ActiveServices} activeServices
@@ -200,6 +294,18 @@ const Services = WebexPlugin.extend({
200
294
  serviceHostMap?.services,
201
295
  serviceHostMap?.timestamp
202
296
  );
297
+ // Build selection metadata for caching discrimination (preauth/signin)
298
+ let selectionMeta: SelectionMeta | undefined;
299
+ if (serviceGroup === 'preauth' || serviceGroup === 'signin') {
300
+ const key = formattedQuery && Object.keys(formattedQuery || {})[0];
301
+ if (key) {
302
+ selectionMeta = {
303
+ selectionType: key,
304
+ selectionValue: formattedQuery[key],
305
+ };
306
+ }
307
+ }
308
+ this._cacheCatalog(serviceGroup, serviceHostMap, selectionMeta);
203
309
  this.updateCredentialsConfig();
204
310
  catalog.status[serviceGroup].collecting = false;
205
311
  })
@@ -476,10 +582,14 @@ const Services = WebexPlugin.extend({
476
582
  ({countryCode, timezone} = clientRegionInfo);
477
583
  }
478
584
 
479
- // Send the user activation request to the **License** service.
585
+ // Send the user activation request.
586
+ // Use user-onboarding service if configured, otherwise use license service.
587
+ const useUserOnboarding =
588
+ this.webex.config.services?.useUserOnboardingServiceForActivations;
589
+
480
590
  return this.request({
481
- service: 'license',
482
- resource: 'users/activations',
591
+ service: useUserOnboarding ? 'user-onboarding' : 'license',
592
+ resource: useUserOnboarding ? 'api/v1/users/activations' : 'users/activations',
483
593
  method: 'POST',
484
594
  headers: {
485
595
  accept: 'application/json',
@@ -512,7 +622,7 @@ const Services = WebexPlugin.extend({
512
622
  updateCatalog(serviceGroup: ServiceGroup, hostMap: ServiceHostmap): Promise<void> {
513
623
  const catalog = this._getCatalog();
514
624
 
515
- const serviceHostMap = this._formatReceivedHostmap(hostMap);
625
+ const serviceHostMap = this._formatReceivedHostmap(hostMap || {});
516
626
 
517
627
  return catalog.updateServiceGroups(
518
628
  serviceGroup,
@@ -757,7 +867,7 @@ const Services = WebexPlugin.extend({
757
867
  _formatReceivedHostmap({services, activeServices, timestamp, orgId, format}) {
758
868
  const formattedHostmap: ServiceHostmap = {
759
869
  activeServices,
760
- services: services.map((service) => this._formatHostMapEntry(service)),
870
+ services: services?.map((service) => this._formatHostMapEntry(service)),
761
871
  timestamp,
762
872
  orgId,
763
873
  format,
@@ -881,6 +991,190 @@ const Services = WebexPlugin.extend({
881
991
  return url.replace(data.defaultUrl, data.priorityUrl);
882
992
  },
883
993
 
994
+ /**
995
+ * @private
996
+ * Cache the catalog in the bounded storage.
997
+ * @param {ServiceGroup} serviceGroup - preauth, signin, postauth
998
+ * @param {ServiceHostmap} hostMap - The hostmap to cache
999
+ * @param {object} [meta] - Optional selection metadata for cache discrimination
1000
+ * @returns {Promise<void>}
1001
+ */
1002
+ async _cacheCatalog(
1003
+ serviceGroup: ServiceGroup,
1004
+ hostMap: ServiceHostmap,
1005
+ meta?: SelectionMeta
1006
+ ): Promise<void> {
1007
+ let current: {orgId?: string; env?: {fedramp?: boolean; u2cDiscoveryUrl?: string}} = {};
1008
+ let orgId: string | undefined;
1009
+ try {
1010
+ // Respect calling.cacheU2C toggle; if disabled, skip writing cache
1011
+ if (!this.webex.config?.calling?.cacheU2C) {
1012
+ this.logger.info(`services: skipping cache write for ${serviceGroup} as per the config`);
1013
+
1014
+ return;
1015
+ }
1016
+
1017
+ try {
1018
+ const ls = this._getLocalStorageSafe();
1019
+ const cachedJson = ls ? ls.getItem(CATALOG_CACHE_KEY_V2) : null;
1020
+ current = cachedJson ? JSON.parse(cachedJson) : {};
1021
+ } catch {
1022
+ current = {};
1023
+ }
1024
+
1025
+ try {
1026
+ const {credentials} = this.webex;
1027
+ orgId = credentials.getOrgId();
1028
+ } catch {
1029
+ orgId = current.orgId;
1030
+ }
1031
+
1032
+ // Capture environment fingerprint to invalidate cache across env changes
1033
+ let {env} = current;
1034
+ const fedramp = !!this.webex?.config?.fedramp;
1035
+ const u2cDiscoveryUrl = this.webex?.config?.services?.discovery?.u2c;
1036
+ env = {fedramp, u2cDiscoveryUrl};
1037
+
1038
+ const updated = {
1039
+ ...current,
1040
+ orgId: orgId || current.orgId,
1041
+ env: env || current.env,
1042
+ // When selection meta is provided, store as an object; otherwise keep legacy shape
1043
+ [serviceGroup]: meta ? {hostMap, meta} : hostMap,
1044
+ cachedAt: Date.now(),
1045
+ };
1046
+
1047
+ const ls = this._getLocalStorageSafe();
1048
+ if (ls) {
1049
+ ls.setItem(CATALOG_CACHE_KEY_V2, JSON.stringify(updated));
1050
+ }
1051
+ } catch (e) {
1052
+ this.logger.warn('services: error caching catalog', e);
1053
+ }
1054
+ },
1055
+
1056
+ /**
1057
+ * @private
1058
+ * Load the catalog from cache and hydrate the in-memory ServiceCatalog.
1059
+ * @returns {Promise<boolean>} true if cache was loaded, false otherwise
1060
+ */
1061
+ async _loadCatalogFromCache(): Promise<boolean> {
1062
+ let currentOrgId: string | undefined;
1063
+ try {
1064
+ // Respect calling.cacheU2C toggle; if disabled, skip using cache
1065
+ if (!this.webex.config?.calling?.cacheU2C) {
1066
+ this.logger.info('services: skipping cache warm-up as per the cache config');
1067
+
1068
+ return false;
1069
+ }
1070
+
1071
+ const ls = this._getLocalStorageSafe();
1072
+ if (!ls) {
1073
+ this.logger.info('services: skipping cache warm-up as no localStorage is available');
1074
+
1075
+ return false;
1076
+ }
1077
+ const cachedJson = ls.getItem(CATALOG_CACHE_KEY_V2);
1078
+ const cached = cachedJson ? JSON.parse(cachedJson) : undefined;
1079
+ if (!cached) {
1080
+ return false;
1081
+ }
1082
+
1083
+ // TTL enforcement
1084
+ const cachedAt = Number(cached.cachedAt) || 0;
1085
+ if (!cachedAt || Date.now() - cachedAt > CATALOG_TTL_MS) {
1086
+ this.clearCatalogCache();
1087
+
1088
+ return false;
1089
+ }
1090
+
1091
+ // If authorized, ensure cached org matches
1092
+ try {
1093
+ if (this.webex.credentials?.canAuthorize) {
1094
+ const {credentials} = this.webex;
1095
+ currentOrgId = credentials.getOrgId();
1096
+ if (cached.orgId && cached.orgId !== currentOrgId) {
1097
+ return false;
1098
+ }
1099
+ }
1100
+ } catch (e) {
1101
+ this.logger.warn('services: error checking orgId', e);
1102
+ }
1103
+
1104
+ // Ensure cached environment matches current environment
1105
+
1106
+ const fedramp = !!this.webex.config?.fedramp;
1107
+ const u2cDiscoveryUrl = this.webex.config?.services?.discovery?.u2c;
1108
+ const currentEnv = {fedramp, u2cDiscoveryUrl};
1109
+ if (cached.env) {
1110
+ const sameEnv =
1111
+ cached.env.fedramp === currentEnv.fedramp &&
1112
+ cached.env.u2cDiscoveryUrl === currentEnv.u2cDiscoveryUrl;
1113
+ if (!sameEnv) {
1114
+ return false;
1115
+ }
1116
+ }
1117
+
1118
+ const catalog = this._getCatalog();
1119
+ const groups: Array<ServiceGroup> = ['preauth', 'signin', 'postauth'];
1120
+
1121
+ groups.forEach((serviceGroup) => {
1122
+ const cachedGroup = cached[serviceGroup];
1123
+ if (!cachedGroup) {
1124
+ return;
1125
+ }
1126
+
1127
+ // Support legacy (hostMap) and new ({hostMap, meta}) shapes
1128
+ const hostMap: ServiceHostmap =
1129
+ cachedGroup && cachedGroup.hostMap ? cachedGroup.hostMap : cachedGroup;
1130
+ const meta: SelectionMeta | undefined = cachedGroup?.meta;
1131
+
1132
+ if (serviceGroup === 'preauth' && meta) {
1133
+ // For proximity-based selection, always fetch fresh to respect IP/region changes
1134
+ if (meta.selectionType === 'mode') {
1135
+ return;
1136
+ }
1137
+
1138
+ const intended = this.getIntendedPreauthSelection(currentOrgId);
1139
+ const matches =
1140
+ intended &&
1141
+ intended.selectionType === meta.selectionType &&
1142
+ intended.selectionValue === meta.selectionValue;
1143
+ if (!matches) {
1144
+ return;
1145
+ }
1146
+ }
1147
+
1148
+ if (hostMap) {
1149
+ catalog.updateServiceGroups(serviceGroup, hostMap?.services, hostMap?.timestamp);
1150
+ }
1151
+ });
1152
+
1153
+ this.updateCredentialsConfig();
1154
+
1155
+ return true;
1156
+ } catch (e) {
1157
+ return false;
1158
+ }
1159
+ },
1160
+
1161
+ /**
1162
+ * Clear the catalog cache from the bounded storage (v2).
1163
+ * @returns {Promise<void>}
1164
+ */
1165
+ clearCatalogCache(): Promise<void> {
1166
+ try {
1167
+ const ls = this._getLocalStorageSafe();
1168
+ if (ls) {
1169
+ ls.removeItem(CATALOG_CACHE_KEY_V2);
1170
+ }
1171
+ } catch (e) {
1172
+ this.logger.warn('services: error clearing catalog cache', e);
1173
+ }
1174
+
1175
+ return Promise.resolve();
1176
+ },
1177
+
884
1178
  /**
885
1179
  * @private
886
1180
  * Simplified method wrapper for sending a request to get
@@ -924,7 +1218,7 @@ const Services = WebexPlugin.extend({
924
1218
 
925
1219
  return this.webex.internal.newMetrics.callDiagnosticLatencies
926
1220
  .measureLatency(() => this.request(requestObject), 'internal.get.u2c.time')
927
- .then(({body}) => this._formatReceivedHostmap(body));
1221
+ .then(({body}) => this._formatReceivedHostmap(body || {}));
928
1222
  },
929
1223
 
930
1224
  /**
@@ -1040,7 +1334,13 @@ const Services = WebexPlugin.extend({
1040
1334
 
1041
1335
  // wait for webex instance to be ready before attempting
1042
1336
  // to update the service catalogs
1043
- this.listenToOnce(this.webex, 'ready', () => {
1337
+ this.listenToOnce(this.webex, 'ready', async () => {
1338
+ const warmed = await this._loadCatalogFromCache();
1339
+ if (warmed) {
1340
+ catalog.isReady = true;
1341
+
1342
+ return;
1343
+ }
1044
1344
  const {supertoken} = this.webex.credentials;
1045
1345
  // Validate if the supertoken exists.
1046
1346
  if (supertoken && supertoken.access_token) {
@@ -2,6 +2,19 @@ type ServiceName = string;
2
2
  type ClusterId = string;
3
3
  export type ServiceGroup = 'discovery' | 'override' | 'preauth' | 'postauth' | 'signin';
4
4
 
5
+ export type SelectionMeta = {
6
+ selectionType: string;
7
+ selectionValue: string;
8
+ };
9
+
10
+ export type ServiceHost = {
11
+ host: string;
12
+ ttl: number;
13
+ priority: number;
14
+ id: string;
15
+ homeCluster?: boolean;
16
+ };
17
+
5
18
  export type ServiceUrl = {
6
19
  baseUrl: string;
7
20
  host: string;
@@ -153,5 +153,5 @@ export const serviceHostmapV2 = {
153
153
  services: formattedServiceHostmapV2,
154
154
  orgId: '3e0e410f-f83f-4ee4-ac32-12692e99355c',
155
155
  timestamp: '1745533341',
156
- format: 'U2Cv2',
156
+ format: 'U2CV2',
157
157
  };
@@ -510,13 +510,19 @@ describe('webex-core', () => {
510
510
  );
511
511
 
512
512
  it('resolves to an authed u2c hostmap when no params specified', () => {
513
- assert.typeOf(fullRemoteHM, 'array');
514
- assert.isAbove(fullRemoteHM.length, 0);
513
+ assert.typeOf(fullRemoteHM, 'object');
514
+ assert.property(fullRemoteHM, 'serviceLinks');
515
+ assert.property(fullRemoteHM, 'hostCatalog');
516
+ assert.equal(fullRemoteHM.format, 'hostmap');
517
+ assert.isAbove(Object.keys(fullRemoteHM.serviceLinks).length, 0);
515
518
  });
516
519
 
517
520
  it('resolves to a limited u2c hostmap when params specified', () => {
518
- assert.typeOf(limitedRemoteHM, 'array');
519
- assert.isAbove(limitedRemoteHM.length, 0);
521
+ assert.typeOf(limitedRemoteHM, 'object');
522
+ assert.property(limitedRemoteHM, 'serviceLinks');
523
+ assert.property(limitedRemoteHM, 'hostCatalog');
524
+ assert.equal(limitedRemoteHM.format, 'hostmap');
525
+ assert.isAbove(Object.keys(limitedRemoteHM.serviceLinks).length, 0);
520
526
  });
521
527
 
522
528
  it('rejects if the params provided are invalid', () =>
@@ -404,10 +404,13 @@ describe('webex-core', () => {
404
404
  assert.isTrue(catalog.isReady);
405
405
  });
406
406
 
407
- it('should call services#initServiceCatalogs() on webex ready', () => {
407
+ it('should call services#initServiceCatalogs() on webex ready', async () => {
408
+ services._loadCatalogFromCache = sinon.stub().resolves(false);
408
409
  services.initServiceCatalogs = sinon.stub().resolves();
409
410
  services.initialize();
410
411
  webex.trigger('ready');
412
+ // Wait for the async 'ready' handler to complete
413
+ await new Promise((resolve) => setTimeout(resolve, 50));
411
414
  assert.called(services.initServiceCatalogs);
412
415
  assert.isTrue(catalog.isReady);
413
416
  });
@@ -697,7 +700,11 @@ describe('webex-core', () => {
697
700
 
698
701
  it('updates query.email to be emailhash-ed using SHA256', (done) => {
699
702
  catalog.updateServiceUrls = sinon.stub().returns({}); // returns `this`
700
- services._fetchNewServiceHostmap = sinon.stub().resolves();
703
+ services._fetchNewServiceHostmap = sinon.stub().resolves({
704
+ serviceLinks: {},
705
+ hostCatalog: {},
706
+ format: 'hostmap',
707
+ });
701
708
 
702
709
  services
703
710
  .updateServices({
@@ -825,9 +832,14 @@ describe('webex-core', () => {
825
832
  const unauthServices = unauthWebex.internal.services;
826
833
  let sandbox = null;
827
834
 
828
- const getActivationRequest = (requestStub) => {
835
+ const getActivationRequest = (requestStub, useUserOnboarding = false) => {
836
+ const expectedService = useUserOnboarding ? 'user-onboarding' : 'license';
837
+ const expectedResource = useUserOnboarding
838
+ ? 'api/v1/users/activations'
839
+ : 'users/activations';
829
840
  const requests = requestStub.args.filter(
830
- ([request]) => request.service === 'license' && request.resource === 'users/activations'
841
+ ([request]) =>
842
+ request.service === expectedService && request.resource === expectedResource
831
843
  );
832
844
 
833
845
  assert.strictEqual(requests.length, 1);
@@ -906,7 +918,7 @@ describe('webex-core', () => {
906
918
  assert.equal(Object.keys(unauthServices.list(false, 'postauth')).length, 0);
907
919
  }));
908
920
 
909
- it.skip('validates new user with activationOptions suppressEmail true', () =>
921
+ it('validates new user with activationOptions suppressEmail true', () =>
910
922
  unauthServices
911
923
  .validateUser({
912
924
  email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
@@ -990,6 +1002,44 @@ describe('webex-core', () => {
990
1002
  );
991
1003
  });
992
1004
  });
1005
+
1006
+ it('uses the license service by default', () => {
1007
+ const requestStub = sandbox.spy(unauthServices, 'request');
1008
+
1009
+ return unauthServices
1010
+ .validateUser({
1011
+ email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
1012
+ activationOptions: {suppressEmail: true},
1013
+ })
1014
+ .then(() => {
1015
+ const request = getActivationRequest(requestStub, false);
1016
+ assert.strictEqual(request.service, 'license');
1017
+ assert.strictEqual(request.resource, 'users/activations');
1018
+ });
1019
+ });
1020
+
1021
+ it('uses the user-onboarding service when useUserOnboardingServiceForActivations config is true', () => {
1022
+ const userOnboardingWebex = new WebexCore({
1023
+ config: {
1024
+ services: {
1025
+ useUserOnboardingServiceForActivations: true,
1026
+ },
1027
+ },
1028
+ });
1029
+ const userOnboardingServices = userOnboardingWebex.internal.services;
1030
+ const requestStub = sandbox.spy(userOnboardingServices, 'request');
1031
+
1032
+ return userOnboardingServices
1033
+ .validateUser({
1034
+ email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
1035
+ activationOptions: {suppressEmail: true},
1036
+ })
1037
+ .then(() => {
1038
+ const request = getActivationRequest(requestStub, true);
1039
+ assert.strictEqual(request.service, 'user-onboarding');
1040
+ assert.strictEqual(request.resource, 'api/v1/users/activations');
1041
+ });
1042
+ });
993
1043
  });
994
1044
 
995
1045
  describe('#waitForService()', () => {
@@ -1214,13 +1264,19 @@ describe('webex-core', () => {
1214
1264
  );
1215
1265
 
1216
1266
  it('resolves to an authed u2c hostmap when no params specified', () => {
1217
- assert.typeOf(fullRemoteHM, 'array');
1218
- assert.isAbove(fullRemoteHM.length, 0);
1267
+ assert.typeOf(fullRemoteHM, 'object');
1268
+ assert.property(fullRemoteHM, 'serviceLinks');
1269
+ assert.property(fullRemoteHM, 'hostCatalog');
1270
+ assert.equal(fullRemoteHM.format, 'hostmap');
1271
+ assert.isAbove(Object.keys(fullRemoteHM.serviceLinks).length, 0);
1219
1272
  });
1220
1273
 
1221
1274
  it('resolves to a limited u2c hostmap when params specified', () => {
1222
- assert.typeOf(limitedRemoteHM, 'array');
1223
- assert.isAbove(limitedRemoteHM.length, 0);
1275
+ assert.typeOf(limitedRemoteHM, 'object');
1276
+ assert.property(limitedRemoteHM, 'serviceLinks');
1277
+ assert.property(limitedRemoteHM, 'hostCatalog');
1278
+ assert.equal(limitedRemoteHM.format, 'hostmap');
1279
+ assert.isAbove(Object.keys(limitedRemoteHM.serviceLinks).length, 0);
1224
1280
  });
1225
1281
 
1226
1282
  it('rejects if the params provided are invalid', () =>
@@ -599,7 +599,7 @@ describe('webex-core', () => {
599
599
  ],
600
600
  orgId: '3e0e410f-f83f-4ee4-ac32-12692e99355c',
601
601
  timestamp: '1745533341',
602
- format: 'U2Cv2',
602
+ format: 'U2CV2',
603
603
  };
604
604
 
605
605
  catalog.updateServiceGroups('preauth', formattedHM.services);
@@ -686,7 +686,7 @@ describe('webex-core', () => {
686
686
  ],
687
687
  orgId: '3e0e410f-f83f-4ee4-ac32-12692e99355c',
688
688
  timestamp: '1745533341',
689
- format: 'U2Cv2',
689
+ format: 'U2CV2',
690
690
  };
691
691
  const notInOrderFormattedHM = services._formatReceivedHostmap(notInOrderServiceHM);
692
692
  const checkFormattedHM = cloneDeep(notInOrderFormattedHM);
@@ -23,7 +23,7 @@ import {
23
23
  formattedServiceHostmapEntryConv,
24
24
  formattedServiceHostmapEntryMercury,
25
25
  formattedServiceHostmapEntryTest,
26
- serviceHostmapV2
26
+ serviceHostmapV2,
27
27
  } from '../../../fixtures/host-catalog-v2';
28
28
 
29
29
  // /* eslint-disable no-underscore-dangle */
@@ -316,10 +316,13 @@ describe('webex-core', () => {
316
316
  assert.isTrue(catalog.isReady);
317
317
  });
318
318
 
319
- it('should call services#initServiceCatalogs() on webex ready', () => {
319
+ it('should call services#initServiceCatalogs() on webex ready', async () => {
320
+ services._loadCatalogFromCache = sinon.stub().resolves(false);
320
321
  services.initServiceCatalogs = sinon.stub().resolves();
321
322
  services.initialize();
322
323
  webex.trigger('ready');
324
+ // Wait for the async 'ready' handler to complete
325
+ await new Promise((resolve) => setTimeout(resolve, 50));
323
326
  assert.called(services.initServiceCatalogs);
324
327
  assert.isTrue(catalog.isReady);
325
328
  });
@@ -413,7 +416,11 @@ describe('webex-core', () => {
413
416
  .initServiceCatalogs(true)
414
417
  // services#updateServices() gets called once by the limited catalog
415
418
  // retrieval and should get called again when authorized.
416
- .then(() => assert.calledTwice(services.updateServices) && assert.calledWith(services.updateServices, sinon.match({forceRefresh: true})))
419
+ .then(
420
+ () =>
421
+ assert.calledTwice(services.updateServices) &&
422
+ assert.calledWith(services.updateServices, sinon.match({forceRefresh: true}))
423
+ )
417
424
  );
418
425
  });
419
426
  });
@@ -748,9 +755,14 @@ describe('webex-core', () => {
748
755
  const unauthServices = unauthWebex.internal.services;
749
756
  let sandbox = null;
750
757
 
751
- const getActivationRequest = (requestStub) => {
758
+ const getActivationRequest = (requestStub, useUserOnboarding = false) => {
759
+ const expectedService = useUserOnboarding ? 'user-onboarding' : 'license';
760
+ const expectedResource = useUserOnboarding
761
+ ? 'api/v1/users/activations'
762
+ : 'users/activations';
752
763
  const requests = requestStub.args.filter(
753
- ([request]) => request.service === 'license' && request.resource === 'users/activations'
764
+ ([request]) =>
765
+ request.service === expectedService && request.resource === expectedResource
754
766
  );
755
767
 
756
768
  assert.strictEqual(requests.length, 1);
@@ -823,7 +835,7 @@ describe('webex-core', () => {
823
835
  assert.equal(r.user.verificationEmailTriggered, true);
824
836
  }));
825
837
 
826
- it.skip('validates new user with activationOptions suppressEmail true', () =>
838
+ it('validates new user with activationOptions suppressEmail true', () =>
827
839
  unauthServices
828
840
  .validateUser({
829
841
  email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
@@ -895,6 +907,44 @@ describe('webex-core', () => {
895
907
  );
896
908
  });
897
909
  });
910
+
911
+ it('uses the license service by default', () => {
912
+ const requestStub = sandbox.spy(unauthServices, 'request');
913
+
914
+ return unauthServices
915
+ .validateUser({
916
+ email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
917
+ activationOptions: {suppressEmail: true},
918
+ })
919
+ .then(() => {
920
+ const request = getActivationRequest(requestStub, false);
921
+ assert.strictEqual(request.service, 'license');
922
+ assert.strictEqual(request.resource, 'users/activations');
923
+ });
924
+ });
925
+
926
+ it('uses the user-onboarding service when useUserOnboardingServiceForActivations config is true', () => {
927
+ const userOnboardingWebex = new WebexCore({
928
+ config: {
929
+ services: {
930
+ useUserOnboardingServiceForActivations: true,
931
+ },
932
+ },
933
+ });
934
+ const userOnboardingServices = userOnboardingWebex.internal.services;
935
+ const requestStub = sandbox.spy(userOnboardingServices, 'request');
936
+
937
+ return userOnboardingServices
938
+ .validateUser({
939
+ email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
940
+ activationOptions: {suppressEmail: true},
941
+ })
942
+ .then(() => {
943
+ const request = getActivationRequest(requestStub, true);
944
+ assert.strictEqual(request.service, 'user-onboarding');
945
+ assert.strictEqual(request.resource, 'api/v1/users/activations');
946
+ });
947
+ });
898
948
  });
899
949
 
900
950
  describe('#waitForService()', () => {