@webex/webex-core 2.59.3-next.1 → 2.59.4

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 (189) hide show
  1. package/.eslintrc.js +6 -6
  2. package/README.md +79 -79
  3. package/babel.config.js +3 -3
  4. package/dist/config.js +24 -24
  5. package/dist/config.js.map +1 -1
  6. package/dist/credentials-config.js +56 -56
  7. package/dist/credentials-config.js.map +1 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/interceptors/auth.js +28 -28
  10. package/dist/interceptors/auth.js.map +1 -1
  11. package/dist/interceptors/default-options.js +24 -24
  12. package/dist/interceptors/default-options.js.map +1 -1
  13. package/dist/interceptors/embargo.js +9 -9
  14. package/dist/interceptors/embargo.js.map +1 -1
  15. package/dist/interceptors/network-timing.js +19 -19
  16. package/dist/interceptors/network-timing.js.map +1 -1
  17. package/dist/interceptors/payload-transformer.js +19 -19
  18. package/dist/interceptors/payload-transformer.js.map +1 -1
  19. package/dist/interceptors/rate-limit.js +40 -40
  20. package/dist/interceptors/rate-limit.js.map +1 -1
  21. package/dist/interceptors/redirect.js +13 -13
  22. package/dist/interceptors/redirect.js.map +1 -1
  23. package/dist/interceptors/request-event.js +23 -23
  24. package/dist/interceptors/request-event.js.map +1 -1
  25. package/dist/interceptors/request-logger.js +13 -13
  26. package/dist/interceptors/request-logger.js.map +1 -1
  27. package/dist/interceptors/request-timing.js +23 -23
  28. package/dist/interceptors/request-timing.js.map +1 -1
  29. package/dist/interceptors/response-logger.js +19 -19
  30. package/dist/interceptors/response-logger.js.map +1 -1
  31. package/dist/interceptors/user-agent.js +29 -29
  32. package/dist/interceptors/user-agent.js.map +1 -1
  33. package/dist/interceptors/webex-tracking-id.js +15 -15
  34. package/dist/interceptors/webex-tracking-id.js.map +1 -1
  35. package/dist/interceptors/webex-user-agent.js +13 -13
  36. package/dist/interceptors/webex-user-agent.js.map +1 -1
  37. package/dist/lib/batcher.js +83 -83
  38. package/dist/lib/batcher.js.map +1 -1
  39. package/dist/lib/credentials/credentials.js +103 -103
  40. package/dist/lib/credentials/credentials.js.map +1 -1
  41. package/dist/lib/credentials/grant-errors.js +17 -17
  42. package/dist/lib/credentials/grant-errors.js.map +1 -1
  43. package/dist/lib/credentials/index.js +2 -2
  44. package/dist/lib/credentials/index.js.map +1 -1
  45. package/dist/lib/credentials/scope.js +11 -11
  46. package/dist/lib/credentials/scope.js.map +1 -1
  47. package/dist/lib/credentials/token-collection.js +2 -2
  48. package/dist/lib/credentials/token-collection.js.map +1 -1
  49. package/dist/lib/credentials/token.js +145 -145
  50. package/dist/lib/credentials/token.js.map +1 -1
  51. package/dist/lib/page.js +49 -49
  52. package/dist/lib/page.js.map +1 -1
  53. package/dist/lib/services/constants.js.map +1 -1
  54. package/dist/lib/services/index.js +2 -2
  55. package/dist/lib/services/index.js.map +1 -1
  56. package/dist/lib/services/interceptors/server-error.js +9 -9
  57. package/dist/lib/services/interceptors/server-error.js.map +1 -1
  58. package/dist/lib/services/interceptors/service.js +24 -24
  59. package/dist/lib/services/interceptors/service.js.map +1 -1
  60. package/dist/lib/services/metrics.js.map +1 -1
  61. package/dist/lib/services/service-catalog.js +104 -104
  62. package/dist/lib/services/service-catalog.js.map +1 -1
  63. package/dist/lib/services/service-fed-ramp.js.map +1 -1
  64. package/dist/lib/services/service-host.js +134 -134
  65. package/dist/lib/services/service-host.js.map +1 -1
  66. package/dist/lib/services/service-registry.js +175 -175
  67. package/dist/lib/services/service-registry.js.map +1 -1
  68. package/dist/lib/services/service-state.js +38 -38
  69. package/dist/lib/services/service-state.js.map +1 -1
  70. package/dist/lib/services/service-url.js +31 -31
  71. package/dist/lib/services/service-url.js.map +1 -1
  72. package/dist/lib/services/services.js +245 -245
  73. package/dist/lib/services/services.js.map +1 -1
  74. package/dist/lib/stateless-webex-plugin.js +28 -28
  75. package/dist/lib/stateless-webex-plugin.js.map +1 -1
  76. package/dist/lib/storage/decorators.js +27 -27
  77. package/dist/lib/storage/decorators.js.map +1 -1
  78. package/dist/lib/storage/errors.js +4 -4
  79. package/dist/lib/storage/errors.js.map +1 -1
  80. package/dist/lib/storage/index.js.map +1 -1
  81. package/dist/lib/storage/make-webex-plugin-store.js +44 -44
  82. package/dist/lib/storage/make-webex-plugin-store.js.map +1 -1
  83. package/dist/lib/storage/make-webex-store.js +40 -40
  84. package/dist/lib/storage/make-webex-store.js.map +1 -1
  85. package/dist/lib/storage/memory-store-adapter.js +9 -9
  86. package/dist/lib/storage/memory-store-adapter.js.map +1 -1
  87. package/dist/lib/webex-core-plugin-mixin.js +13 -13
  88. package/dist/lib/webex-core-plugin-mixin.js.map +1 -1
  89. package/dist/lib/webex-http-error.js +9 -9
  90. package/dist/lib/webex-http-error.js.map +1 -1
  91. package/dist/lib/webex-internal-core-plugin-mixin.js +13 -13
  92. package/dist/lib/webex-internal-core-plugin-mixin.js.map +1 -1
  93. package/dist/lib/webex-plugin.js +36 -36
  94. package/dist/lib/webex-plugin.js.map +1 -1
  95. package/dist/plugins/logger.js +9 -9
  96. package/dist/plugins/logger.js.map +1 -1
  97. package/dist/webex-core.js +104 -104
  98. package/dist/webex-core.js.map +1 -1
  99. package/dist/webex-internal-core.js +12 -12
  100. package/dist/webex-internal-core.js.map +1 -1
  101. package/jest.config.js +3 -3
  102. package/package.json +19 -20
  103. package/process +1 -1
  104. package/src/config.js +90 -90
  105. package/src/credentials-config.js +212 -212
  106. package/src/index.js +62 -62
  107. package/src/interceptors/auth.js +186 -186
  108. package/src/interceptors/default-options.js +55 -55
  109. package/src/interceptors/embargo.js +43 -43
  110. package/src/interceptors/network-timing.js +54 -54
  111. package/src/interceptors/payload-transformer.js +55 -55
  112. package/src/interceptors/rate-limit.js +169 -169
  113. package/src/interceptors/redirect.js +106 -106
  114. package/src/interceptors/request-event.js +93 -93
  115. package/src/interceptors/request-logger.js +78 -78
  116. package/src/interceptors/request-timing.js +65 -65
  117. package/src/interceptors/response-logger.js +98 -98
  118. package/src/interceptors/user-agent.js +77 -77
  119. package/src/interceptors/webex-tracking-id.js +73 -73
  120. package/src/interceptors/webex-user-agent.js +79 -79
  121. package/src/lib/batcher.js +307 -307
  122. package/src/lib/credentials/credentials.js +552 -552
  123. package/src/lib/credentials/grant-errors.js +92 -92
  124. package/src/lib/credentials/index.js +16 -16
  125. package/src/lib/credentials/scope.js +34 -34
  126. package/src/lib/credentials/token-collection.js +17 -17
  127. package/src/lib/credentials/token.js +559 -559
  128. package/src/lib/page.js +159 -159
  129. package/src/lib/services/constants.js +9 -9
  130. package/src/lib/services/index.js +26 -26
  131. package/src/lib/services/interceptors/server-error.js +48 -48
  132. package/src/lib/services/interceptors/service.js +101 -101
  133. package/src/lib/services/metrics.js +4 -4
  134. package/src/lib/services/service-catalog.js +435 -435
  135. package/src/lib/services/service-fed-ramp.js +4 -4
  136. package/src/lib/services/service-host.js +267 -267
  137. package/src/lib/services/service-registry.js +465 -465
  138. package/src/lib/services/service-state.js +78 -78
  139. package/src/lib/services/service-url.js +124 -124
  140. package/src/lib/services/services.js +1018 -1018
  141. package/src/lib/stateless-webex-plugin.js +98 -98
  142. package/src/lib/storage/decorators.js +220 -220
  143. package/src/lib/storage/errors.js +15 -15
  144. package/src/lib/storage/index.js +10 -10
  145. package/src/lib/storage/make-webex-plugin-store.js +211 -211
  146. package/src/lib/storage/make-webex-store.js +140 -140
  147. package/src/lib/storage/memory-store-adapter.js +79 -79
  148. package/src/lib/webex-core-plugin-mixin.js +114 -114
  149. package/src/lib/webex-http-error.js +61 -61
  150. package/src/lib/webex-internal-core-plugin-mixin.js +107 -107
  151. package/src/lib/webex-plugin.js +222 -222
  152. package/src/plugins/logger.js +60 -60
  153. package/src/webex-core.js +745 -745
  154. package/src/webex-internal-core.js +46 -46
  155. package/test/integration/spec/credentials/credentials.js +139 -139
  156. package/test/integration/spec/credentials/token.js +102 -102
  157. package/test/integration/spec/services/service-catalog.js +838 -838
  158. package/test/integration/spec/services/services.js +1221 -1221
  159. package/test/integration/spec/webex-core.js +178 -178
  160. package/test/unit/spec/_setup.js +44 -44
  161. package/test/unit/spec/credentials/credentials.js +1017 -1017
  162. package/test/unit/spec/credentials/token.js +441 -441
  163. package/test/unit/spec/interceptors/auth.js +521 -521
  164. package/test/unit/spec/interceptors/default-options.js +84 -84
  165. package/test/unit/spec/interceptors/embargo.js +144 -144
  166. package/test/unit/spec/interceptors/network-timing.js +49 -49
  167. package/test/unit/spec/interceptors/payload-transformer.js +155 -155
  168. package/test/unit/spec/interceptors/rate-limit.js +302 -302
  169. package/test/unit/spec/interceptors/redirect.js +102 -102
  170. package/test/unit/spec/interceptors/request-timing.js +92 -92
  171. package/test/unit/spec/interceptors/user-agent.js +76 -76
  172. package/test/unit/spec/interceptors/webex-tracking-id.js +76 -76
  173. package/test/unit/spec/interceptors/webex-user-agent.js +159 -159
  174. package/test/unit/spec/lib/batcher.js +330 -330
  175. package/test/unit/spec/lib/page.js +148 -148
  176. package/test/unit/spec/lib/webex-plugin.js +48 -48
  177. package/test/unit/spec/services/interceptors/server-error.js +204 -204
  178. package/test/unit/spec/services/interceptors/service.js +188 -188
  179. package/test/unit/spec/services/service-catalog.js +194 -194
  180. package/test/unit/spec/services/service-host.js +260 -260
  181. package/test/unit/spec/services/service-registry.js +747 -747
  182. package/test/unit/spec/services/service-state.js +60 -60
  183. package/test/unit/spec/services/service-url.js +258 -258
  184. package/test/unit/spec/services/services.js +348 -348
  185. package/test/unit/spec/storage/persist.js +50 -50
  186. package/test/unit/spec/storage/storage-adapter.js +12 -12
  187. package/test/unit/spec/storage/wait-for-value.js +81 -81
  188. package/test/unit/spec/webex-core.js +253 -253
  189. package/test/unit/spec/webex-internal-core.js +91 -91
@@ -1,1018 +1,1018 @@
1
- import Url from 'url';
2
-
3
- import sha256 from 'crypto-js/sha256';
4
-
5
- import WebexPlugin from '../webex-plugin';
6
-
7
- import METRICS from './metrics';
8
- import ServiceCatalog from './service-catalog';
9
- import ServiceRegistry from './service-registry';
10
- import ServiceState from './service-state';
11
- import fedRampServices from './service-fed-ramp';
12
-
13
- const trailingSlashes = /(?:^\/)|(?:\/$)/;
14
-
15
- // The default cluster when one is not provided (usually as 'US' from hydra)
16
- export const DEFAULT_CLUSTER = 'urn:TEAM:us-east-2_a';
17
- // The default service name for convo (currently identityLookup due to some weird CSB issue)
18
- export const DEFAULT_CLUSTER_SERVICE = 'identityLookup';
19
-
20
- const CLUSTER_SERVICE = process.env.WEBEX_CONVERSATION_CLUSTER_SERVICE || DEFAULT_CLUSTER_SERVICE;
21
- const DEFAULT_CLUSTER_IDENTIFIER =
22
- process.env.WEBEX_CONVERSATION_DEFAULT_CLUSTER || `${DEFAULT_CLUSTER}:${CLUSTER_SERVICE}`;
23
-
24
- /* eslint-disable no-underscore-dangle */
25
- /**
26
- * @class
27
- */
28
- const Services = WebexPlugin.extend({
29
- namespace: 'Services',
30
-
31
- /**
32
- * The {@link WeakMap} of {@link ServiceRegistry} class instances that are
33
- * keyed with WebexCore instances.
34
- *
35
- * @instance
36
- * @type {WeakMap<WebexCore, ServiceRegistry>}
37
- * @private
38
- * @memberof Services
39
- */
40
- registries: new WeakMap(),
41
-
42
- /**
43
- * The {@link WeakMap} of {@link ServiceState} class instances that are
44
- * keyed with WebexCore instances.
45
- *
46
- * @instance
47
- * @type {WeakMap<WebexCore, ServiceState>}
48
- * @private
49
- * @memberof Services
50
- */
51
- states: new WeakMap(),
52
-
53
- props: {
54
- validateDomains: ['boolean', false, true],
55
- },
56
-
57
- _catalogs: new WeakMap(),
58
-
59
- _serviceUrls: null,
60
-
61
- /**
62
- * Get the registry associated with this webex instance.
63
- *
64
- * @private
65
- * @memberof Services
66
- * @returns {ServiceRegistry} - The associated {@link ServiceRegistry}.
67
- */
68
- getRegistry() {
69
- return this.registries.get(this.webex);
70
- },
71
-
72
- /**
73
- * Get the state associated with this webex instance.
74
- *
75
- * @private
76
- * @memberof Services
77
- * @returns {ServiceState} - The associated {@link ServiceState}.
78
- */
79
- getState() {
80
- return this.states.get(this.webex);
81
- },
82
-
83
- /**
84
- * @private
85
- * Get the current catalog based on the assocaited
86
- * webex instance.
87
- * @returns {ServiceCatalog}
88
- */
89
- _getCatalog() {
90
- return this._catalogs.get(this.webex);
91
- },
92
-
93
- /**
94
- * Get a service url from the current services list by name
95
- * from the associated instance catalog.
96
- * @param {string} name
97
- * @param {boolean} [priorityHost]
98
- * @param {string} [serviceGroup]
99
- * @returns {string|undefined}
100
- */
101
- get(name, priorityHost, serviceGroup) {
102
- const catalog = this._getCatalog();
103
-
104
- return catalog.get(name, priorityHost, serviceGroup);
105
- },
106
-
107
- /**
108
- * Determine if the catalog contains a specific service
109
- *
110
- * @param {string} serviceName - The service name to validate.
111
- * @returns {boolean} - True if the service exists.
112
- */
113
- hasService(serviceName) {
114
- return !!this.get(serviceName);
115
- },
116
-
117
- /**
118
- * Determine if a whilelist exists in the service catalog.
119
- *
120
- * @returns {boolean} - True if a allowed domains list exists.
121
- */
122
- hasAllowedDomains() {
123
- const catalog = this._getCatalog();
124
-
125
- return catalog.getAllowedDomains().length > 0;
126
- },
127
-
128
- /**
129
- * Generate a service catalog as an object from
130
- * the associated instance catalog.
131
- * @param {boolean} [priorityHost] - use highest priority host if set to `true`
132
- * @param {string} [serviceGroup]
133
- * @returns {Record<string, string>}
134
- */
135
- list(priorityHost, serviceGroup) {
136
- const catalog = this._getCatalog();
137
-
138
- return catalog.list(priorityHost, serviceGroup);
139
- },
140
-
141
- /**
142
- * Mark a priority host service url as failed.
143
- * This will mark the host associated with the
144
- * `ServiceUrl` to be removed from the its
145
- * respective host array, and then return the next
146
- * viable host from the `ServiceUrls` host array,
147
- * or the `ServiceUrls` default url if no other priority
148
- * hosts are available, or if `noPriorityHosts` is set to
149
- * `true`.
150
- * @param {string} url
151
- * @param {boolean} noPriorityHosts
152
- * @returns {string}
153
- */
154
- markFailedUrl(url, noPriorityHosts) {
155
- const catalog = this._getCatalog();
156
-
157
- return catalog.markFailedUrl(url, noPriorityHosts);
158
- },
159
-
160
- /**
161
- * saves all the services from the pre and post catalog service
162
- * @param {Object} serviceUrls
163
- * @returns {void}
164
- */
165
- _updateServiceUrls(serviceUrls) {
166
- this._serviceUrls = {...this._serviceUrls, ...serviceUrls};
167
- },
168
-
169
- /**
170
- * Update a list of `serviceUrls` to the most current
171
- * catalog via the defined `discoveryUrl` then returns the current
172
- * list of services.
173
- * @param {object} [param]
174
- * @param {string} [param.from] - This accepts `limited` or `signin`
175
- * @param {object} [param.query] - This accepts `email`, `orgId` or `userId` key values
176
- * @param {string} [param.query.email] - must be a standard-format email
177
- * @param {string} [param.query.orgId] - must be an organization id
178
- * @param {string} [param.query.userId] - must be a user id
179
- * @param {string} [param.token] - used for signin catalog
180
- * @returns {Promise<object>}
181
- */
182
- updateServices({from, query, token, forceRefresh} = {}) {
183
- const catalog = this._getCatalog();
184
- let formattedQuery;
185
- let serviceGroup;
186
-
187
- // map catalog name to service group name.
188
- switch (from) {
189
- case 'limited':
190
- serviceGroup = 'preauth';
191
- break;
192
- case 'signin':
193
- serviceGroup = 'signin';
194
- break;
195
- default:
196
- serviceGroup = 'postauth';
197
- break;
198
- }
199
-
200
- // confirm catalog update for group is not in progress.
201
- if (catalog.status[serviceGroup].collecting) {
202
- return this.waitForCatalog(serviceGroup);
203
- }
204
-
205
- catalog.status[serviceGroup].collecting = true;
206
-
207
- if (serviceGroup === 'preauth') {
208
- const queryKey = query && Object.keys(query)[0];
209
-
210
- if (!['email', 'emailhash', 'userId', 'orgId', 'mode'].includes(queryKey)) {
211
- return Promise.reject(
212
- new Error('a query param of email, emailhash, userId, orgId, or mode is required')
213
- );
214
- }
215
- }
216
- // encode email when query key is email
217
- if (serviceGroup === 'preauth' || serviceGroup === 'signin') {
218
- const queryKey = Object.keys(query)[0];
219
-
220
- formattedQuery = {};
221
-
222
- if (queryKey === 'email' && query.email) {
223
- formattedQuery.emailhash = sha256(query.email.toLowerCase()).toString();
224
- } else {
225
- formattedQuery[queryKey] = query[queryKey];
226
- }
227
- }
228
-
229
- return this._fetchNewServiceHostmap({
230
- from,
231
- token,
232
- query: formattedQuery,
233
- forceRefresh,
234
- })
235
- .then((serviceHostMap) => {
236
- catalog.updateServiceUrls(serviceGroup, serviceHostMap);
237
- this.updateCredentialsConfig();
238
- catalog.status[serviceGroup].collecting = false;
239
- })
240
- .catch((error) => {
241
- catalog.status[serviceGroup].collecting = false;
242
-
243
- return Promise.reject(error);
244
- });
245
- },
246
-
247
- /**
248
- * User validation parameter transfer object for {@link validateUser}.
249
- * @param {object} ValidateUserPTO
250
- * @property {string} ValidateUserPTO.email - The email of the user.
251
- * @property {string} [ValidateUserPTO.reqId] - The activation requester.
252
- * @property {object} [ValidateUserPTO.activationOptions] - Extra options to pass when sending the activation
253
- * @property {object} [ValidateUserPTO.preloginUserId] - The prelogin user id to set when sending the activation.
254
- */
255
-
256
- /**
257
- * User validation return transfer object for {@link validateUser}.
258
- * @param {object} ValidateUserRTO
259
- * @property {boolean} ValidateUserRTO.activated - If the user is activated.
260
- * @property {boolean} ValidateUserRTO.exists - If the user exists.
261
- * @property {string} ValidateUserRTO.details - A descriptive status message.
262
- * @property {object} ValidateUserRTO.user - **License** service user object.
263
- */
264
-
265
- /**
266
- * Validate if a user is activated and update the service catalogs as needed
267
- * based on the user's activation status.
268
- *
269
- * @param {ValidateUserPTO} - The parameter transfer object.
270
- * @returns {ValidateUserRTO} - The return transfer object.
271
- */
272
- validateUser({
273
- email,
274
- reqId = 'WEBCLIENT',
275
- forceRefresh = false,
276
- activationOptions = {},
277
- preloginUserId,
278
- }) {
279
- this.logger.info('services: validating a user');
280
-
281
- // Validate that an email parameter key was provided.
282
- if (!email) {
283
- return Promise.reject(new Error('`email` is required'));
284
- }
285
-
286
- // Destructure the credentials object.
287
- const {canAuthorize} = this.webex.credentials;
288
-
289
- // Validate that the user is already authorized.
290
- if (canAuthorize) {
291
- return this.updateServices({forceRefresh})
292
- .then(() => this.webex.credentials.getUserToken())
293
- .then((token) =>
294
- this.sendUserActivation({
295
- email,
296
- reqId,
297
- token: token.toString(),
298
- activationOptions,
299
- preloginUserId,
300
- })
301
- )
302
- .then((userObj) => ({
303
- activated: true,
304
- exists: true,
305
- details: 'user is authorized via a user token',
306
- user: userObj,
307
- }));
308
- }
309
-
310
- // Destructure the client authorization details.
311
- /* eslint-disable camelcase */
312
- const {client_id, client_secret} = this.webex.credentials.config;
313
-
314
- // Validate that client authentication details exist.
315
- if (!client_id || !client_secret) {
316
- return Promise.reject(new Error('client authentication details are not available'));
317
- }
318
- /* eslint-enable camelcase */
319
-
320
- // Declare a class-memeber-scoped token for usage within the promise chain.
321
- let token;
322
-
323
- // Begin client authentication user validation.
324
- return (
325
- this.collectPreauthCatalog({email})
326
- .then(() => {
327
- // Retrieve the service url from the updated catalog. This is required
328
- // since `WebexCore` is usually not fully initialized at the time this
329
- // request completes.
330
- const idbrokerService = this.get('idbroker', true);
331
-
332
- // Collect the client auth token.
333
- return this.webex.credentials.getClientToken({
334
- uri: `${idbrokerService}idb/oauth2/v1/access_token`,
335
- scope: 'webexsquare:admin webexsquare:get_conversation Identity:SCIM',
336
- });
337
- })
338
- .then((tokenObj) => {
339
- // Generate the token string.
340
- token = tokenObj.toString();
341
-
342
- // Collect the signin catalog using the client auth information.
343
- return this.collectSigninCatalog({email, token, forceRefresh});
344
- })
345
- // Validate if collecting the signin catalog failed and populate the RTO
346
- // with the appropriate content.
347
- .catch((error) => ({
348
- exists: error.name !== 'NotFound',
349
- activated: false,
350
- details:
351
- error.name !== 'NotFound'
352
- ? 'user exists but is not activated'
353
- : 'user does not exist and is not activated',
354
- }))
355
- // Validate if the previous promise resolved with an RTO and populate the
356
- // new RTO accordingly.
357
- .then((rto) =>
358
- Promise.all([
359
- rto || {
360
- activated: true,
361
- exists: true,
362
- details: 'user exists and is activated',
363
- },
364
- this.sendUserActivation({
365
- email,
366
- reqId,
367
- token,
368
- activationOptions,
369
- preloginUserId,
370
- }),
371
- ])
372
- )
373
- .then(([rto, user]) => ({...rto, user}))
374
- .catch((error) => {
375
- const response = {
376
- statusCode: error.statusCode,
377
- responseText: error.body && error.body.message,
378
- body: error.body,
379
- };
380
-
381
- return Promise.reject(response);
382
- })
383
- );
384
- },
385
-
386
- /**
387
- * Get user meeting preferences (preferred webex site).
388
- *
389
- * @returns {object} - User Information including user preferrences .
390
- */
391
- getMeetingPreferences() {
392
- return this.request({
393
- method: 'GET',
394
- service: 'hydra',
395
- resource: 'meetingPreferences',
396
- })
397
- .then((res) => {
398
- this.logger.info('services: received user region info');
399
-
400
- return res.body;
401
- })
402
- .catch((err) => {
403
- this.logger.info('services: was not able to fetch user login information', err);
404
- // resolve successfully even if request failed
405
- });
406
- },
407
-
408
- /**
409
- * Fetches client region info such as countryCode and timezone.
410
- *
411
- * @returns {object} - The region info object.
412
- */
413
- fetchClientRegionInfo() {
414
- return this.request({
415
- uri: 'https://ds.ciscospark.com/v1/region',
416
- addAuthHeader: false,
417
- headers: {
418
- 'spark-user-agent': null,
419
- },
420
- timeout: 5000,
421
- })
422
- .then((res) => {
423
- this.logger.info('services: received user region info');
424
-
425
- return res.body;
426
- })
427
- .catch((err) => {
428
- this.logger.info('services: was not able to get user region info', err);
429
- // resolve successfully even if request failed
430
- });
431
- },
432
-
433
- /**
434
- * User activation parameter transfer object for {@link sendUserActivation}.
435
- * @typedef {object} SendUserActivationPTO
436
- * @property {string} SendUserActivationPTO.email - The email of the user.
437
- * @property {string} SendUserActivationPTO.reqId - The activation requester.
438
- * @property {string} SendUserActivationPTO.token - The client auth token.
439
- * @property {object} SendUserActivationPTO.activationOptions - Extra options to pass when sending the activation.
440
- * @property {object} SendUserActivationPTO.preloginUserId - The prelogin user id to set when sending the activation.
441
- */
442
-
443
- /**
444
- * Send a request to activate a user using a client token.
445
- *
446
- * @param {SendUserActivationPTO} - The Parameter transfer object.
447
- * @returns {LicenseDTO} - The DTO returned from the **License** service.
448
- */
449
- sendUserActivation({email, reqId, token, activationOptions, preloginUserId}) {
450
- this.logger.info('services: sending user activation request');
451
- let countryCode;
452
- let timezone;
453
-
454
- // try to fetch client region info first
455
- return (
456
- this.fetchClientRegionInfo()
457
- .then((clientRegionInfo) => {
458
- if (clientRegionInfo) {
459
- ({countryCode, timezone} = clientRegionInfo);
460
- }
461
-
462
- // Send the user activation request to the **License** service.
463
- return this.request({
464
- service: 'license',
465
- resource: 'users/activations',
466
- method: 'POST',
467
- headers: {
468
- accept: 'application/json',
469
- authorization: token,
470
- 'x-prelogin-userid': preloginUserId,
471
- },
472
- body: {
473
- email,
474
- reqId,
475
- countryCode,
476
- timeZone: timezone,
477
- ...activationOptions,
478
- },
479
- shouldRefreshAccessToken: false,
480
- });
481
- })
482
- // On success, return the **License** user object.
483
- .then(({body}) => body)
484
- // On failure, reject with error from **License**.
485
- .catch((error) => Promise.reject(error))
486
- );
487
- },
488
-
489
- /**
490
- * simplified method to update the preauth catalog via email
491
- *
492
- * @param {object} query
493
- * @param {string} query.email - A standard format email.
494
- * @param {string} query.orgId - The user's OrgId.
495
- * @param {boolean} forceRefresh - Boolean to bypass u2c cache control header
496
- * @returns {Promise<void>}
497
- */
498
- collectPreauthCatalog(query, forceRefresh = false) {
499
- if (!query) {
500
- return this.updateServices({
501
- from: 'limited',
502
- query: {mode: 'DEFAULT_BY_PROXIMITY'},
503
- forceRefresh,
504
- });
505
- }
506
-
507
- return this.updateServices({from: 'limited', query, forceRefresh});
508
- },
509
-
510
- /**
511
- * simplified method to update the signin catalog via email and token
512
- * @param {object} param
513
- * @param {string} param.email - must be a standard-format email
514
- * @param {string} param.token - must be a client token
515
- * @returns {Promise<void>}
516
- */
517
- collectSigninCatalog({email, token, forceRefresh} = {}) {
518
- if (!email) {
519
- return Promise.reject(new Error('`email` is required'));
520
- }
521
- if (!token) {
522
- return Promise.reject(new Error('`token` is required'));
523
- }
524
-
525
- return this.updateServices({
526
- from: 'signin',
527
- query: {email},
528
- token,
529
- forceRefresh,
530
- });
531
- },
532
-
533
- /**
534
- * Updates credentials config to utilize u2c catalog
535
- * urls.
536
- * @returns {void}
537
- */
538
- updateCredentialsConfig() {
539
- const {idbroker, identity} = this.list(true);
540
-
541
- if (idbroker && identity) {
542
- const {authorizationString, authorizeUrl} = this.webex.config.credentials;
543
-
544
- // This must be set outside of the setConfig method used to assign the
545
- // idbroker and identity url values.
546
- this.webex.config.credentials.authorizeUrl = authorizationString
547
- ? authorizeUrl
548
- : `${idbroker.replace(trailingSlashes, '')}/idb/oauth2/v1/authorize`;
549
-
550
- this.webex.setConfig({
551
- credentials: {
552
- idbroker: {
553
- url: idbroker.replace(trailingSlashes, ''), // remove trailing slash
554
- },
555
- identity: {
556
- url: identity.replace(trailingSlashes, ''), // remove trailing slash
557
- },
558
- },
559
- });
560
- }
561
- },
562
-
563
- /**
564
- * Wait until the service catalog is available,
565
- * or reject afte ra timeout of 60 seconds.
566
- * @param {string} serviceGroup
567
- * @param {number} [timeout] - in seconds
568
- * @returns {Promise<void>}
569
- */
570
- waitForCatalog(serviceGroup, timeout) {
571
- const catalog = this._getCatalog();
572
- const {supertoken} = this.webex.credentials;
573
-
574
- if (
575
- serviceGroup === 'postauth' &&
576
- supertoken &&
577
- supertoken.access_token &&
578
- !catalog.status.postauth.collecting &&
579
- !catalog.status.postauth.ready
580
- ) {
581
- if (!catalog.status.preauth.ready) {
582
- return this.initServiceCatalogs();
583
- }
584
-
585
- return this.updateServices();
586
- }
587
-
588
- return catalog.waitForCatalog(serviceGroup, timeout);
589
- },
590
-
591
- /**
592
- * Service waiting parameter transfer object for {@link waitForService}.
593
- *
594
- * @typedef {object} WaitForServicePTO
595
- * @property {string} [WaitForServicePTO.name] - The service name.
596
- * @property {string} [WaitForServicePTO.url] - The service url.
597
- * @property {string} [WaitForServicePTO.timeout] - wait duration in seconds.
598
- */
599
-
600
- /**
601
- * Wait until the service has been ammended to any service catalog. This
602
- * method prioritizes the service name over the service url when searching.
603
- *
604
- * @param {WaitForServicePTO} - The parameter transfer object.
605
- * @returns {Promise<string>} - Resolves to the priority host of a service.
606
- */
607
- waitForService({name, timeout = 5, url}) {
608
- const {services} = this.webex.config;
609
-
610
- // Save memory by grabbing the catalog after there isn't a priortyURL
611
- const catalog = this._getCatalog();
612
-
613
- const fetchFromServiceUrl = services.servicesNotNeedValidation.find(
614
- (service) => service === name
615
- );
616
-
617
- if (fetchFromServiceUrl) {
618
- return Promise.resolve(this._serviceUrls[name]);
619
- }
620
-
621
- const priorityUrl = this.get(name, true);
622
- const priorityUrlObj = this.getServiceFromUrl(url);
623
-
624
- if (priorityUrl || priorityUrlObj) {
625
- return Promise.resolve(priorityUrl || priorityUrlObj.priorityUrl);
626
- }
627
-
628
- if (catalog.isReady) {
629
- if (url) {
630
- return Promise.resolve(url);
631
- }
632
-
633
- this.webex.internal.metrics.submitClientMetrics(METRICS.JS_SDK_SERVICE_NOT_FOUND, {
634
- fields: {service_name: name},
635
- });
636
-
637
- return Promise.reject(
638
- new Error(`services: service '${name}' was not found in any of the catalogs`)
639
- );
640
- }
641
-
642
- return new Promise((resolve, reject) => {
643
- const groupsToCheck = ['preauth', 'signin', 'postauth'];
644
- const checkCatalog = (catalogGroup) =>
645
- catalog
646
- .waitForCatalog(catalogGroup, timeout)
647
- .then(() => {
648
- const scopedPriorityUrl = this.get(name, true);
649
- const scopedPrioriryUrlObj = this.getServiceFromUrl(url);
650
-
651
- if (scopedPriorityUrl || scopedPrioriryUrlObj) {
652
- resolve(scopedPriorityUrl || scopedPrioriryUrlObj.priorityUrl);
653
- }
654
- })
655
- .catch(() => undefined);
656
-
657
- Promise.all(groupsToCheck.map((group) => checkCatalog(group))).then(() => {
658
- this.webex.internal.metrics.submitClientMetrics(METRICS.JS_SDK_SERVICE_NOT_FOUND, {
659
- fields: {service_name: name},
660
- });
661
- reject(new Error(`services: service '${name}' was not found after waiting`));
662
- });
663
- });
664
- },
665
-
666
- /**
667
- * @private
668
- * Organize a received hostmap from a service
669
- * catalog endpoint.
670
- * @param {object} serviceHostmap
671
- * @returns {object}
672
- */
673
- _formatReceivedHostmap(serviceHostmap) {
674
- // map the host catalog items to a formatted hostmap
675
- const formattedHostmap = Object.keys(serviceHostmap.hostCatalog).reduce((accumulator, key) => {
676
- if (serviceHostmap.hostCatalog[key].length === 0) {
677
- return accumulator;
678
- }
679
-
680
- const serviceName = serviceHostmap.hostCatalog[key][0].id.split(':')[3];
681
- const defaultUrl = serviceHostmap.serviceLinks[serviceName];
682
-
683
- let serviceItem = accumulator.find((item) => item.name === serviceName);
684
-
685
- if (!serviceItem) {
686
- serviceItem = {
687
- name: serviceName,
688
- defaultUrl,
689
- defaultHost: Url.parse(defaultUrl).hostname,
690
- hosts: [],
691
- };
692
-
693
- accumulator.push(serviceItem);
694
- }
695
-
696
- serviceItem.hosts.push(
697
- // map the default key as a low priority default for cluster matching
698
- {
699
- host: key,
700
- ttl: -1,
701
- priority: 10,
702
- id: serviceHostmap.hostCatalog[key][0].id,
703
- homeCluster: serviceItem.defaultHost === key,
704
- },
705
- // map the rest of the hosts in their proper locations
706
- ...serviceHostmap.hostCatalog[key].map((host) => ({
707
- ...host,
708
- homeCluster: serviceItem.defaultHost === key,
709
- }))
710
- );
711
-
712
- return accumulator;
713
- }, []);
714
-
715
- // append service links that do not exist in the host catalog
716
- Object.keys(serviceHostmap.serviceLinks).forEach((key) => {
717
- const service = formattedHostmap.find((item) => item.name === key);
718
-
719
- if (!service) {
720
- formattedHostmap.push({
721
- name: key,
722
- defaultUrl: serviceHostmap.serviceLinks[key],
723
- defaultHost: Url.parse(serviceHostmap.serviceLinks[key]).hostname,
724
- hosts: [],
725
- });
726
- }
727
- });
728
-
729
- // update all the service urls in the host catalog
730
-
731
- this._updateServiceUrls(serviceHostmap.serviceLinks);
732
-
733
- return formattedHostmap;
734
- },
735
-
736
- /**
737
- * Get the clusterId associated with a URL string.
738
- * @param {string} url
739
- * @returns {string} - Cluster ID of url provided
740
- */
741
- getClusterId(url) {
742
- const catalog = this._getCatalog();
743
-
744
- return catalog.findClusterId(url);
745
- },
746
-
747
- /**
748
- * Get a service value from a provided clusterId. This method will
749
- * return an object containing both the name and url of a found service.
750
- * @param {object} params
751
- * @param {string} params.clusterId - clusterId of found service
752
- * @param {boolean} [params.priorityHost] - returns priority host url if true
753
- * @param {string} [params.serviceGroup] - specify service group
754
- * @returns {object} service
755
- * @returns {string} service.name
756
- * @returns {string} service.url
757
- */
758
- getServiceFromClusterId(params) {
759
- const catalog = this._getCatalog();
760
-
761
- return catalog.findServiceFromClusterId(params);
762
- },
763
-
764
- /**
765
- * @param {String} cluster the cluster containing the id
766
- * @param {UUID} [id] the id of the conversation.
767
- * If empty, just return the base URL.
768
- * @returns {String} url of the service
769
- */
770
- getServiceUrlFromClusterId({cluster = 'us'} = {}) {
771
- let clusterId = cluster === 'us' ? DEFAULT_CLUSTER_IDENTIFIER : cluster;
772
-
773
- // Determine if cluster has service name (non-US clusters from hydra do not)
774
- if (clusterId.split(':').length < 4) {
775
- // Add Service to cluster identifier
776
- clusterId = `${cluster}:${CLUSTER_SERVICE}`;
777
- }
778
-
779
- const {url} = this.getServiceFromClusterId({clusterId}) || {};
780
-
781
- if (!url) {
782
- throw Error(`Could not find service for cluster [${cluster}]`);
783
- }
784
-
785
- return url;
786
- },
787
-
788
- /**
789
- * Get a service object from a service url if the service url exists in the
790
- * catalog.
791
- *
792
- * @param {string} url - The url to be validated.
793
- * @returns {object} - Service object.
794
- * @returns {object.name} - The name of the service found.
795
- * @returns {object.priorityUrl} - The priority url of the found service.
796
- * @returns {object.defaultUrl} - The default url of the found service.
797
- */
798
- getServiceFromUrl(url = '') {
799
- const service = this._getCatalog().findServiceUrlFromUrl(url);
800
-
801
- if (!service) {
802
- return undefined;
803
- }
804
-
805
- return {
806
- name: service.name,
807
- priorityUrl: service.get(true),
808
- defaultUrl: service.get(),
809
- };
810
- },
811
-
812
- /**
813
- * Verify that a provided url exists in the service
814
- * catalog.
815
- * @param {string} url
816
- * @returns {boolean} - true if exists, false otherwise
817
- */
818
- isServiceUrl(url) {
819
- const catalog = this._getCatalog();
820
-
821
- return !!catalog.findServiceUrlFromUrl(url);
822
- },
823
-
824
- /**
825
- * Determine if a provided url is in the catalog's allowed domains.
826
- *
827
- * @param {string} url - The url to match allowed domains against.
828
- * @returns {boolean} - True if the url provided is allowed.
829
- */
830
- isAllowedDomainUrl(url) {
831
- const catalog = this._getCatalog();
832
-
833
- return !!catalog.findAllowedDomain(url);
834
- },
835
-
836
- /**
837
- * Converts the host portion of the url from default host
838
- * to a priority host
839
- *
840
- * @param {string} url a service url that contains a default host
841
- * @returns {string} a service url that contains the top priority host.
842
- * @throws if url isn't a service url
843
- */
844
- convertUrlToPriorityHostUrl(url = '') {
845
- const data = this.getServiceFromUrl(url);
846
-
847
- if (!data) {
848
- throw Error(`No service associated with url: [${url}]`);
849
- }
850
-
851
- return url.replace(data.defaultUrl, data.priorityUrl);
852
- },
853
-
854
- /**
855
- * @private
856
- * Simplified method wrapper for sending a request to get
857
- * an updated service hostmap.
858
- * @param {object} [param]
859
- * @param {string} [param.from] - This accepts `limited` or `signin`
860
- * @param {object} [param.query] - This accepts `email`, `orgId` or `userId` key values
861
- * @param {string} [param.query.email] - must be a standard-format email
862
- * @param {string} [param.query.orgId] - must be an organization id
863
- * @param {string} [param.query.userId] - must be a user id
864
- * @param {string} [param.token] - used for signin catalog
865
- * @returns {Promise<object>}
866
- */
867
- _fetchNewServiceHostmap({from, query, token, forceRefresh} = {}) {
868
- const service = 'u2c';
869
- const resource = from ? `/${from}/catalog` : '/catalog';
870
- const qs = {...query, format: 'hostmap'};
871
-
872
- if (forceRefresh) {
873
- qs.timestamp = new Date().getTime();
874
- }
875
-
876
- const requestObject = {
877
- method: 'GET',
878
- service,
879
- resource,
880
- qs,
881
- };
882
-
883
- if (token) {
884
- requestObject.headers = {authorization: token};
885
- }
886
-
887
- return this.request(requestObject).then(({body}) => this._formatReceivedHostmap(body));
888
- },
889
-
890
- /**
891
- * Initialize the discovery services and the whitelisted services.
892
- *
893
- * @returns {void}
894
- */
895
- initConfig() {
896
- // Get the catalog and destructure the services config.
897
- const catalog = this._getCatalog();
898
- const {services, fedramp} = this.webex.config;
899
-
900
- // Validate that the services configuration exists.
901
- if (services) {
902
- if (fedramp) {
903
- services.discovery = fedRampServices;
904
- }
905
- // Check for discovery services.
906
- if (services.discovery) {
907
- // Format the discovery configuration into an injectable array.
908
- const formattedDiscoveryServices = Object.keys(services.discovery).map((key) => ({
909
- name: key,
910
- defaultUrl: services.discovery[key],
911
- }));
912
-
913
- // Inject formatted discovery services into services catalog.
914
- catalog.updateServiceUrls('discovery', formattedDiscoveryServices);
915
- }
916
-
917
- if (services.override) {
918
- // Format the override configuration into an injectable array.
919
- const formattedOverrideServices = Object.keys(services.override).map((key) => ({
920
- name: key,
921
- defaultUrl: services.override[key],
922
- }));
923
-
924
- // Inject formatted override services into services catalog.
925
- catalog.updateServiceUrls('override', formattedOverrideServices);
926
- }
927
-
928
- // Check for allowed host domains.
929
- if (services.allowedDomains) {
930
- // Store the allowed domains as a property of the catalog.
931
- catalog.setAllowedDomains(services.allowedDomains);
932
- }
933
-
934
- // Set `validateDomains` property to match configuration
935
- this.validateDomains = services.validateDomains;
936
- }
937
- },
938
-
939
- /**
940
- * Make the initial requests to collect the root catalogs.
941
- *
942
- * @returns {Promise<void, Error>} - Errors if the token is unavailable.
943
- */
944
- initServiceCatalogs() {
945
- this.logger.info('services: initializing initial service catalogs');
946
-
947
- // Destructure the credentials plugin.
948
- const {credentials} = this.webex;
949
-
950
- // Init a promise chain. Must be done as a Promise.resolve() to allow
951
- // credentials#getOrgId() to properly throw.
952
- return (
953
- Promise.resolve()
954
- // Get the user's OrgId.
955
- .then(() => credentials.getOrgId())
956
- // Begin collecting the preauth/limited catalog.
957
- .then((orgId) => this.collectPreauthCatalog({orgId}))
958
- .then(() => {
959
- // Validate if the token is authorized.
960
- if (credentials.canAuthorize) {
961
- // Attempt to collect the postauth catalog.
962
- return this.updateServices().catch(() =>
963
- this.logger.warn('services: cannot retrieve postauth catalog')
964
- );
965
- }
966
-
967
- // Return a resolved promise for consistent return value.
968
- return Promise.resolve();
969
- })
970
- );
971
- },
972
-
973
- /**
974
- * Initializer
975
- *
976
- * @instance
977
- * @memberof Services
978
- * @returns {Services}
979
- */
980
- initialize() {
981
- const catalog = new ServiceCatalog();
982
- const registry = new ServiceRegistry();
983
- const state = new ServiceState();
984
-
985
- this._catalogs.set(this.webex, catalog);
986
- this.registries.set(this.webex, registry);
987
- this.states.set(this.webex, state);
988
-
989
- // Listen for configuration changes once.
990
- this.listenToOnce(this.webex, 'change:config', () => {
991
- this.initConfig();
992
- });
993
-
994
- // wait for webex instance to be ready before attempting
995
- // to update the service catalogs
996
- this.listenToOnce(this.webex, 'ready', () => {
997
- const {supertoken} = this.webex.credentials;
998
-
999
- // Validate if the supertoken exists.
1000
- if (supertoken && supertoken.access_token) {
1001
- this.initServiceCatalogs()
1002
- .then(() => {
1003
- catalog.isReady = true;
1004
- })
1005
- .catch((error) =>
1006
- this.logger.error(`services: failed to init initial services, ${error.message}`)
1007
- );
1008
- } else {
1009
- const {email} = this.webex.config;
1010
-
1011
- this.collectPreauthCatalog(email ? {email} : undefined);
1012
- }
1013
- });
1014
- },
1015
- });
1016
- /* eslint-enable no-underscore-dangle */
1017
-
1018
- export default Services;
1
+ import Url from 'url';
2
+
3
+ import sha256 from 'crypto-js/sha256';
4
+
5
+ import WebexPlugin from '../webex-plugin';
6
+
7
+ import METRICS from './metrics';
8
+ import ServiceCatalog from './service-catalog';
9
+ import ServiceRegistry from './service-registry';
10
+ import ServiceState from './service-state';
11
+ import fedRampServices from './service-fed-ramp';
12
+
13
+ const trailingSlashes = /(?:^\/)|(?:\/$)/;
14
+
15
+ // The default cluster when one is not provided (usually as 'US' from hydra)
16
+ export const DEFAULT_CLUSTER = 'urn:TEAM:us-east-2_a';
17
+ // The default service name for convo (currently identityLookup due to some weird CSB issue)
18
+ export const DEFAULT_CLUSTER_SERVICE = 'identityLookup';
19
+
20
+ const CLUSTER_SERVICE = process.env.WEBEX_CONVERSATION_CLUSTER_SERVICE || DEFAULT_CLUSTER_SERVICE;
21
+ const DEFAULT_CLUSTER_IDENTIFIER =
22
+ process.env.WEBEX_CONVERSATION_DEFAULT_CLUSTER || `${DEFAULT_CLUSTER}:${CLUSTER_SERVICE}`;
23
+
24
+ /* eslint-disable no-underscore-dangle */
25
+ /**
26
+ * @class
27
+ */
28
+ const Services = WebexPlugin.extend({
29
+ namespace: 'Services',
30
+
31
+ /**
32
+ * The {@link WeakMap} of {@link ServiceRegistry} class instances that are
33
+ * keyed with WebexCore instances.
34
+ *
35
+ * @instance
36
+ * @type {WeakMap<WebexCore, ServiceRegistry>}
37
+ * @private
38
+ * @memberof Services
39
+ */
40
+ registries: new WeakMap(),
41
+
42
+ /**
43
+ * The {@link WeakMap} of {@link ServiceState} class instances that are
44
+ * keyed with WebexCore instances.
45
+ *
46
+ * @instance
47
+ * @type {WeakMap<WebexCore, ServiceState>}
48
+ * @private
49
+ * @memberof Services
50
+ */
51
+ states: new WeakMap(),
52
+
53
+ props: {
54
+ validateDomains: ['boolean', false, true],
55
+ },
56
+
57
+ _catalogs: new WeakMap(),
58
+
59
+ _serviceUrls: null,
60
+
61
+ /**
62
+ * Get the registry associated with this webex instance.
63
+ *
64
+ * @private
65
+ * @memberof Services
66
+ * @returns {ServiceRegistry} - The associated {@link ServiceRegistry}.
67
+ */
68
+ getRegistry() {
69
+ return this.registries.get(this.webex);
70
+ },
71
+
72
+ /**
73
+ * Get the state associated with this webex instance.
74
+ *
75
+ * @private
76
+ * @memberof Services
77
+ * @returns {ServiceState} - The associated {@link ServiceState}.
78
+ */
79
+ getState() {
80
+ return this.states.get(this.webex);
81
+ },
82
+
83
+ /**
84
+ * @private
85
+ * Get the current catalog based on the assocaited
86
+ * webex instance.
87
+ * @returns {ServiceCatalog}
88
+ */
89
+ _getCatalog() {
90
+ return this._catalogs.get(this.webex);
91
+ },
92
+
93
+ /**
94
+ * Get a service url from the current services list by name
95
+ * from the associated instance catalog.
96
+ * @param {string} name
97
+ * @param {boolean} [priorityHost]
98
+ * @param {string} [serviceGroup]
99
+ * @returns {string|undefined}
100
+ */
101
+ get(name, priorityHost, serviceGroup) {
102
+ const catalog = this._getCatalog();
103
+
104
+ return catalog.get(name, priorityHost, serviceGroup);
105
+ },
106
+
107
+ /**
108
+ * Determine if the catalog contains a specific service
109
+ *
110
+ * @param {string} serviceName - The service name to validate.
111
+ * @returns {boolean} - True if the service exists.
112
+ */
113
+ hasService(serviceName) {
114
+ return !!this.get(serviceName);
115
+ },
116
+
117
+ /**
118
+ * Determine if a whilelist exists in the service catalog.
119
+ *
120
+ * @returns {boolean} - True if a allowed domains list exists.
121
+ */
122
+ hasAllowedDomains() {
123
+ const catalog = this._getCatalog();
124
+
125
+ return catalog.getAllowedDomains().length > 0;
126
+ },
127
+
128
+ /**
129
+ * Generate a service catalog as an object from
130
+ * the associated instance catalog.
131
+ * @param {boolean} [priorityHost] - use highest priority host if set to `true`
132
+ * @param {string} [serviceGroup]
133
+ * @returns {Record<string, string>}
134
+ */
135
+ list(priorityHost, serviceGroup) {
136
+ const catalog = this._getCatalog();
137
+
138
+ return catalog.list(priorityHost, serviceGroup);
139
+ },
140
+
141
+ /**
142
+ * Mark a priority host service url as failed.
143
+ * This will mark the host associated with the
144
+ * `ServiceUrl` to be removed from the its
145
+ * respective host array, and then return the next
146
+ * viable host from the `ServiceUrls` host array,
147
+ * or the `ServiceUrls` default url if no other priority
148
+ * hosts are available, or if `noPriorityHosts` is set to
149
+ * `true`.
150
+ * @param {string} url
151
+ * @param {boolean} noPriorityHosts
152
+ * @returns {string}
153
+ */
154
+ markFailedUrl(url, noPriorityHosts) {
155
+ const catalog = this._getCatalog();
156
+
157
+ return catalog.markFailedUrl(url, noPriorityHosts);
158
+ },
159
+
160
+ /**
161
+ * saves all the services from the pre and post catalog service
162
+ * @param {Object} serviceUrls
163
+ * @returns {void}
164
+ */
165
+ _updateServiceUrls(serviceUrls) {
166
+ this._serviceUrls = {...this._serviceUrls, ...serviceUrls};
167
+ },
168
+
169
+ /**
170
+ * Update a list of `serviceUrls` to the most current
171
+ * catalog via the defined `discoveryUrl` then returns the current
172
+ * list of services.
173
+ * @param {object} [param]
174
+ * @param {string} [param.from] - This accepts `limited` or `signin`
175
+ * @param {object} [param.query] - This accepts `email`, `orgId` or `userId` key values
176
+ * @param {string} [param.query.email] - must be a standard-format email
177
+ * @param {string} [param.query.orgId] - must be an organization id
178
+ * @param {string} [param.query.userId] - must be a user id
179
+ * @param {string} [param.token] - used for signin catalog
180
+ * @returns {Promise<object>}
181
+ */
182
+ updateServices({from, query, token, forceRefresh} = {}) {
183
+ const catalog = this._getCatalog();
184
+ let formattedQuery;
185
+ let serviceGroup;
186
+
187
+ // map catalog name to service group name.
188
+ switch (from) {
189
+ case 'limited':
190
+ serviceGroup = 'preauth';
191
+ break;
192
+ case 'signin':
193
+ serviceGroup = 'signin';
194
+ break;
195
+ default:
196
+ serviceGroup = 'postauth';
197
+ break;
198
+ }
199
+
200
+ // confirm catalog update for group is not in progress.
201
+ if (catalog.status[serviceGroup].collecting) {
202
+ return this.waitForCatalog(serviceGroup);
203
+ }
204
+
205
+ catalog.status[serviceGroup].collecting = true;
206
+
207
+ if (serviceGroup === 'preauth') {
208
+ const queryKey = query && Object.keys(query)[0];
209
+
210
+ if (!['email', 'emailhash', 'userId', 'orgId', 'mode'].includes(queryKey)) {
211
+ return Promise.reject(
212
+ new Error('a query param of email, emailhash, userId, orgId, or mode is required')
213
+ );
214
+ }
215
+ }
216
+ // encode email when query key is email
217
+ if (serviceGroup === 'preauth' || serviceGroup === 'signin') {
218
+ const queryKey = Object.keys(query)[0];
219
+
220
+ formattedQuery = {};
221
+
222
+ if (queryKey === 'email' && query.email) {
223
+ formattedQuery.emailhash = sha256(query.email.toLowerCase()).toString();
224
+ } else {
225
+ formattedQuery[queryKey] = query[queryKey];
226
+ }
227
+ }
228
+
229
+ return this._fetchNewServiceHostmap({
230
+ from,
231
+ token,
232
+ query: formattedQuery,
233
+ forceRefresh,
234
+ })
235
+ .then((serviceHostMap) => {
236
+ catalog.updateServiceUrls(serviceGroup, serviceHostMap);
237
+ this.updateCredentialsConfig();
238
+ catalog.status[serviceGroup].collecting = false;
239
+ })
240
+ .catch((error) => {
241
+ catalog.status[serviceGroup].collecting = false;
242
+
243
+ return Promise.reject(error);
244
+ });
245
+ },
246
+
247
+ /**
248
+ * User validation parameter transfer object for {@link validateUser}.
249
+ * @param {object} ValidateUserPTO
250
+ * @property {string} ValidateUserPTO.email - The email of the user.
251
+ * @property {string} [ValidateUserPTO.reqId] - The activation requester.
252
+ * @property {object} [ValidateUserPTO.activationOptions] - Extra options to pass when sending the activation
253
+ * @property {object} [ValidateUserPTO.preloginUserId] - The prelogin user id to set when sending the activation.
254
+ */
255
+
256
+ /**
257
+ * User validation return transfer object for {@link validateUser}.
258
+ * @param {object} ValidateUserRTO
259
+ * @property {boolean} ValidateUserRTO.activated - If the user is activated.
260
+ * @property {boolean} ValidateUserRTO.exists - If the user exists.
261
+ * @property {string} ValidateUserRTO.details - A descriptive status message.
262
+ * @property {object} ValidateUserRTO.user - **License** service user object.
263
+ */
264
+
265
+ /**
266
+ * Validate if a user is activated and update the service catalogs as needed
267
+ * based on the user's activation status.
268
+ *
269
+ * @param {ValidateUserPTO} - The parameter transfer object.
270
+ * @returns {ValidateUserRTO} - The return transfer object.
271
+ */
272
+ validateUser({
273
+ email,
274
+ reqId = 'WEBCLIENT',
275
+ forceRefresh = false,
276
+ activationOptions = {},
277
+ preloginUserId,
278
+ }) {
279
+ this.logger.info('services: validating a user');
280
+
281
+ // Validate that an email parameter key was provided.
282
+ if (!email) {
283
+ return Promise.reject(new Error('`email` is required'));
284
+ }
285
+
286
+ // Destructure the credentials object.
287
+ const {canAuthorize} = this.webex.credentials;
288
+
289
+ // Validate that the user is already authorized.
290
+ if (canAuthorize) {
291
+ return this.updateServices({forceRefresh})
292
+ .then(() => this.webex.credentials.getUserToken())
293
+ .then((token) =>
294
+ this.sendUserActivation({
295
+ email,
296
+ reqId,
297
+ token: token.toString(),
298
+ activationOptions,
299
+ preloginUserId,
300
+ })
301
+ )
302
+ .then((userObj) => ({
303
+ activated: true,
304
+ exists: true,
305
+ details: 'user is authorized via a user token',
306
+ user: userObj,
307
+ }));
308
+ }
309
+
310
+ // Destructure the client authorization details.
311
+ /* eslint-disable camelcase */
312
+ const {client_id, client_secret} = this.webex.credentials.config;
313
+
314
+ // Validate that client authentication details exist.
315
+ if (!client_id || !client_secret) {
316
+ return Promise.reject(new Error('client authentication details are not available'));
317
+ }
318
+ /* eslint-enable camelcase */
319
+
320
+ // Declare a class-memeber-scoped token for usage within the promise chain.
321
+ let token;
322
+
323
+ // Begin client authentication user validation.
324
+ return (
325
+ this.collectPreauthCatalog({email})
326
+ .then(() => {
327
+ // Retrieve the service url from the updated catalog. This is required
328
+ // since `WebexCore` is usually not fully initialized at the time this
329
+ // request completes.
330
+ const idbrokerService = this.get('idbroker', true);
331
+
332
+ // Collect the client auth token.
333
+ return this.webex.credentials.getClientToken({
334
+ uri: `${idbrokerService}idb/oauth2/v1/access_token`,
335
+ scope: 'webexsquare:admin webexsquare:get_conversation Identity:SCIM',
336
+ });
337
+ })
338
+ .then((tokenObj) => {
339
+ // Generate the token string.
340
+ token = tokenObj.toString();
341
+
342
+ // Collect the signin catalog using the client auth information.
343
+ return this.collectSigninCatalog({email, token, forceRefresh});
344
+ })
345
+ // Validate if collecting the signin catalog failed and populate the RTO
346
+ // with the appropriate content.
347
+ .catch((error) => ({
348
+ exists: error.name !== 'NotFound',
349
+ activated: false,
350
+ details:
351
+ error.name !== 'NotFound'
352
+ ? 'user exists but is not activated'
353
+ : 'user does not exist and is not activated',
354
+ }))
355
+ // Validate if the previous promise resolved with an RTO and populate the
356
+ // new RTO accordingly.
357
+ .then((rto) =>
358
+ Promise.all([
359
+ rto || {
360
+ activated: true,
361
+ exists: true,
362
+ details: 'user exists and is activated',
363
+ },
364
+ this.sendUserActivation({
365
+ email,
366
+ reqId,
367
+ token,
368
+ activationOptions,
369
+ preloginUserId,
370
+ }),
371
+ ])
372
+ )
373
+ .then(([rto, user]) => ({...rto, user}))
374
+ .catch((error) => {
375
+ const response = {
376
+ statusCode: error.statusCode,
377
+ responseText: error.body && error.body.message,
378
+ body: error.body,
379
+ };
380
+
381
+ return Promise.reject(response);
382
+ })
383
+ );
384
+ },
385
+
386
+ /**
387
+ * Get user meeting preferences (preferred webex site).
388
+ *
389
+ * @returns {object} - User Information including user preferrences .
390
+ */
391
+ getMeetingPreferences() {
392
+ return this.request({
393
+ method: 'GET',
394
+ service: 'hydra',
395
+ resource: 'meetingPreferences',
396
+ })
397
+ .then((res) => {
398
+ this.logger.info('services: received user region info');
399
+
400
+ return res.body;
401
+ })
402
+ .catch((err) => {
403
+ this.logger.info('services: was not able to fetch user login information', err);
404
+ // resolve successfully even if request failed
405
+ });
406
+ },
407
+
408
+ /**
409
+ * Fetches client region info such as countryCode and timezone.
410
+ *
411
+ * @returns {object} - The region info object.
412
+ */
413
+ fetchClientRegionInfo() {
414
+ return this.request({
415
+ uri: 'https://ds.ciscospark.com/v1/region',
416
+ addAuthHeader: false,
417
+ headers: {
418
+ 'spark-user-agent': null,
419
+ },
420
+ timeout: 5000,
421
+ })
422
+ .then((res) => {
423
+ this.logger.info('services: received user region info');
424
+
425
+ return res.body;
426
+ })
427
+ .catch((err) => {
428
+ this.logger.info('services: was not able to get user region info', err);
429
+ // resolve successfully even if request failed
430
+ });
431
+ },
432
+
433
+ /**
434
+ * User activation parameter transfer object for {@link sendUserActivation}.
435
+ * @typedef {object} SendUserActivationPTO
436
+ * @property {string} SendUserActivationPTO.email - The email of the user.
437
+ * @property {string} SendUserActivationPTO.reqId - The activation requester.
438
+ * @property {string} SendUserActivationPTO.token - The client auth token.
439
+ * @property {object} SendUserActivationPTO.activationOptions - Extra options to pass when sending the activation.
440
+ * @property {object} SendUserActivationPTO.preloginUserId - The prelogin user id to set when sending the activation.
441
+ */
442
+
443
+ /**
444
+ * Send a request to activate a user using a client token.
445
+ *
446
+ * @param {SendUserActivationPTO} - The Parameter transfer object.
447
+ * @returns {LicenseDTO} - The DTO returned from the **License** service.
448
+ */
449
+ sendUserActivation({email, reqId, token, activationOptions, preloginUserId}) {
450
+ this.logger.info('services: sending user activation request');
451
+ let countryCode;
452
+ let timezone;
453
+
454
+ // try to fetch client region info first
455
+ return (
456
+ this.fetchClientRegionInfo()
457
+ .then((clientRegionInfo) => {
458
+ if (clientRegionInfo) {
459
+ ({countryCode, timezone} = clientRegionInfo);
460
+ }
461
+
462
+ // Send the user activation request to the **License** service.
463
+ return this.request({
464
+ service: 'license',
465
+ resource: 'users/activations',
466
+ method: 'POST',
467
+ headers: {
468
+ accept: 'application/json',
469
+ authorization: token,
470
+ 'x-prelogin-userid': preloginUserId,
471
+ },
472
+ body: {
473
+ email,
474
+ reqId,
475
+ countryCode,
476
+ timeZone: timezone,
477
+ ...activationOptions,
478
+ },
479
+ shouldRefreshAccessToken: false,
480
+ });
481
+ })
482
+ // On success, return the **License** user object.
483
+ .then(({body}) => body)
484
+ // On failure, reject with error from **License**.
485
+ .catch((error) => Promise.reject(error))
486
+ );
487
+ },
488
+
489
+ /**
490
+ * simplified method to update the preauth catalog via email
491
+ *
492
+ * @param {object} query
493
+ * @param {string} query.email - A standard format email.
494
+ * @param {string} query.orgId - The user's OrgId.
495
+ * @param {boolean} forceRefresh - Boolean to bypass u2c cache control header
496
+ * @returns {Promise<void>}
497
+ */
498
+ collectPreauthCatalog(query, forceRefresh = false) {
499
+ if (!query) {
500
+ return this.updateServices({
501
+ from: 'limited',
502
+ query: {mode: 'DEFAULT_BY_PROXIMITY'},
503
+ forceRefresh,
504
+ });
505
+ }
506
+
507
+ return this.updateServices({from: 'limited', query, forceRefresh});
508
+ },
509
+
510
+ /**
511
+ * simplified method to update the signin catalog via email and token
512
+ * @param {object} param
513
+ * @param {string} param.email - must be a standard-format email
514
+ * @param {string} param.token - must be a client token
515
+ * @returns {Promise<void>}
516
+ */
517
+ collectSigninCatalog({email, token, forceRefresh} = {}) {
518
+ if (!email) {
519
+ return Promise.reject(new Error('`email` is required'));
520
+ }
521
+ if (!token) {
522
+ return Promise.reject(new Error('`token` is required'));
523
+ }
524
+
525
+ return this.updateServices({
526
+ from: 'signin',
527
+ query: {email},
528
+ token,
529
+ forceRefresh,
530
+ });
531
+ },
532
+
533
+ /**
534
+ * Updates credentials config to utilize u2c catalog
535
+ * urls.
536
+ * @returns {void}
537
+ */
538
+ updateCredentialsConfig() {
539
+ const {idbroker, identity} = this.list(true);
540
+
541
+ if (idbroker && identity) {
542
+ const {authorizationString, authorizeUrl} = this.webex.config.credentials;
543
+
544
+ // This must be set outside of the setConfig method used to assign the
545
+ // idbroker and identity url values.
546
+ this.webex.config.credentials.authorizeUrl = authorizationString
547
+ ? authorizeUrl
548
+ : `${idbroker.replace(trailingSlashes, '')}/idb/oauth2/v1/authorize`;
549
+
550
+ this.webex.setConfig({
551
+ credentials: {
552
+ idbroker: {
553
+ url: idbroker.replace(trailingSlashes, ''), // remove trailing slash
554
+ },
555
+ identity: {
556
+ url: identity.replace(trailingSlashes, ''), // remove trailing slash
557
+ },
558
+ },
559
+ });
560
+ }
561
+ },
562
+
563
+ /**
564
+ * Wait until the service catalog is available,
565
+ * or reject afte ra timeout of 60 seconds.
566
+ * @param {string} serviceGroup
567
+ * @param {number} [timeout] - in seconds
568
+ * @returns {Promise<void>}
569
+ */
570
+ waitForCatalog(serviceGroup, timeout) {
571
+ const catalog = this._getCatalog();
572
+ const {supertoken} = this.webex.credentials;
573
+
574
+ if (
575
+ serviceGroup === 'postauth' &&
576
+ supertoken &&
577
+ supertoken.access_token &&
578
+ !catalog.status.postauth.collecting &&
579
+ !catalog.status.postauth.ready
580
+ ) {
581
+ if (!catalog.status.preauth.ready) {
582
+ return this.initServiceCatalogs();
583
+ }
584
+
585
+ return this.updateServices();
586
+ }
587
+
588
+ return catalog.waitForCatalog(serviceGroup, timeout);
589
+ },
590
+
591
+ /**
592
+ * Service waiting parameter transfer object for {@link waitForService}.
593
+ *
594
+ * @typedef {object} WaitForServicePTO
595
+ * @property {string} [WaitForServicePTO.name] - The service name.
596
+ * @property {string} [WaitForServicePTO.url] - The service url.
597
+ * @property {string} [WaitForServicePTO.timeout] - wait duration in seconds.
598
+ */
599
+
600
+ /**
601
+ * Wait until the service has been ammended to any service catalog. This
602
+ * method prioritizes the service name over the service url when searching.
603
+ *
604
+ * @param {WaitForServicePTO} - The parameter transfer object.
605
+ * @returns {Promise<string>} - Resolves to the priority host of a service.
606
+ */
607
+ waitForService({name, timeout = 5, url}) {
608
+ const {services} = this.webex.config;
609
+
610
+ // Save memory by grabbing the catalog after there isn't a priortyURL
611
+ const catalog = this._getCatalog();
612
+
613
+ const fetchFromServiceUrl = services.servicesNotNeedValidation.find(
614
+ (service) => service === name
615
+ );
616
+
617
+ if (fetchFromServiceUrl) {
618
+ return Promise.resolve(this._serviceUrls[name]);
619
+ }
620
+
621
+ const priorityUrl = this.get(name, true);
622
+ const priorityUrlObj = this.getServiceFromUrl(url);
623
+
624
+ if (priorityUrl || priorityUrlObj) {
625
+ return Promise.resolve(priorityUrl || priorityUrlObj.priorityUrl);
626
+ }
627
+
628
+ if (catalog.isReady) {
629
+ if (url) {
630
+ return Promise.resolve(url);
631
+ }
632
+
633
+ this.webex.internal.metrics.submitClientMetrics(METRICS.JS_SDK_SERVICE_NOT_FOUND, {
634
+ fields: {service_name: name},
635
+ });
636
+
637
+ return Promise.reject(
638
+ new Error(`services: service '${name}' was not found in any of the catalogs`)
639
+ );
640
+ }
641
+
642
+ return new Promise((resolve, reject) => {
643
+ const groupsToCheck = ['preauth', 'signin', 'postauth'];
644
+ const checkCatalog = (catalogGroup) =>
645
+ catalog
646
+ .waitForCatalog(catalogGroup, timeout)
647
+ .then(() => {
648
+ const scopedPriorityUrl = this.get(name, true);
649
+ const scopedPrioriryUrlObj = this.getServiceFromUrl(url);
650
+
651
+ if (scopedPriorityUrl || scopedPrioriryUrlObj) {
652
+ resolve(scopedPriorityUrl || scopedPrioriryUrlObj.priorityUrl);
653
+ }
654
+ })
655
+ .catch(() => undefined);
656
+
657
+ Promise.all(groupsToCheck.map((group) => checkCatalog(group))).then(() => {
658
+ this.webex.internal.metrics.submitClientMetrics(METRICS.JS_SDK_SERVICE_NOT_FOUND, {
659
+ fields: {service_name: name},
660
+ });
661
+ reject(new Error(`services: service '${name}' was not found after waiting`));
662
+ });
663
+ });
664
+ },
665
+
666
+ /**
667
+ * @private
668
+ * Organize a received hostmap from a service
669
+ * catalog endpoint.
670
+ * @param {object} serviceHostmap
671
+ * @returns {object}
672
+ */
673
+ _formatReceivedHostmap(serviceHostmap) {
674
+ // map the host catalog items to a formatted hostmap
675
+ const formattedHostmap = Object.keys(serviceHostmap.hostCatalog).reduce((accumulator, key) => {
676
+ if (serviceHostmap.hostCatalog[key].length === 0) {
677
+ return accumulator;
678
+ }
679
+
680
+ const serviceName = serviceHostmap.hostCatalog[key][0].id.split(':')[3];
681
+ const defaultUrl = serviceHostmap.serviceLinks[serviceName];
682
+
683
+ let serviceItem = accumulator.find((item) => item.name === serviceName);
684
+
685
+ if (!serviceItem) {
686
+ serviceItem = {
687
+ name: serviceName,
688
+ defaultUrl,
689
+ defaultHost: Url.parse(defaultUrl).hostname,
690
+ hosts: [],
691
+ };
692
+
693
+ accumulator.push(serviceItem);
694
+ }
695
+
696
+ serviceItem.hosts.push(
697
+ // map the default key as a low priority default for cluster matching
698
+ {
699
+ host: key,
700
+ ttl: -1,
701
+ priority: 10,
702
+ id: serviceHostmap.hostCatalog[key][0].id,
703
+ homeCluster: serviceItem.defaultHost === key,
704
+ },
705
+ // map the rest of the hosts in their proper locations
706
+ ...serviceHostmap.hostCatalog[key].map((host) => ({
707
+ ...host,
708
+ homeCluster: serviceItem.defaultHost === key,
709
+ }))
710
+ );
711
+
712
+ return accumulator;
713
+ }, []);
714
+
715
+ // append service links that do not exist in the host catalog
716
+ Object.keys(serviceHostmap.serviceLinks).forEach((key) => {
717
+ const service = formattedHostmap.find((item) => item.name === key);
718
+
719
+ if (!service) {
720
+ formattedHostmap.push({
721
+ name: key,
722
+ defaultUrl: serviceHostmap.serviceLinks[key],
723
+ defaultHost: Url.parse(serviceHostmap.serviceLinks[key]).hostname,
724
+ hosts: [],
725
+ });
726
+ }
727
+ });
728
+
729
+ // update all the service urls in the host catalog
730
+
731
+ this._updateServiceUrls(serviceHostmap.serviceLinks);
732
+
733
+ return formattedHostmap;
734
+ },
735
+
736
+ /**
737
+ * Get the clusterId associated with a URL string.
738
+ * @param {string} url
739
+ * @returns {string} - Cluster ID of url provided
740
+ */
741
+ getClusterId(url) {
742
+ const catalog = this._getCatalog();
743
+
744
+ return catalog.findClusterId(url);
745
+ },
746
+
747
+ /**
748
+ * Get a service value from a provided clusterId. This method will
749
+ * return an object containing both the name and url of a found service.
750
+ * @param {object} params
751
+ * @param {string} params.clusterId - clusterId of found service
752
+ * @param {boolean} [params.priorityHost] - returns priority host url if true
753
+ * @param {string} [params.serviceGroup] - specify service group
754
+ * @returns {object} service
755
+ * @returns {string} service.name
756
+ * @returns {string} service.url
757
+ */
758
+ getServiceFromClusterId(params) {
759
+ const catalog = this._getCatalog();
760
+
761
+ return catalog.findServiceFromClusterId(params);
762
+ },
763
+
764
+ /**
765
+ * @param {String} cluster the cluster containing the id
766
+ * @param {UUID} [id] the id of the conversation.
767
+ * If empty, just return the base URL.
768
+ * @returns {String} url of the service
769
+ */
770
+ getServiceUrlFromClusterId({cluster = 'us'} = {}) {
771
+ let clusterId = cluster === 'us' ? DEFAULT_CLUSTER_IDENTIFIER : cluster;
772
+
773
+ // Determine if cluster has service name (non-US clusters from hydra do not)
774
+ if (clusterId.split(':').length < 4) {
775
+ // Add Service to cluster identifier
776
+ clusterId = `${cluster}:${CLUSTER_SERVICE}`;
777
+ }
778
+
779
+ const {url} = this.getServiceFromClusterId({clusterId}) || {};
780
+
781
+ if (!url) {
782
+ throw Error(`Could not find service for cluster [${cluster}]`);
783
+ }
784
+
785
+ return url;
786
+ },
787
+
788
+ /**
789
+ * Get a service object from a service url if the service url exists in the
790
+ * catalog.
791
+ *
792
+ * @param {string} url - The url to be validated.
793
+ * @returns {object} - Service object.
794
+ * @returns {object.name} - The name of the service found.
795
+ * @returns {object.priorityUrl} - The priority url of the found service.
796
+ * @returns {object.defaultUrl} - The default url of the found service.
797
+ */
798
+ getServiceFromUrl(url = '') {
799
+ const service = this._getCatalog().findServiceUrlFromUrl(url);
800
+
801
+ if (!service) {
802
+ return undefined;
803
+ }
804
+
805
+ return {
806
+ name: service.name,
807
+ priorityUrl: service.get(true),
808
+ defaultUrl: service.get(),
809
+ };
810
+ },
811
+
812
+ /**
813
+ * Verify that a provided url exists in the service
814
+ * catalog.
815
+ * @param {string} url
816
+ * @returns {boolean} - true if exists, false otherwise
817
+ */
818
+ isServiceUrl(url) {
819
+ const catalog = this._getCatalog();
820
+
821
+ return !!catalog.findServiceUrlFromUrl(url);
822
+ },
823
+
824
+ /**
825
+ * Determine if a provided url is in the catalog's allowed domains.
826
+ *
827
+ * @param {string} url - The url to match allowed domains against.
828
+ * @returns {boolean} - True if the url provided is allowed.
829
+ */
830
+ isAllowedDomainUrl(url) {
831
+ const catalog = this._getCatalog();
832
+
833
+ return !!catalog.findAllowedDomain(url);
834
+ },
835
+
836
+ /**
837
+ * Converts the host portion of the url from default host
838
+ * to a priority host
839
+ *
840
+ * @param {string} url a service url that contains a default host
841
+ * @returns {string} a service url that contains the top priority host.
842
+ * @throws if url isn't a service url
843
+ */
844
+ convertUrlToPriorityHostUrl(url = '') {
845
+ const data = this.getServiceFromUrl(url);
846
+
847
+ if (!data) {
848
+ throw Error(`No service associated with url: [${url}]`);
849
+ }
850
+
851
+ return url.replace(data.defaultUrl, data.priorityUrl);
852
+ },
853
+
854
+ /**
855
+ * @private
856
+ * Simplified method wrapper for sending a request to get
857
+ * an updated service hostmap.
858
+ * @param {object} [param]
859
+ * @param {string} [param.from] - This accepts `limited` or `signin`
860
+ * @param {object} [param.query] - This accepts `email`, `orgId` or `userId` key values
861
+ * @param {string} [param.query.email] - must be a standard-format email
862
+ * @param {string} [param.query.orgId] - must be an organization id
863
+ * @param {string} [param.query.userId] - must be a user id
864
+ * @param {string} [param.token] - used for signin catalog
865
+ * @returns {Promise<object>}
866
+ */
867
+ _fetchNewServiceHostmap({from, query, token, forceRefresh} = {}) {
868
+ const service = 'u2c';
869
+ const resource = from ? `/${from}/catalog` : '/catalog';
870
+ const qs = {...query, format: 'hostmap'};
871
+
872
+ if (forceRefresh) {
873
+ qs.timestamp = new Date().getTime();
874
+ }
875
+
876
+ const requestObject = {
877
+ method: 'GET',
878
+ service,
879
+ resource,
880
+ qs,
881
+ };
882
+
883
+ if (token) {
884
+ requestObject.headers = {authorization: token};
885
+ }
886
+
887
+ return this.request(requestObject).then(({body}) => this._formatReceivedHostmap(body));
888
+ },
889
+
890
+ /**
891
+ * Initialize the discovery services and the whitelisted services.
892
+ *
893
+ * @returns {void}
894
+ */
895
+ initConfig() {
896
+ // Get the catalog and destructure the services config.
897
+ const catalog = this._getCatalog();
898
+ const {services, fedramp} = this.webex.config;
899
+
900
+ // Validate that the services configuration exists.
901
+ if (services) {
902
+ if (fedramp) {
903
+ services.discovery = fedRampServices;
904
+ }
905
+ // Check for discovery services.
906
+ if (services.discovery) {
907
+ // Format the discovery configuration into an injectable array.
908
+ const formattedDiscoveryServices = Object.keys(services.discovery).map((key) => ({
909
+ name: key,
910
+ defaultUrl: services.discovery[key],
911
+ }));
912
+
913
+ // Inject formatted discovery services into services catalog.
914
+ catalog.updateServiceUrls('discovery', formattedDiscoveryServices);
915
+ }
916
+
917
+ if (services.override) {
918
+ // Format the override configuration into an injectable array.
919
+ const formattedOverrideServices = Object.keys(services.override).map((key) => ({
920
+ name: key,
921
+ defaultUrl: services.override[key],
922
+ }));
923
+
924
+ // Inject formatted override services into services catalog.
925
+ catalog.updateServiceUrls('override', formattedOverrideServices);
926
+ }
927
+
928
+ // Check for allowed host domains.
929
+ if (services.allowedDomains) {
930
+ // Store the allowed domains as a property of the catalog.
931
+ catalog.setAllowedDomains(services.allowedDomains);
932
+ }
933
+
934
+ // Set `validateDomains` property to match configuration
935
+ this.validateDomains = services.validateDomains;
936
+ }
937
+ },
938
+
939
+ /**
940
+ * Make the initial requests to collect the root catalogs.
941
+ *
942
+ * @returns {Promise<void, Error>} - Errors if the token is unavailable.
943
+ */
944
+ initServiceCatalogs() {
945
+ this.logger.info('services: initializing initial service catalogs');
946
+
947
+ // Destructure the credentials plugin.
948
+ const {credentials} = this.webex;
949
+
950
+ // Init a promise chain. Must be done as a Promise.resolve() to allow
951
+ // credentials#getOrgId() to properly throw.
952
+ return (
953
+ Promise.resolve()
954
+ // Get the user's OrgId.
955
+ .then(() => credentials.getOrgId())
956
+ // Begin collecting the preauth/limited catalog.
957
+ .then((orgId) => this.collectPreauthCatalog({orgId}))
958
+ .then(() => {
959
+ // Validate if the token is authorized.
960
+ if (credentials.canAuthorize) {
961
+ // Attempt to collect the postauth catalog.
962
+ return this.updateServices().catch(() =>
963
+ this.logger.warn('services: cannot retrieve postauth catalog')
964
+ );
965
+ }
966
+
967
+ // Return a resolved promise for consistent return value.
968
+ return Promise.resolve();
969
+ })
970
+ );
971
+ },
972
+
973
+ /**
974
+ * Initializer
975
+ *
976
+ * @instance
977
+ * @memberof Services
978
+ * @returns {Services}
979
+ */
980
+ initialize() {
981
+ const catalog = new ServiceCatalog();
982
+ const registry = new ServiceRegistry();
983
+ const state = new ServiceState();
984
+
985
+ this._catalogs.set(this.webex, catalog);
986
+ this.registries.set(this.webex, registry);
987
+ this.states.set(this.webex, state);
988
+
989
+ // Listen for configuration changes once.
990
+ this.listenToOnce(this.webex, 'change:config', () => {
991
+ this.initConfig();
992
+ });
993
+
994
+ // wait for webex instance to be ready before attempting
995
+ // to update the service catalogs
996
+ this.listenToOnce(this.webex, 'ready', () => {
997
+ const {supertoken} = this.webex.credentials;
998
+
999
+ // Validate if the supertoken exists.
1000
+ if (supertoken && supertoken.access_token) {
1001
+ this.initServiceCatalogs()
1002
+ .then(() => {
1003
+ catalog.isReady = true;
1004
+ })
1005
+ .catch((error) =>
1006
+ this.logger.error(`services: failed to init initial services, ${error.message}`)
1007
+ );
1008
+ } else {
1009
+ const {email} = this.webex.config;
1010
+
1011
+ this.collectPreauthCatalog(email ? {email} : undefined);
1012
+ }
1013
+ });
1014
+ },
1015
+ });
1016
+ /* eslint-enable no-underscore-dangle */
1017
+
1018
+ export default Services;