@webex/webex-core 3.12.0-task-refactor.1 → 3.12.0-webex-services-ready.1
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.
- package/dist/config.js +7 -0
- package/dist/config.js.map +1 -1
- package/dist/credentials-config.js +12 -0
- package/dist/credentials-config.js.map +1 -1
- package/dist/interceptors/redirect.js +1 -1
- package/dist/interceptors/redirect.js.map +1 -1
- package/dist/lib/batcher.js +23 -7
- package/dist/lib/batcher.js.map +1 -1
- package/dist/lib/credentials/credentials.js +48 -4
- package/dist/lib/credentials/credentials.js.map +1 -1
- package/dist/lib/credentials/token.js +1 -1
- package/dist/lib/services/service-url.js +11 -1
- package/dist/lib/services/service-url.js.map +1 -1
- package/dist/lib/services/services.js +501 -96
- package/dist/lib/services/services.js.map +1 -1
- package/dist/lib/services-v2/services-v2.js +426 -42
- package/dist/lib/services-v2/services-v2.js.map +1 -1
- package/dist/lib/services-v2/types.js.map +1 -1
- package/dist/plugins/logger.js +1 -1
- package/dist/webex-core.js +2 -2
- package/dist/webex-core.js.map +1 -1
- package/package.json +13 -13
- package/src/config.js +7 -0
- package/src/credentials-config.js +13 -0
- package/src/interceptors/redirect.js +4 -1
- package/src/lib/batcher.js +25 -10
- package/src/lib/credentials/credentials.js +50 -3
- package/src/lib/services/service-url.js +9 -1
- package/src/lib/services/services.js +368 -14
- package/src/lib/services-v2/services-v2.ts +353 -10
- package/src/lib/services-v2/types.ts +5 -0
- package/test/integration/spec/services/service-catalog.js +16 -11
- package/test/integration/spec/services/services.js +38 -11
- package/test/integration/spec/services-v2/services-v2.js +29 -8
- package/test/unit/spec/credentials/credentials.js +133 -2
- package/test/unit/spec/lib/batcher.js +56 -0
- package/test/unit/spec/services/service-url.js +110 -0
- package/test/unit/spec/services/services.js +680 -80
- package/test/unit/spec/services-v2/services-v2.ts +484 -62
- package/test/unit/spec/webex-core.js +2 -0
- package/test/unit/spec/webex-internal-core.js +2 -0
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
ServiceHostmap,
|
|
16
16
|
ServiceGroup,
|
|
17
17
|
ServiceHost,
|
|
18
|
+
SelectionMeta,
|
|
18
19
|
} from './types';
|
|
19
20
|
|
|
20
21
|
const trailingSlashes = /(?:^\/)|(?:\/$)/;
|
|
@@ -28,6 +29,15 @@ const CLUSTER_SERVICE = process.env.WEBEX_CONVERSATION_CLUSTER_SERVICE || DEFAUL
|
|
|
28
29
|
const DEFAULT_CLUSTER_IDENTIFIER =
|
|
29
30
|
process.env.WEBEX_CONVERSATION_DEFAULT_CLUSTER || `${DEFAULT_CLUSTER}:${CLUSTER_SERVICE}`;
|
|
30
31
|
|
|
32
|
+
const CATALOG_CACHE_KEY_V2 = 'services.v2.u2cHostMap';
|
|
33
|
+
const CATALOG_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
34
|
+
|
|
35
|
+
// Maximum time we will wait for the initial catalog collection before letting
|
|
36
|
+
// `services.ready` (and therefore `webex.ready`) fire anyway. A hung request
|
|
37
|
+
// must never leave the app on a permanent spinner - past this point downstream
|
|
38
|
+
// consumers must fall through to their normal error/login paths.
|
|
39
|
+
const SERVICES_INIT_TIMEOUT_MS = 15_000;
|
|
40
|
+
|
|
31
41
|
/* eslint-disable no-underscore-dangle */
|
|
32
42
|
/**
|
|
33
43
|
* @class
|
|
@@ -40,6 +50,20 @@ const Services = WebexPlugin.extend({
|
|
|
40
50
|
initFailed: ['boolean', false, false],
|
|
41
51
|
},
|
|
42
52
|
|
|
53
|
+
session: {
|
|
54
|
+
/**
|
|
55
|
+
* Becomes `true` once services initialization has completed.
|
|
56
|
+
* This blocks `webex.ready` until services are initialized.
|
|
57
|
+
* @instance
|
|
58
|
+
* @memberof Services
|
|
59
|
+
* @type {boolean}
|
|
60
|
+
*/
|
|
61
|
+
ready: {
|
|
62
|
+
default: false,
|
|
63
|
+
type: 'boolean',
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
|
|
43
67
|
_catalogs: new WeakMap(),
|
|
44
68
|
|
|
45
69
|
_activeServices: {},
|
|
@@ -56,6 +80,46 @@ const Services = WebexPlugin.extend({
|
|
|
56
80
|
return this._catalogs.get(this.webex);
|
|
57
81
|
},
|
|
58
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Safely access localStorage if available; returns the Storage or null.
|
|
85
|
+
* @returns {Storage | null}
|
|
86
|
+
*/
|
|
87
|
+
_getLocalStorageSafe(): Storage | null {
|
|
88
|
+
if (typeof window !== 'undefined' && (window as any).localStorage) {
|
|
89
|
+
return (window as any).localStorage as Storage;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return null;
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Determine the intended preauth selection based on the current context.
|
|
97
|
+
* @param {string} [currentOrgId]
|
|
98
|
+
* @returns {{selectionType: string, selectionValue: string}}
|
|
99
|
+
*/
|
|
100
|
+
getIntendedPreauthSelection(currentOrgId?: string): {
|
|
101
|
+
selectionType: string;
|
|
102
|
+
selectionValue: string;
|
|
103
|
+
} {
|
|
104
|
+
if (this.webex.credentials?.canAuthorize) {
|
|
105
|
+
if (currentOrgId) {
|
|
106
|
+
return {selectionType: 'orgId', selectionValue: currentOrgId};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const emailConfig = this.webex.config && this.webex.config.email;
|
|
111
|
+
|
|
112
|
+
if (typeof emailConfig === 'string' && emailConfig.trim()) {
|
|
113
|
+
return {
|
|
114
|
+
selectionType: 'emailhash',
|
|
115
|
+
selectionValue: sha256(emailConfig.toLowerCase()).toString(),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// fall back to proximity mode when no orgId or email available
|
|
120
|
+
return {selectionType: 'mode', selectionValue: 'DEFAULT_BY_PROXIMITY'};
|
|
121
|
+
},
|
|
122
|
+
|
|
59
123
|
/**
|
|
60
124
|
* Get a service url from the current services list by name
|
|
61
125
|
* from the associated instance catalog.
|
|
@@ -110,6 +174,7 @@ const Services = WebexPlugin.extend({
|
|
|
110
174
|
* @returns {Array<ServiceHost>} - An array of `ServiceHost` objects.
|
|
111
175
|
*/
|
|
112
176
|
getMobiusClusters(): Array<ServiceHost> {
|
|
177
|
+
this.logger.info('services: fetching mobius clusters');
|
|
113
178
|
const clusters: Array<ServiceHost> = [];
|
|
114
179
|
const services: Array<Service> = this._services || [];
|
|
115
180
|
|
|
@@ -153,6 +218,27 @@ const Services = WebexPlugin.extend({
|
|
|
153
218
|
});
|
|
154
219
|
});
|
|
155
220
|
},
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Checks if the current environment is an integration (INT) environment
|
|
224
|
+
* by examining the u2c discovery URL from webex config.
|
|
225
|
+
* INT environments use discovery URLs containing 'intb' (e.g., u2c-intb.ciscospark.com).
|
|
226
|
+
* @returns {boolean} True if INT environment, false otherwise
|
|
227
|
+
*/
|
|
228
|
+
isIntegrationEnvironment(): boolean {
|
|
229
|
+
try {
|
|
230
|
+
const u2cUrl = this.webex?.config?.services?.discovery?.u2c || '';
|
|
231
|
+
const isInt = u2cUrl.includes('intb');
|
|
232
|
+
|
|
233
|
+
this.logger.info(`services: isIntegrationEnvironment: ${isInt}`);
|
|
234
|
+
|
|
235
|
+
return isInt;
|
|
236
|
+
} catch (error) {
|
|
237
|
+
this.logger.error('services: failed to determine integration environment', error);
|
|
238
|
+
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
},
|
|
156
242
|
/**
|
|
157
243
|
* saves all the services from the pre and post catalog service
|
|
158
244
|
* @param {ActiveServices} activeServices
|
|
@@ -249,6 +335,18 @@ const Services = WebexPlugin.extend({
|
|
|
249
335
|
serviceHostMap?.services,
|
|
250
336
|
serviceHostMap?.timestamp
|
|
251
337
|
);
|
|
338
|
+
// Build selection metadata for caching discrimination (preauth/signin)
|
|
339
|
+
let selectionMeta: SelectionMeta | undefined;
|
|
340
|
+
if (serviceGroup === 'preauth' || serviceGroup === 'signin') {
|
|
341
|
+
const key = formattedQuery && Object.keys(formattedQuery || {})[0];
|
|
342
|
+
if (key) {
|
|
343
|
+
selectionMeta = {
|
|
344
|
+
selectionType: key,
|
|
345
|
+
selectionValue: formattedQuery[key],
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
this._cacheCatalog(serviceGroup, serviceHostMap, selectionMeta);
|
|
252
350
|
this.updateCredentialsConfig();
|
|
253
351
|
catalog.status[serviceGroup].collecting = false;
|
|
254
352
|
})
|
|
@@ -934,6 +1032,190 @@ const Services = WebexPlugin.extend({
|
|
|
934
1032
|
return url.replace(data.defaultUrl, data.priorityUrl);
|
|
935
1033
|
},
|
|
936
1034
|
|
|
1035
|
+
/**
|
|
1036
|
+
* @private
|
|
1037
|
+
* Cache the catalog in the bounded storage.
|
|
1038
|
+
* @param {ServiceGroup} serviceGroup - preauth, signin, postauth
|
|
1039
|
+
* @param {ServiceHostmap} hostMap - The hostmap to cache
|
|
1040
|
+
* @param {object} [meta] - Optional selection metadata for cache discrimination
|
|
1041
|
+
* @returns {Promise<void>}
|
|
1042
|
+
*/
|
|
1043
|
+
async _cacheCatalog(
|
|
1044
|
+
serviceGroup: ServiceGroup,
|
|
1045
|
+
hostMap: ServiceHostmap,
|
|
1046
|
+
meta?: SelectionMeta
|
|
1047
|
+
): Promise<void> {
|
|
1048
|
+
let current: {orgId?: string; env?: {fedramp?: boolean; u2cDiscoveryUrl?: string}} = {};
|
|
1049
|
+
let orgId: string | undefined;
|
|
1050
|
+
try {
|
|
1051
|
+
// Respect calling.cacheU2C toggle; if disabled, skip writing cache
|
|
1052
|
+
if (!this.webex.config?.calling?.cacheU2C) {
|
|
1053
|
+
this.logger.info(`services: skipping cache write for ${serviceGroup} as per the config`);
|
|
1054
|
+
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
try {
|
|
1059
|
+
const ls = this._getLocalStorageSafe();
|
|
1060
|
+
const cachedJson = ls ? ls.getItem(CATALOG_CACHE_KEY_V2) : null;
|
|
1061
|
+
current = cachedJson ? JSON.parse(cachedJson) : {};
|
|
1062
|
+
} catch {
|
|
1063
|
+
current = {};
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
try {
|
|
1067
|
+
const {credentials} = this.webex;
|
|
1068
|
+
orgId = credentials.getOrgId();
|
|
1069
|
+
} catch {
|
|
1070
|
+
orgId = current.orgId;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// Capture environment fingerprint to invalidate cache across env changes
|
|
1074
|
+
let {env} = current;
|
|
1075
|
+
const fedramp = !!this.webex?.config?.fedramp;
|
|
1076
|
+
const u2cDiscoveryUrl = this.webex?.config?.services?.discovery?.u2c;
|
|
1077
|
+
env = {fedramp, u2cDiscoveryUrl};
|
|
1078
|
+
|
|
1079
|
+
const updated = {
|
|
1080
|
+
...current,
|
|
1081
|
+
orgId: orgId || current.orgId,
|
|
1082
|
+
env: env || current.env,
|
|
1083
|
+
// When selection meta is provided, store as an object; otherwise keep legacy shape
|
|
1084
|
+
[serviceGroup]: meta ? {hostMap, meta} : hostMap,
|
|
1085
|
+
cachedAt: Date.now(),
|
|
1086
|
+
};
|
|
1087
|
+
|
|
1088
|
+
const ls = this._getLocalStorageSafe();
|
|
1089
|
+
if (ls) {
|
|
1090
|
+
ls.setItem(CATALOG_CACHE_KEY_V2, JSON.stringify(updated));
|
|
1091
|
+
}
|
|
1092
|
+
} catch (e) {
|
|
1093
|
+
this.logger.warn('services: error caching catalog', e);
|
|
1094
|
+
}
|
|
1095
|
+
},
|
|
1096
|
+
|
|
1097
|
+
/**
|
|
1098
|
+
* @private
|
|
1099
|
+
* Load the catalog from cache and hydrate the in-memory ServiceCatalog.
|
|
1100
|
+
* @returns {Promise<boolean>} true if cache was loaded, false otherwise
|
|
1101
|
+
*/
|
|
1102
|
+
async _loadCatalogFromCache(): Promise<boolean> {
|
|
1103
|
+
let currentOrgId: string | undefined;
|
|
1104
|
+
try {
|
|
1105
|
+
// Respect calling.cacheU2C toggle; if disabled, skip using cache
|
|
1106
|
+
if (!this.webex.config?.calling?.cacheU2C) {
|
|
1107
|
+
this.logger.info('services: skipping cache warm-up as per the cache config');
|
|
1108
|
+
|
|
1109
|
+
return false;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
const ls = this._getLocalStorageSafe();
|
|
1113
|
+
if (!ls) {
|
|
1114
|
+
this.logger.info('services: skipping cache warm-up as no localStorage is available');
|
|
1115
|
+
|
|
1116
|
+
return false;
|
|
1117
|
+
}
|
|
1118
|
+
const cachedJson = ls.getItem(CATALOG_CACHE_KEY_V2);
|
|
1119
|
+
const cached = cachedJson ? JSON.parse(cachedJson) : undefined;
|
|
1120
|
+
if (!cached) {
|
|
1121
|
+
return false;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// TTL enforcement
|
|
1125
|
+
const cachedAt = Number(cached.cachedAt) || 0;
|
|
1126
|
+
if (!cachedAt || Date.now() - cachedAt > CATALOG_TTL_MS) {
|
|
1127
|
+
this.clearCatalogCache();
|
|
1128
|
+
|
|
1129
|
+
return false;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// If authorized, ensure cached org matches
|
|
1133
|
+
try {
|
|
1134
|
+
if (this.webex.credentials?.canAuthorize) {
|
|
1135
|
+
const {credentials} = this.webex;
|
|
1136
|
+
currentOrgId = credentials.getOrgId();
|
|
1137
|
+
if (cached.orgId && cached.orgId !== currentOrgId) {
|
|
1138
|
+
return false;
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
} catch (e) {
|
|
1142
|
+
this.logger.warn('services: error checking orgId', e);
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// Ensure cached environment matches current environment
|
|
1146
|
+
|
|
1147
|
+
const fedramp = !!this.webex.config?.fedramp;
|
|
1148
|
+
const u2cDiscoveryUrl = this.webex.config?.services?.discovery?.u2c;
|
|
1149
|
+
const currentEnv = {fedramp, u2cDiscoveryUrl};
|
|
1150
|
+
if (cached.env) {
|
|
1151
|
+
const sameEnv =
|
|
1152
|
+
cached.env.fedramp === currentEnv.fedramp &&
|
|
1153
|
+
cached.env.u2cDiscoveryUrl === currentEnv.u2cDiscoveryUrl;
|
|
1154
|
+
if (!sameEnv) {
|
|
1155
|
+
return false;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
const catalog = this._getCatalog();
|
|
1160
|
+
const groups: Array<ServiceGroup> = ['preauth', 'signin', 'postauth'];
|
|
1161
|
+
|
|
1162
|
+
groups.forEach((serviceGroup) => {
|
|
1163
|
+
const cachedGroup = cached[serviceGroup];
|
|
1164
|
+
if (!cachedGroup) {
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// Support legacy (hostMap) and new ({hostMap, meta}) shapes
|
|
1169
|
+
const hostMap: ServiceHostmap =
|
|
1170
|
+
cachedGroup && cachedGroup.hostMap ? cachedGroup.hostMap : cachedGroup;
|
|
1171
|
+
const meta: SelectionMeta | undefined = cachedGroup?.meta;
|
|
1172
|
+
|
|
1173
|
+
if (serviceGroup === 'preauth' && meta) {
|
|
1174
|
+
// For proximity-based selection, always fetch fresh to respect IP/region changes
|
|
1175
|
+
if (meta.selectionType === 'mode') {
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
const intended = this.getIntendedPreauthSelection(currentOrgId);
|
|
1180
|
+
const matches =
|
|
1181
|
+
intended &&
|
|
1182
|
+
intended.selectionType === meta.selectionType &&
|
|
1183
|
+
intended.selectionValue === meta.selectionValue;
|
|
1184
|
+
if (!matches) {
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
if (hostMap) {
|
|
1190
|
+
catalog.updateServiceGroups(serviceGroup, hostMap?.services, hostMap?.timestamp);
|
|
1191
|
+
}
|
|
1192
|
+
});
|
|
1193
|
+
|
|
1194
|
+
this.updateCredentialsConfig();
|
|
1195
|
+
|
|
1196
|
+
return true;
|
|
1197
|
+
} catch (e) {
|
|
1198
|
+
return false;
|
|
1199
|
+
}
|
|
1200
|
+
},
|
|
1201
|
+
|
|
1202
|
+
/**
|
|
1203
|
+
* Clear the catalog cache from the bounded storage (v2).
|
|
1204
|
+
* @returns {Promise<void>}
|
|
1205
|
+
*/
|
|
1206
|
+
clearCatalogCache(): Promise<void> {
|
|
1207
|
+
try {
|
|
1208
|
+
const ls = this._getLocalStorageSafe();
|
|
1209
|
+
if (ls) {
|
|
1210
|
+
ls.removeItem(CATALOG_CACHE_KEY_V2);
|
|
1211
|
+
}
|
|
1212
|
+
} catch (e) {
|
|
1213
|
+
this.logger.warn('services: error clearing catalog cache', e);
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
return Promise.resolve();
|
|
1217
|
+
},
|
|
1218
|
+
|
|
937
1219
|
/**
|
|
938
1220
|
* @private
|
|
939
1221
|
* Simplified method wrapper for sending a request to get
|
|
@@ -1075,6 +1357,30 @@ const Services = WebexPlugin.extend({
|
|
|
1075
1357
|
);
|
|
1076
1358
|
},
|
|
1077
1359
|
|
|
1360
|
+
/**
|
|
1361
|
+
* Await any in-flight credentials refresh, then flip `services.ready` so
|
|
1362
|
+
* `webex.ready` can fire. Closes the parallel-refresh window: if a credential
|
|
1363
|
+
* refresh is in flight when initial catalog collection settles, we must not
|
|
1364
|
+
* signal ready until the refresh has resolved - otherwise downstream
|
|
1365
|
+
* consumers may observe `canAuthorize`/token state that is about to change
|
|
1366
|
+
* under them.
|
|
1367
|
+
*
|
|
1368
|
+
* @private
|
|
1369
|
+
* @returns {Promise<void>}
|
|
1370
|
+
*/
|
|
1371
|
+
async _finalizeReady(): Promise<void> {
|
|
1372
|
+
const {credentials} = this.webex;
|
|
1373
|
+
|
|
1374
|
+
if (credentials && credentials.isRefreshing) {
|
|
1375
|
+
await new Promise<void>((resolve) => {
|
|
1376
|
+
credentials.once('change:isRefreshing', resolve);
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
this.ready = true;
|
|
1381
|
+
this.trigger('services:initialized');
|
|
1382
|
+
},
|
|
1383
|
+
|
|
1078
1384
|
/**
|
|
1079
1385
|
* Initializer
|
|
1080
1386
|
*
|
|
@@ -1091,13 +1397,32 @@ const Services = WebexPlugin.extend({
|
|
|
1091
1397
|
this.initConfig();
|
|
1092
1398
|
});
|
|
1093
1399
|
|
|
1094
|
-
//
|
|
1095
|
-
//
|
|
1096
|
-
|
|
1400
|
+
// Wait for storage to be loaded before attempting to update the service catalogs.
|
|
1401
|
+
// We listen for 'loaded' instead of 'ready' because services.ready is a dependency
|
|
1402
|
+
// of webex.ready - listening to 'ready' would cause a deadlock.
|
|
1403
|
+
this.listenToOnce(this.webex, 'loaded', async () => {
|
|
1404
|
+
const warmed = await this._loadCatalogFromCache();
|
|
1405
|
+
if (warmed) {
|
|
1406
|
+
catalog.isReady = true;
|
|
1407
|
+
await this._finalizeReady();
|
|
1408
|
+
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1097
1411
|
const {supertoken} = this.webex.credentials;
|
|
1412
|
+
|
|
1413
|
+
// Race init against a hard timeout so a hung request never leaves
|
|
1414
|
+
// `services.ready` false forever - that would stall `webex.ready` and
|
|
1415
|
+
// leave the app on a permanent spinner.
|
|
1416
|
+
const timeout = new Promise<never>((_, reject) => {
|
|
1417
|
+
setTimeout(
|
|
1418
|
+
() => reject(new Error(`services: init timed out after ${SERVICES_INIT_TIMEOUT_MS}ms`)),
|
|
1419
|
+
SERVICES_INIT_TIMEOUT_MS
|
|
1420
|
+
);
|
|
1421
|
+
});
|
|
1422
|
+
|
|
1098
1423
|
// Validate if the supertoken exists.
|
|
1099
1424
|
if (supertoken && supertoken.access_token) {
|
|
1100
|
-
this.initServiceCatalogs()
|
|
1425
|
+
Promise.race([this.initServiceCatalogs(), timeout])
|
|
1101
1426
|
.then(() => {
|
|
1102
1427
|
catalog.isReady = true;
|
|
1103
1428
|
})
|
|
@@ -1106,15 +1431,33 @@ const Services = WebexPlugin.extend({
|
|
|
1106
1431
|
this.logger.error(
|
|
1107
1432
|
`services: failed to init initial services when credentials available, ${error?.message}`
|
|
1108
1433
|
);
|
|
1109
|
-
})
|
|
1434
|
+
})
|
|
1435
|
+
.finally(() => this._finalizeReady());
|
|
1110
1436
|
} else {
|
|
1111
1437
|
const {email} = this.webex.config;
|
|
1112
1438
|
|
|
1113
|
-
this.collectPreauthCatalog(email ? {email} : undefined)
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1439
|
+
Promise.race([this.collectPreauthCatalog(email ? {email} : undefined), timeout])
|
|
1440
|
+
.catch((error) => {
|
|
1441
|
+
this.initFailed = true;
|
|
1442
|
+
this.logger.error(
|
|
1443
|
+
`services: failed to init initial services when no credentials available, ${error?.message}`
|
|
1444
|
+
);
|
|
1445
|
+
})
|
|
1446
|
+
.finally(() => this._finalizeReady());
|
|
1447
|
+
// Listen for when credentials become available to fetch the full catalog.
|
|
1448
|
+
// This handles fresh login where 'loaded' fires before OAuth completes.
|
|
1449
|
+
this.listenToOnce(this.webex, 'change:canAuthorize', () => {
|
|
1450
|
+
if (this.webex.canAuthorize && !catalog.status.postauth.ready) {
|
|
1451
|
+
this.initServiceCatalogs()
|
|
1452
|
+
.then(() => {
|
|
1453
|
+
catalog.isReady = true;
|
|
1454
|
+
})
|
|
1455
|
+
.catch((error) => {
|
|
1456
|
+
this.logger.error(
|
|
1457
|
+
`services: failed to init service catalogs after auth, ${error?.message}`
|
|
1458
|
+
);
|
|
1459
|
+
});
|
|
1460
|
+
}
|
|
1118
1461
|
});
|
|
1119
1462
|
}
|
|
1120
1463
|
});
|
|
@@ -2,6 +2,11 @@ type ServiceName = string;
|
|
|
2
2
|
type ClusterId = string;
|
|
3
3
|
export type ServiceGroup = 'discovery' | 'override' | 'preauth' | 'postauth' | 'signin';
|
|
4
4
|
|
|
5
|
+
export type SelectionMeta = {
|
|
6
|
+
selectionType: string;
|
|
7
|
+
selectionValue: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
5
10
|
export type ServiceHost = {
|
|
6
11
|
host: string;
|
|
7
12
|
ttl: number;
|
|
@@ -23,13 +23,12 @@ describe('webex-core', () => {
|
|
|
23
23
|
.then(
|
|
24
24
|
([user]) =>
|
|
25
25
|
new Promise((resolve) => {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}, 1000);
|
|
26
|
+
webexUser = user;
|
|
27
|
+
webex = new WebexCore({credentials: user.token});
|
|
28
|
+
services = webex.internal.services;
|
|
29
|
+
catalog = services._getCatalog();
|
|
30
|
+
// Wait for webex ready event before registering device to ensure newMetrics.callDiagnosticMetrics is initialized
|
|
31
|
+
webex.once('ready', resolve);
|
|
33
32
|
})
|
|
34
33
|
)
|
|
35
34
|
.then(() => webex.internal.device.register())
|
|
@@ -510,13 +509,19 @@ describe('webex-core', () => {
|
|
|
510
509
|
);
|
|
511
510
|
|
|
512
511
|
it('resolves to an authed u2c hostmap when no params specified', () => {
|
|
513
|
-
assert.typeOf(fullRemoteHM, '
|
|
514
|
-
assert.
|
|
512
|
+
assert.typeOf(fullRemoteHM, 'object');
|
|
513
|
+
assert.property(fullRemoteHM, 'serviceLinks');
|
|
514
|
+
assert.property(fullRemoteHM, 'hostCatalog');
|
|
515
|
+
assert.equal(fullRemoteHM.format, 'hostmap');
|
|
516
|
+
assert.isAbove(Object.keys(fullRemoteHM.serviceLinks).length, 0);
|
|
515
517
|
});
|
|
516
518
|
|
|
517
519
|
it('resolves to a limited u2c hostmap when params specified', () => {
|
|
518
|
-
assert.typeOf(limitedRemoteHM, '
|
|
519
|
-
assert.
|
|
520
|
+
assert.typeOf(limitedRemoteHM, 'object');
|
|
521
|
+
assert.property(limitedRemoteHM, 'serviceLinks');
|
|
522
|
+
assert.property(limitedRemoteHM, 'hostCatalog');
|
|
523
|
+
assert.equal(limitedRemoteHM.format, 'hostmap');
|
|
524
|
+
assert.isAbove(Object.keys(limitedRemoteHM.serviceLinks).length, 0);
|
|
520
525
|
});
|
|
521
526
|
|
|
522
527
|
it('rejects if the params provided are invalid', () =>
|
|
@@ -404,12 +404,26 @@ describe('webex-core', () => {
|
|
|
404
404
|
assert.isTrue(catalog.isReady);
|
|
405
405
|
});
|
|
406
406
|
|
|
407
|
-
it('should call services#initServiceCatalogs() on webex
|
|
407
|
+
it('should call services#initServiceCatalogs() on webex loaded', async () => {
|
|
408
408
|
services.initServiceCatalogs = sinon.stub().resolves();
|
|
409
409
|
services.initialize();
|
|
410
|
-
webex.trigger('
|
|
410
|
+
webex.trigger('loaded');
|
|
411
|
+
// Wait for the async callback to execute
|
|
412
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
411
413
|
assert.called(services.initServiceCatalogs);
|
|
412
|
-
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it('should set services.ready to true after initialization completes', async () => {
|
|
417
|
+
// services.ready starts as false
|
|
418
|
+
const newWebex = new WebexCore({credentials: {supertoken: webexUser.token}});
|
|
419
|
+
const newServices = newWebex.internal.services;
|
|
420
|
+
|
|
421
|
+
// Wait for initialization to complete
|
|
422
|
+
await new Promise((resolve) => {
|
|
423
|
+
newServices.on('services:initialized', resolve);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
assert.isTrue(newServices.ready);
|
|
413
427
|
});
|
|
414
428
|
|
|
415
429
|
it('should collect different catalogs based on OrgId region', () =>
|
|
@@ -697,7 +711,11 @@ describe('webex-core', () => {
|
|
|
697
711
|
|
|
698
712
|
it('updates query.email to be emailhash-ed using SHA256', (done) => {
|
|
699
713
|
catalog.updateServiceUrls = sinon.stub().returns({}); // returns `this`
|
|
700
|
-
services._fetchNewServiceHostmap = sinon.stub().resolves(
|
|
714
|
+
services._fetchNewServiceHostmap = sinon.stub().resolves({
|
|
715
|
+
serviceLinks: {},
|
|
716
|
+
hostCatalog: {},
|
|
717
|
+
format: 'hostmap',
|
|
718
|
+
});
|
|
701
719
|
|
|
702
720
|
services
|
|
703
721
|
.updateServices({
|
|
@@ -827,9 +845,12 @@ describe('webex-core', () => {
|
|
|
827
845
|
|
|
828
846
|
const getActivationRequest = (requestStub, useUserOnboarding = false) => {
|
|
829
847
|
const expectedService = useUserOnboarding ? 'user-onboarding' : 'license';
|
|
830
|
-
const expectedResource = useUserOnboarding
|
|
848
|
+
const expectedResource = useUserOnboarding
|
|
849
|
+
? 'api/v1/users/activations'
|
|
850
|
+
: 'users/activations';
|
|
831
851
|
const requests = requestStub.args.filter(
|
|
832
|
-
([request]) =>
|
|
852
|
+
([request]) =>
|
|
853
|
+
request.service === expectedService && request.resource === expectedResource
|
|
833
854
|
);
|
|
834
855
|
|
|
835
856
|
assert.strictEqual(requests.length, 1);
|
|
@@ -908,7 +929,7 @@ describe('webex-core', () => {
|
|
|
908
929
|
assert.equal(Object.keys(unauthServices.list(false, 'postauth')).length, 0);
|
|
909
930
|
}));
|
|
910
931
|
|
|
911
|
-
it
|
|
932
|
+
it('validates new user with activationOptions suppressEmail true', () =>
|
|
912
933
|
unauthServices
|
|
913
934
|
.validateUser({
|
|
914
935
|
email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
|
|
@@ -1254,13 +1275,19 @@ describe('webex-core', () => {
|
|
|
1254
1275
|
);
|
|
1255
1276
|
|
|
1256
1277
|
it('resolves to an authed u2c hostmap when no params specified', () => {
|
|
1257
|
-
assert.typeOf(fullRemoteHM, '
|
|
1258
|
-
assert.
|
|
1278
|
+
assert.typeOf(fullRemoteHM, 'object');
|
|
1279
|
+
assert.property(fullRemoteHM, 'serviceLinks');
|
|
1280
|
+
assert.property(fullRemoteHM, 'hostCatalog');
|
|
1281
|
+
assert.equal(fullRemoteHM.format, 'hostmap');
|
|
1282
|
+
assert.isAbove(Object.keys(fullRemoteHM.serviceLinks).length, 0);
|
|
1259
1283
|
});
|
|
1260
1284
|
|
|
1261
1285
|
it('resolves to a limited u2c hostmap when params specified', () => {
|
|
1262
|
-
assert.typeOf(limitedRemoteHM, '
|
|
1263
|
-
assert.
|
|
1286
|
+
assert.typeOf(limitedRemoteHM, 'object');
|
|
1287
|
+
assert.property(limitedRemoteHM, 'serviceLinks');
|
|
1288
|
+
assert.property(limitedRemoteHM, 'hostCatalog');
|
|
1289
|
+
assert.equal(limitedRemoteHM.format, 'hostmap');
|
|
1290
|
+
assert.isAbove(Object.keys(limitedRemoteHM.serviceLinks).length, 0);
|
|
1264
1291
|
});
|
|
1265
1292
|
|
|
1266
1293
|
it('rejects if the params provided are invalid', () =>
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
formattedServiceHostmapEntryConv,
|
|
24
24
|
formattedServiceHostmapEntryMercury,
|
|
25
25
|
formattedServiceHostmapEntryTest,
|
|
26
|
-
serviceHostmapV2
|
|
26
|
+
serviceHostmapV2,
|
|
27
27
|
} from '../../../fixtures/host-catalog-v2';
|
|
28
28
|
|
|
29
29
|
// /* eslint-disable no-underscore-dangle */
|
|
@@ -316,12 +316,26 @@ describe('webex-core', () => {
|
|
|
316
316
|
assert.isTrue(catalog.isReady);
|
|
317
317
|
});
|
|
318
318
|
|
|
319
|
-
it('should call services#initServiceCatalogs() on webex
|
|
319
|
+
it('should call services#initServiceCatalogs() on webex loaded', async () => {
|
|
320
320
|
services.initServiceCatalogs = sinon.stub().resolves();
|
|
321
321
|
services.initialize();
|
|
322
|
-
webex.trigger('
|
|
322
|
+
webex.trigger('loaded');
|
|
323
|
+
// Wait for the async callback to execute
|
|
324
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
323
325
|
assert.called(services.initServiceCatalogs);
|
|
324
|
-
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('should set services.ready to true after initialization completes', async () => {
|
|
329
|
+
// services.ready starts as false
|
|
330
|
+
const newWebex = new WebexCore({credentials: {supertoken: webexUser.token}});
|
|
331
|
+
const newServices = newWebex.internal.services;
|
|
332
|
+
|
|
333
|
+
// Wait for initialization to complete
|
|
334
|
+
await new Promise((resolve) => {
|
|
335
|
+
newServices.on('services:initialized', resolve);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
assert.isTrue(newServices.ready);
|
|
325
339
|
});
|
|
326
340
|
|
|
327
341
|
it('should collect different catalogs based on OrgId region', () =>
|
|
@@ -413,7 +427,11 @@ describe('webex-core', () => {
|
|
|
413
427
|
.initServiceCatalogs(true)
|
|
414
428
|
// services#updateServices() gets called once by the limited catalog
|
|
415
429
|
// retrieval and should get called again when authorized.
|
|
416
|
-
.then(
|
|
430
|
+
.then(
|
|
431
|
+
() =>
|
|
432
|
+
assert.calledTwice(services.updateServices) &&
|
|
433
|
+
assert.calledWith(services.updateServices, sinon.match({forceRefresh: true}))
|
|
434
|
+
)
|
|
417
435
|
);
|
|
418
436
|
});
|
|
419
437
|
});
|
|
@@ -750,9 +768,12 @@ describe('webex-core', () => {
|
|
|
750
768
|
|
|
751
769
|
const getActivationRequest = (requestStub, useUserOnboarding = false) => {
|
|
752
770
|
const expectedService = useUserOnboarding ? 'user-onboarding' : 'license';
|
|
753
|
-
const expectedResource = useUserOnboarding
|
|
771
|
+
const expectedResource = useUserOnboarding
|
|
772
|
+
? 'api/v1/users/activations'
|
|
773
|
+
: 'users/activations';
|
|
754
774
|
const requests = requestStub.args.filter(
|
|
755
|
-
([request]) =>
|
|
775
|
+
([request]) =>
|
|
776
|
+
request.service === expectedService && request.resource === expectedResource
|
|
756
777
|
);
|
|
757
778
|
|
|
758
779
|
assert.strictEqual(requests.length, 1);
|
|
@@ -825,7 +846,7 @@ describe('webex-core', () => {
|
|
|
825
846
|
assert.equal(r.user.verificationEmailTriggered, true);
|
|
826
847
|
}));
|
|
827
848
|
|
|
828
|
-
it
|
|
849
|
+
it('validates new user with activationOptions suppressEmail true', () =>
|
|
829
850
|
unauthServices
|
|
830
851
|
.validateUser({
|
|
831
852
|
email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
|