@webex/webex-core 3.12.0-webex-services-ready.2 → 3.12.0-webex-services-ready.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 (36) hide show
  1. package/README.md +5 -2
  2. package/dist/config.js +15 -0
  3. package/dist/config.js.map +1 -1
  4. package/dist/lib/batcher.js +1 -1
  5. package/dist/lib/credentials/credentials.js +1 -1
  6. package/dist/lib/credentials/token.js +1 -1
  7. package/dist/lib/domains.js +90 -0
  8. package/dist/lib/domains.js.map +1 -0
  9. package/dist/lib/services/service-catalog.js +6 -10
  10. package/dist/lib/services/service-catalog.js.map +1 -1
  11. package/dist/lib/services/services.js +26 -22
  12. package/dist/lib/services/services.js.map +1 -1
  13. package/dist/lib/services-v2/service-catalog.js +6 -12
  14. package/dist/lib/services-v2/service-catalog.js.map +1 -1
  15. package/dist/lib/services-v2/services-v2.js +33 -24
  16. package/dist/lib/services-v2/services-v2.js.map +1 -1
  17. package/dist/plugins/logger.js +1 -1
  18. package/dist/webex-core.js +2 -2
  19. package/package.json +13 -13
  20. package/src/config.js +17 -0
  21. package/src/lib/domains.ts +94 -0
  22. package/src/lib/services/service-catalog.js +6 -10
  23. package/src/lib/services/services.js +37 -31
  24. package/src/lib/services-v2/service-catalog.ts +6 -11
  25. package/src/lib/services-v2/services-v2.ts +41 -31
  26. package/test/fixtures/activation-email.ts +22 -0
  27. package/test/integration/spec/services/service-catalog.js +7 -6
  28. package/test/integration/spec/services/services.js +11 -8
  29. package/test/integration/spec/services-v2/services-v2.js +11 -8
  30. package/test/unit/spec/interceptors/auth.js +56 -0
  31. package/test/unit/spec/services/service-catalog.js +93 -11
  32. package/test/unit/spec/services/services.js +6 -1
  33. package/test/unit/spec/services-v2/service-catalog.ts +93 -11
  34. package/test/unit/spec/services-v2/services-v2.ts +22 -1
  35. package/test/unit/spec/webex-core.js +0 -2
  36. package/test/unit/spec/webex-internal-core.js +0 -2
@@ -14,6 +14,8 @@ import {
14
14
  AuthInterceptor,
15
15
  config,
16
16
  Credentials,
17
+ Services,
18
+ ServicesV2,
17
19
  WebexHttpError,
18
20
  Token,
19
21
  serviceConstants,
@@ -380,6 +382,60 @@ describe('webex-core', () => {
380
382
  });
381
383
  });
382
384
 
385
+ describe('#onRequest() against a real service catalog', () => {
386
+ // The cases above stub `isAllowedDomainUrl`, so they answer the
387
+ // allowed-domain question themselves and cannot show that the catalog
388
+ // matcher is reached by the code that attaches the token. These use a
389
+ // real catalog and real allowed-domain methods.
390
+ [
391
+ {name: 'Services', Constructor: Services},
392
+ {name: 'ServicesV2', Constructor: ServicesV2},
393
+ ].forEach(({name, Constructor}) => {
394
+ describe(name, () => {
395
+ let getUserToken;
396
+
397
+ beforeEach(() => {
398
+ const services = new Constructor(undefined, {parent: webex});
399
+
400
+ services._getCatalog().setAllowedDomains(['webex.com']);
401
+ // the catalog holds no services, so a url that is not covered by
402
+ // an allowed domain has nothing else to authorize it
403
+ services.waitForService = sinon.stub().rejects(new Error('no such service'));
404
+
405
+ webex.internal.services = services;
406
+ getUserToken = sinon.spy(webex.credentials, 'getUserToken');
407
+ });
408
+
409
+ afterEach(() => {
410
+ getUserToken.restore();
411
+ delete webex.internal.services;
412
+ });
413
+
414
+ it('adds the authorization header for a url under an allowed domain', () =>
415
+ interceptor
416
+ .onRequest({uri: 'https://api.webex.com/resource', headers: {}})
417
+ .then((options) => {
418
+ assert.equal(
419
+ options.headers.authorization,
420
+ webex.credentials.supertoken.toString()
421
+ );
422
+ assert.calledOnce(getUserToken);
423
+ }));
424
+
425
+ [
426
+ 'https://notwebex.com/resource',
427
+ 'https://webex.com.unrelated.example/resource',
428
+ ].forEach((uri) => {
429
+ it(`does not add the authorization header for ${uri}`, () =>
430
+ interceptor.onRequest({uri, headers: {}}).then((options) => {
431
+ assert.notProperty(options.headers, 'authorization');
432
+ assert.notCalled(getUserToken);
433
+ }));
434
+ });
435
+ });
436
+ });
437
+ });
438
+
383
439
  describe('#onResponseError()', () => {
384
440
  describe('when the server responds with 401', () => {
385
441
  nodeOnly(it)('refreshes the access token and replays the request', () => {
@@ -101,7 +101,24 @@ describe('webex-core', () => {
101
101
  const domains = [];
102
102
 
103
103
  beforeEach(() => {
104
- domains.push('example-a', 'example-b', 'example-c');
104
+ // Shaped like a real catalog rather than a tidy list: the commercial
105
+ // domains the sdk ships with, a multi-part suffix, sites added at
106
+ // runtime from meeting preferences, an entry that overlaps another,
107
+ // and an unset entry, which callers can add and which must match
108
+ // nothing.
109
+ domains.push(
110
+ 'wbx2.com',
111
+ 'ciscospark.com',
112
+ 'webex.com',
113
+ 'webexapis.com',
114
+ 'broadcloud.com.au',
115
+ 'webexgov.us',
116
+ 'example-a.com',
117
+ 'example-b.com',
118
+ 'example-c.com',
119
+ 'go.example-a.com',
120
+ ''
121
+ );
105
122
 
106
123
  catalog.setAllowedDomains(domains);
107
124
  });
@@ -110,10 +127,60 @@ describe('webex-core', () => {
110
127
  domains.length = 0;
111
128
  });
112
129
 
113
- it('finds an allowed domain that matches a specific url', () => {
114
- const domain = catalog.findAllowedDomain('http://example-a.com/resource/id');
115
-
116
- assert.include(domains, domain);
130
+ [
131
+ // real service hosts resolve to the entry that covers them
132
+ ['https://u2c-a.wbx2.com/u2c/api/v1/limited/catalog', 'wbx2.com'],
133
+ ['https://conv-a.wbx2.com/conversation/api/v1/conversations', 'wbx2.com'],
134
+ ['https://foobar.ciscospark.com/resource/id', 'ciscospark.com'],
135
+ ['https://idbroker.webex.com/idb/token', 'webex.com'],
136
+ ['https://webexapis.com/v1/people/me', 'webexapis.com'],
137
+ // an entry is not required to be a bare registrable domain
138
+ ['https://foo.broadcloud.com.au/resource/id', 'broadcloud.com.au'],
139
+ ['https://a.b.webexgov.us/resource/id', 'webexgov.us'],
140
+ // the domain itself and its subdomains match
141
+ ['https://example-a.com/resource/id', 'example-a.com'],
142
+ ['https://sub.example-b.com/resource/id', 'example-b.com'],
143
+ ['https://deep.sub.example-c.com/resource/id', 'example-c.com'],
144
+ // overlapping entries resolve to the first covering entry in the list
145
+ ['https://go.example-a.com/resource/id', 'example-a.com'],
146
+ // an explicit port must not defeat the match
147
+ ['https://example-a.com:8443/resource/id', 'example-a.com'],
148
+ ['https://u2c-a.wbx2.com:8000/resource/id', 'wbx2.com'],
149
+ // hostname comparison is case insensitive
150
+ ['https://U2C-A.WBX2.COM/resource/id', 'wbx2.com'],
151
+ // a trailing dot is the same name
152
+ ['https://u2c-a.wbx2.com./resource/id', 'wbx2.com'],
153
+ // a sibling registrable domain must not match
154
+ ['https://notwebex.com/resource/id', undefined],
155
+ ['https://mywebexgov.us/resource/id', undefined],
156
+ ['https://webex.company/resource/id', undefined],
157
+ ['https://webex.com-unrelated.example/resource/id', undefined],
158
+ ['https://example-a.community/resource/id', undefined],
159
+ // a partial label must not match, even against a multi-part entry
160
+ ['https://broadcloud.com/resource/id', undefined],
161
+ // the domain matches only as a suffix, on a label boundary
162
+ ['https://webex.com.unrelated.example/resource/id', undefined],
163
+ ['https://unrelated.example/webex.com/resource/id', undefined],
164
+ ['https://unrelated.example/?next=https://webex.com', undefined],
165
+ // userinfo is not the host
166
+ ['https://webex.com@unrelated.example/resource/id', undefined],
167
+ // an encoded or unusual separator is not a label boundary, and the url
168
+ // parsers used by the transports do not agree on where these end the
169
+ // host, so they must not resolve to an allowed domain
170
+ ['https://webex.com%2eunrelated.example/resource/id', undefined],
171
+ ['https://webex.com%2Eunrelated.example/resource/id', undefined],
172
+ ['https://unrelated.example%2ewebex.com/resource/id', undefined],
173
+ ['https://unrelated.example%2Ewebex.com/resource/id', undefined],
174
+ ['https://unrelated.example;.webex.com/resource/id', undefined],
175
+ // an empty allowed domain entry matches nothing
176
+ ['https://unrelated.example/resource/id', undefined],
177
+ // unparseable urls
178
+ ['', undefined],
179
+ ['not a url', undefined],
180
+ ].forEach(([url, expected]) => {
181
+ it(`returns ${expected} for ${url || '<empty>'}`, () => {
182
+ assert.equal(catalog.findAllowedDomain(url), expected);
183
+ });
117
184
  });
118
185
  });
119
186
 
@@ -121,7 +188,7 @@ describe('webex-core', () => {
121
188
  const domains = [];
122
189
 
123
190
  beforeEach(() => {
124
- domains.push('example-a', 'example-b', 'example-c');
191
+ domains.push('example-a.com', 'example-b.com', 'example-c.com');
125
192
 
126
193
  catalog.setAllowedDomains(domains);
127
194
  });
@@ -160,7 +227,7 @@ describe('webex-core', () => {
160
227
  const domains = [];
161
228
 
162
229
  beforeEach(() => {
163
- domains.push('example-a', 'example-b', 'example-c');
230
+ domains.push('example-a.com', 'example-b.com', 'example-c.com');
164
231
 
165
232
  catalog.setAllowedDomains(domains);
166
233
  });
@@ -170,19 +237,31 @@ describe('webex-core', () => {
170
237
  });
171
238
 
172
239
  it('sets the allowed domain entries to new values', () => {
173
- const newValues = ['example-d', 'example-e', 'example-f'];
240
+ const newValues = ['example-d.com', 'example-e.com', 'example-f.com'];
174
241
 
175
242
  catalog.setAllowedDomains(newValues);
176
243
 
177
244
  assert.notDeepInclude(domains, newValues);
178
245
  });
246
+
247
+ it('canonicalizes entries and discards those that are not usable', () => {
248
+ catalog.setAllowedDomains([
249
+ 'Example-D.COM',
250
+ 'example-d.com',
251
+ 'example-e.com.',
252
+ '',
253
+ undefined,
254
+ ]);
255
+
256
+ assert.deepEqual(catalog.getAllowedDomains(), ['example-d.com', 'example-e.com']);
257
+ });
179
258
  });
180
259
 
181
260
  describe('#addAllowedDomains()', () => {
182
261
  const domains = [];
183
262
 
184
263
  beforeEach(() => {
185
- domains.push('example-a', 'example-b', 'example-c');
264
+ domains.push('example-a.com', 'example-b.com', 'example-c.com');
186
265
 
187
266
  catalog.setAllowedDomains(domains);
188
267
  });
@@ -192,13 +271,16 @@ describe('webex-core', () => {
192
271
  });
193
272
 
194
273
  it('merge the allowed domain entries with new values', () => {
195
- const newValues = ['example-c', 'example-e', 'example-f'];
274
+ const newValues = ['example-c.com', 'example-e.com', 'example-f.com'];
196
275
 
197
276
  catalog.addAllowedDomains(newValues);
198
277
 
199
278
  const list = catalog.getAllowedDomains();
200
279
 
201
- assert.match(['example-a', 'example-b', 'example-c', 'example-e', 'example-f'], list);
280
+ assert.match(
281
+ ['example-a.com', 'example-b.com', 'example-c.com', 'example-e.com', 'example-f.com'],
282
+ list
283
+ );
202
284
  });
203
285
  });
204
286
 
@@ -155,7 +155,7 @@ describe('webex-core', () => {
155
155
  beforeEach(() => {
156
156
  services.webex.config = {
157
157
  ...(services.webex.config || {}),
158
- services: {waitForCatalogInit: true},
158
+ services: {waitForCatalogInit: true, catalogInitTimeout: 15000},
159
159
  };
160
160
  // Reset ready flag: the outer beforeEach constructs services which
161
161
  // runs initialize() once with no config (ungated path, flips ready to
@@ -325,6 +325,11 @@ describe('webex-core', () => {
325
325
  services.initServiceCatalogs.called,
326
326
  'expected postauth catalog init after canAuthorize flips'
327
327
  );
328
+
329
+ // `catalog.isReady` is now set inside initServiceCatalogs (after the
330
+ // postauth updateServices) rather than by this listener, so it is not
331
+ // asserted here where initServiceCatalogs is stubbed.
332
+ await waitForAsync();
328
333
  });
329
334
 
330
335
  it('does not re-init postauth catalog when canAuthorize fires but catalog is already ready', async () => {
@@ -101,7 +101,24 @@ describe('webex-core', () => {
101
101
  const domains = [];
102
102
 
103
103
  beforeEach(() => {
104
- domains.push('example-a', 'example-b', 'example-c');
104
+ // Shaped like a real catalog rather than a tidy list: the commercial
105
+ // domains the sdk ships with, a multi-part suffix, sites added at
106
+ // runtime from meeting preferences, an entry that overlaps another,
107
+ // and an unset entry, which callers can add and which must match
108
+ // nothing.
109
+ domains.push(
110
+ 'wbx2.com',
111
+ 'ciscospark.com',
112
+ 'webex.com',
113
+ 'webexapis.com',
114
+ 'broadcloud.com.au',
115
+ 'webexgov.us',
116
+ 'example-a.com',
117
+ 'example-b.com',
118
+ 'example-c.com',
119
+ 'go.example-a.com',
120
+ ''
121
+ );
105
122
 
106
123
  catalog.setAllowedDomains(domains);
107
124
  });
@@ -110,10 +127,60 @@ describe('webex-core', () => {
110
127
  domains.length = 0;
111
128
  });
112
129
 
113
- it('finds an allowed domain that matches a specific url', () => {
114
- const domain = catalog.findAllowedDomain('http://example-a.com/resource/id');
115
-
116
- assert.include(domains, domain);
130
+ [
131
+ // real service hosts resolve to the entry that covers them
132
+ ['https://u2c-a.wbx2.com/u2c/api/v1/limited/catalog', 'wbx2.com'],
133
+ ['https://conv-a.wbx2.com/conversation/api/v1/conversations', 'wbx2.com'],
134
+ ['https://foobar.ciscospark.com/resource/id', 'ciscospark.com'],
135
+ ['https://idbroker.webex.com/idb/token', 'webex.com'],
136
+ ['https://webexapis.com/v1/people/me', 'webexapis.com'],
137
+ // an entry is not required to be a bare registrable domain
138
+ ['https://foo.broadcloud.com.au/resource/id', 'broadcloud.com.au'],
139
+ ['https://a.b.webexgov.us/resource/id', 'webexgov.us'],
140
+ // the domain itself and its subdomains match
141
+ ['https://example-a.com/resource/id', 'example-a.com'],
142
+ ['https://sub.example-b.com/resource/id', 'example-b.com'],
143
+ ['https://deep.sub.example-c.com/resource/id', 'example-c.com'],
144
+ // overlapping entries resolve to the first covering entry in the list
145
+ ['https://go.example-a.com/resource/id', 'example-a.com'],
146
+ // an explicit port must not defeat the match
147
+ ['https://example-a.com:8443/resource/id', 'example-a.com'],
148
+ ['https://u2c-a.wbx2.com:8000/resource/id', 'wbx2.com'],
149
+ // hostname comparison is case insensitive
150
+ ['https://U2C-A.WBX2.COM/resource/id', 'wbx2.com'],
151
+ // a trailing dot is the same name
152
+ ['https://u2c-a.wbx2.com./resource/id', 'wbx2.com'],
153
+ // a sibling registrable domain must not match
154
+ ['https://notwebex.com/resource/id', undefined],
155
+ ['https://mywebexgov.us/resource/id', undefined],
156
+ ['https://webex.company/resource/id', undefined],
157
+ ['https://webex.com-unrelated.example/resource/id', undefined],
158
+ ['https://example-a.community/resource/id', undefined],
159
+ // a partial label must not match, even against a multi-part entry
160
+ ['https://broadcloud.com/resource/id', undefined],
161
+ // the domain matches only as a suffix, on a label boundary
162
+ ['https://webex.com.unrelated.example/resource/id', undefined],
163
+ ['https://unrelated.example/webex.com/resource/id', undefined],
164
+ ['https://unrelated.example/?next=https://webex.com', undefined],
165
+ // userinfo is not the host
166
+ ['https://webex.com@unrelated.example/resource/id', undefined],
167
+ // an encoded or unusual separator is not a label boundary, and the url
168
+ // parsers used by the transports do not agree on where these end the
169
+ // host, so they must not resolve to an allowed domain
170
+ ['https://webex.com%2eunrelated.example/resource/id', undefined],
171
+ ['https://webex.com%2Eunrelated.example/resource/id', undefined],
172
+ ['https://unrelated.example%2ewebex.com/resource/id', undefined],
173
+ ['https://unrelated.example%2Ewebex.com/resource/id', undefined],
174
+ ['https://unrelated.example;.webex.com/resource/id', undefined],
175
+ // an empty allowed domain entry matches nothing
176
+ ['https://unrelated.example/resource/id', undefined],
177
+ // unparseable urls
178
+ ['', undefined],
179
+ ['not a url', undefined],
180
+ ].forEach(([url, expected]) => {
181
+ it(`returns ${expected} for ${url || '<empty>'}`, () => {
182
+ assert.equal(catalog.findAllowedDomain(url), expected);
183
+ });
117
184
  });
118
185
  });
119
186
 
@@ -121,7 +188,7 @@ describe('webex-core', () => {
121
188
  const domains = [];
122
189
 
123
190
  beforeEach(() => {
124
- domains.push('example-a', 'example-b', 'example-c');
191
+ domains.push('example-a.com', 'example-b.com', 'example-c.com');
125
192
 
126
193
  catalog.setAllowedDomains(domains);
127
194
  });
@@ -141,7 +208,7 @@ describe('webex-core', () => {
141
208
  const domains = [];
142
209
 
143
210
  beforeEach(() => {
144
- domains.push('example-a', 'example-b', 'example-c');
211
+ domains.push('example-a.com', 'example-b.com', 'example-c.com');
145
212
 
146
213
  catalog.setAllowedDomains(domains);
147
214
  });
@@ -151,19 +218,31 @@ describe('webex-core', () => {
151
218
  });
152
219
 
153
220
  it('sets the allowed domain entries to new values', () => {
154
- const newValues = ['example-d', 'example-e', 'example-f'];
221
+ const newValues = ['example-d.com', 'example-e.com', 'example-f.com'];
155
222
 
156
223
  catalog.setAllowedDomains(newValues);
157
224
 
158
225
  assert.notDeepInclude(domains, newValues);
159
226
  });
227
+
228
+ it('canonicalizes entries and discards those that are not usable', () => {
229
+ catalog.setAllowedDomains([
230
+ 'Example-D.COM',
231
+ 'example-d.com',
232
+ 'example-e.com.',
233
+ '',
234
+ undefined,
235
+ ]);
236
+
237
+ assert.deepEqual(catalog.getAllowedDomains(), ['example-d.com', 'example-e.com']);
238
+ });
160
239
  });
161
240
 
162
241
  describe('#addAllowedDomains()', () => {
163
242
  const domains = [];
164
243
 
165
244
  beforeEach(() => {
166
- domains.push('example-a', 'example-b', 'example-c');
245
+ domains.push('example-a.com', 'example-b.com', 'example-c.com');
167
246
 
168
247
  catalog.setAllowedDomains(domains);
169
248
  });
@@ -173,13 +252,16 @@ describe('webex-core', () => {
173
252
  });
174
253
 
175
254
  it('merge the allowed domain entries with new values', () => {
176
- const newValues = ['example-c', 'example-e', 'example-f'];
255
+ const newValues = ['example-c.com', 'example-e.com', 'example-f.com'];
177
256
 
178
257
  catalog.addAllowedDomains(newValues);
179
258
 
180
259
  const list = catalog.getAllowedDomains();
181
260
 
182
- assert.match(['example-a', 'example-b', 'example-c', 'example-e', 'example-f'], list);
261
+ assert.match(
262
+ ['example-a.com', 'example-b.com', 'example-c.com', 'example-e.com', 'example-f.com'],
263
+ list
264
+ );
183
265
  });
184
266
  });
185
267
 
@@ -157,7 +157,7 @@ describe('webex-core', () => {
157
157
  beforeEach(() => {
158
158
  services.webex.config = {
159
159
  ...(services.webex.config || {}),
160
- services: {waitForCatalogInit: true},
160
+ services: {waitForCatalogInit: true, catalogInitTimeout: 15000},
161
161
  };
162
162
  // Reset ready flag: the outer beforeEach constructs services which
163
163
  // runs initialize() once with no config (ungated path, flips ready to
@@ -319,6 +319,11 @@ describe('webex-core', () => {
319
319
  services.initServiceCatalogs.called,
320
320
  'expected postauth catalog init after canAuthorize flips'
321
321
  );
322
+
323
+ // `catalog.isReady` is now set inside initServiceCatalogs (after the
324
+ // postauth updateServices) rather than by this listener, so it is not
325
+ // asserted here where initServiceCatalogs is stubbed.
326
+ await waitForAsync();
322
327
  });
323
328
 
324
329
  it('does not re-init postauth catalog when canAuthorize fires but catalog is already ready', async () => {
@@ -670,6 +675,22 @@ describe('webex-core', () => {
670
675
  );
671
676
  });
672
677
 
678
+ it('includes the catalog override query when configured', async () => {
679
+ services.webex.config.services = {useCatalogOverride: true};
680
+ sinon.stub(services, '_formatReceivedHostmap').resolves('map response');
681
+ sinon.stub(services, 'request').resolves({});
682
+
683
+ await services._fetchNewServiceHostmap({from: 'limited'});
684
+
685
+ assert.calledOnceWithExactly(services.request, {
686
+ method: 'GET',
687
+ service: 'u2c',
688
+ resource: '/limited/catalog',
689
+ qs: {format: 'U2CV2', useCatalogOverride: true},
690
+ headers: {},
691
+ });
692
+ });
693
+
673
694
  it('checks service request rejects', async () => {
674
695
  const error = new Error('some error');
675
696
 
@@ -397,8 +397,6 @@ describe('Webex', () => {
397
397
  return new Promise((resolve) => webex.once('loaded', resolve)).then(() => {
398
398
  assert.isFalse(webex.ready);
399
399
  assert.isFalse(webex.test.ready);
400
- // Set services.ready to true since it now blocks webex.ready
401
- webex.internal.services.ready = true;
402
400
  webex.test.ready = true;
403
401
  assert.isTrue(webex.test.ready);
404
402
  assert.isTrue(webex.ready);
@@ -82,8 +82,6 @@ describe('Webex', () => {
82
82
  assert.isFalse(webex.internal.test.ready);
83
83
  assert.isFalse(webex.internal.ready);
84
84
  assert.isFalse(webex.ready);
85
- // Set services.ready to true since it now blocks webex.ready
86
- webex.internal.services.ready = true;
87
85
  webex.internal.test.ready = true;
88
86
  assert.isTrue(webex.internal.ready);
89
87
  assert.isTrue(webex.ready);