@webex/webex-core 3.12.0-task-refactor.1 → 3.12.0-webex-services-ready.2
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 +583 -94
- package/dist/lib/services/services.js.map +1 -1
- package/dist/lib/services-v2/services-v2.js +507 -41
- 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 +433 -6
- package/src/lib/services-v2/services-v2.ts +417 -2
- 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 +58 -9
- package/test/integration/spec/services-v2/services-v2.js +49 -6
- 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 +692 -12
- package/test/unit/spec/services-v2/services-v2.ts +539 -0
- 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,22 @@ const Services = WebexPlugin.extend({
|
|
|
40
50
|
initFailed: ['boolean', false, false],
|
|
41
51
|
},
|
|
42
52
|
|
|
53
|
+
session: {
|
|
54
|
+
/**
|
|
55
|
+
* Becomes `true` once the initial catalog collection has completed
|
|
56
|
+
* (successfully or otherwise) and any in-flight credentials refresh has
|
|
57
|
+
* settled. Blocks `webex.ready` so consumers can rely on `webex.ready`
|
|
58
|
+
* implying "catalogs populated AND credential state stable".
|
|
59
|
+
* @instance
|
|
60
|
+
* @memberof Services
|
|
61
|
+
* @type {boolean}
|
|
62
|
+
*/
|
|
63
|
+
ready: {
|
|
64
|
+
default: false,
|
|
65
|
+
type: 'boolean',
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
|
|
43
69
|
_catalogs: new WeakMap(),
|
|
44
70
|
|
|
45
71
|
_activeServices: {},
|
|
@@ -56,6 +82,46 @@ const Services = WebexPlugin.extend({
|
|
|
56
82
|
return this._catalogs.get(this.webex);
|
|
57
83
|
},
|
|
58
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Safely access localStorage if available; returns the Storage or null.
|
|
87
|
+
* @returns {Storage | null}
|
|
88
|
+
*/
|
|
89
|
+
_getLocalStorageSafe(): Storage | null {
|
|
90
|
+
if (typeof window !== 'undefined' && (window as any).localStorage) {
|
|
91
|
+
return (window as any).localStorage as Storage;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return null;
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Determine the intended preauth selection based on the current context.
|
|
99
|
+
* @param {string} [currentOrgId]
|
|
100
|
+
* @returns {{selectionType: string, selectionValue: string}}
|
|
101
|
+
*/
|
|
102
|
+
getIntendedPreauthSelection(currentOrgId?: string): {
|
|
103
|
+
selectionType: string;
|
|
104
|
+
selectionValue: string;
|
|
105
|
+
} {
|
|
106
|
+
if (this.webex.credentials?.canAuthorize) {
|
|
107
|
+
if (currentOrgId) {
|
|
108
|
+
return {selectionType: 'orgId', selectionValue: currentOrgId};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const emailConfig = this.webex.config && this.webex.config.email;
|
|
113
|
+
|
|
114
|
+
if (typeof emailConfig === 'string' && emailConfig.trim()) {
|
|
115
|
+
return {
|
|
116
|
+
selectionType: 'emailhash',
|
|
117
|
+
selectionValue: sha256(emailConfig.toLowerCase()).toString(),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// fall back to proximity mode when no orgId or email available
|
|
122
|
+
return {selectionType: 'mode', selectionValue: 'DEFAULT_BY_PROXIMITY'};
|
|
123
|
+
},
|
|
124
|
+
|
|
59
125
|
/**
|
|
60
126
|
* Get a service url from the current services list by name
|
|
61
127
|
* from the associated instance catalog.
|
|
@@ -110,6 +176,7 @@ const Services = WebexPlugin.extend({
|
|
|
110
176
|
* @returns {Array<ServiceHost>} - An array of `ServiceHost` objects.
|
|
111
177
|
*/
|
|
112
178
|
getMobiusClusters(): Array<ServiceHost> {
|
|
179
|
+
this.logger.info('services: fetching mobius clusters');
|
|
113
180
|
const clusters: Array<ServiceHost> = [];
|
|
114
181
|
const services: Array<Service> = this._services || [];
|
|
115
182
|
|
|
@@ -153,6 +220,27 @@ const Services = WebexPlugin.extend({
|
|
|
153
220
|
});
|
|
154
221
|
});
|
|
155
222
|
},
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Checks if the current environment is an integration (INT) environment
|
|
226
|
+
* by examining the u2c discovery URL from webex config.
|
|
227
|
+
* INT environments use discovery URLs containing 'intb' (e.g., u2c-intb.ciscospark.com).
|
|
228
|
+
* @returns {boolean} True if INT environment, false otherwise
|
|
229
|
+
*/
|
|
230
|
+
isIntegrationEnvironment(): boolean {
|
|
231
|
+
try {
|
|
232
|
+
const u2cUrl = this.webex?.config?.services?.discovery?.u2c || '';
|
|
233
|
+
const isInt = u2cUrl.includes('intb');
|
|
234
|
+
|
|
235
|
+
this.logger.info(`services: isIntegrationEnvironment: ${isInt}`);
|
|
236
|
+
|
|
237
|
+
return isInt;
|
|
238
|
+
} catch (error) {
|
|
239
|
+
this.logger.error('services: failed to determine integration environment', error);
|
|
240
|
+
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
},
|
|
156
244
|
/**
|
|
157
245
|
* saves all the services from the pre and post catalog service
|
|
158
246
|
* @param {ActiveServices} activeServices
|
|
@@ -249,6 +337,18 @@ const Services = WebexPlugin.extend({
|
|
|
249
337
|
serviceHostMap?.services,
|
|
250
338
|
serviceHostMap?.timestamp
|
|
251
339
|
);
|
|
340
|
+
// Build selection metadata for caching discrimination (preauth/signin)
|
|
341
|
+
let selectionMeta: SelectionMeta | undefined;
|
|
342
|
+
if (serviceGroup === 'preauth' || serviceGroup === 'signin') {
|
|
343
|
+
const key = formattedQuery && Object.keys(formattedQuery || {})[0];
|
|
344
|
+
if (key) {
|
|
345
|
+
selectionMeta = {
|
|
346
|
+
selectionType: key,
|
|
347
|
+
selectionValue: formattedQuery[key],
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
this._cacheCatalog(serviceGroup, serviceHostMap, selectionMeta);
|
|
252
352
|
this.updateCredentialsConfig();
|
|
253
353
|
catalog.status[serviceGroup].collecting = false;
|
|
254
354
|
})
|
|
@@ -934,6 +1034,190 @@ const Services = WebexPlugin.extend({
|
|
|
934
1034
|
return url.replace(data.defaultUrl, data.priorityUrl);
|
|
935
1035
|
},
|
|
936
1036
|
|
|
1037
|
+
/**
|
|
1038
|
+
* @private
|
|
1039
|
+
* Cache the catalog in the bounded storage.
|
|
1040
|
+
* @param {ServiceGroup} serviceGroup - preauth, signin, postauth
|
|
1041
|
+
* @param {ServiceHostmap} hostMap - The hostmap to cache
|
|
1042
|
+
* @param {object} [meta] - Optional selection metadata for cache discrimination
|
|
1043
|
+
* @returns {Promise<void>}
|
|
1044
|
+
*/
|
|
1045
|
+
async _cacheCatalog(
|
|
1046
|
+
serviceGroup: ServiceGroup,
|
|
1047
|
+
hostMap: ServiceHostmap,
|
|
1048
|
+
meta?: SelectionMeta
|
|
1049
|
+
): Promise<void> {
|
|
1050
|
+
let current: {orgId?: string; env?: {fedramp?: boolean; u2cDiscoveryUrl?: string}} = {};
|
|
1051
|
+
let orgId: string | undefined;
|
|
1052
|
+
try {
|
|
1053
|
+
// Respect calling.cacheU2C toggle; if disabled, skip writing cache
|
|
1054
|
+
if (!this.webex.config?.calling?.cacheU2C) {
|
|
1055
|
+
this.logger.info(`services: skipping cache write for ${serviceGroup} as per the config`);
|
|
1056
|
+
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
try {
|
|
1061
|
+
const ls = this._getLocalStorageSafe();
|
|
1062
|
+
const cachedJson = ls ? ls.getItem(CATALOG_CACHE_KEY_V2) : null;
|
|
1063
|
+
current = cachedJson ? JSON.parse(cachedJson) : {};
|
|
1064
|
+
} catch {
|
|
1065
|
+
current = {};
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
try {
|
|
1069
|
+
const {credentials} = this.webex;
|
|
1070
|
+
orgId = credentials.getOrgId();
|
|
1071
|
+
} catch {
|
|
1072
|
+
orgId = current.orgId;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// Capture environment fingerprint to invalidate cache across env changes
|
|
1076
|
+
let {env} = current;
|
|
1077
|
+
const fedramp = !!this.webex?.config?.fedramp;
|
|
1078
|
+
const u2cDiscoveryUrl = this.webex?.config?.services?.discovery?.u2c;
|
|
1079
|
+
env = {fedramp, u2cDiscoveryUrl};
|
|
1080
|
+
|
|
1081
|
+
const updated = {
|
|
1082
|
+
...current,
|
|
1083
|
+
orgId: orgId || current.orgId,
|
|
1084
|
+
env: env || current.env,
|
|
1085
|
+
// When selection meta is provided, store as an object; otherwise keep legacy shape
|
|
1086
|
+
[serviceGroup]: meta ? {hostMap, meta} : hostMap,
|
|
1087
|
+
cachedAt: Date.now(),
|
|
1088
|
+
};
|
|
1089
|
+
|
|
1090
|
+
const ls = this._getLocalStorageSafe();
|
|
1091
|
+
if (ls) {
|
|
1092
|
+
ls.setItem(CATALOG_CACHE_KEY_V2, JSON.stringify(updated));
|
|
1093
|
+
}
|
|
1094
|
+
} catch (e) {
|
|
1095
|
+
this.logger.warn('services: error caching catalog', e);
|
|
1096
|
+
}
|
|
1097
|
+
},
|
|
1098
|
+
|
|
1099
|
+
/**
|
|
1100
|
+
* @private
|
|
1101
|
+
* Load the catalog from cache and hydrate the in-memory ServiceCatalog.
|
|
1102
|
+
* @returns {Promise<boolean>} true if cache was loaded, false otherwise
|
|
1103
|
+
*/
|
|
1104
|
+
async _loadCatalogFromCache(): Promise<boolean> {
|
|
1105
|
+
let currentOrgId: string | undefined;
|
|
1106
|
+
try {
|
|
1107
|
+
// Respect calling.cacheU2C toggle; if disabled, skip using cache
|
|
1108
|
+
if (!this.webex.config?.calling?.cacheU2C) {
|
|
1109
|
+
this.logger.info('services: skipping cache warm-up as per the cache config');
|
|
1110
|
+
|
|
1111
|
+
return false;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
const ls = this._getLocalStorageSafe();
|
|
1115
|
+
if (!ls) {
|
|
1116
|
+
this.logger.info('services: skipping cache warm-up as no localStorage is available');
|
|
1117
|
+
|
|
1118
|
+
return false;
|
|
1119
|
+
}
|
|
1120
|
+
const cachedJson = ls.getItem(CATALOG_CACHE_KEY_V2);
|
|
1121
|
+
const cached = cachedJson ? JSON.parse(cachedJson) : undefined;
|
|
1122
|
+
if (!cached) {
|
|
1123
|
+
return false;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
// TTL enforcement
|
|
1127
|
+
const cachedAt = Number(cached.cachedAt) || 0;
|
|
1128
|
+
if (!cachedAt || Date.now() - cachedAt > CATALOG_TTL_MS) {
|
|
1129
|
+
this.clearCatalogCache();
|
|
1130
|
+
|
|
1131
|
+
return false;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// If authorized, ensure cached org matches
|
|
1135
|
+
try {
|
|
1136
|
+
if (this.webex.credentials?.canAuthorize) {
|
|
1137
|
+
const {credentials} = this.webex;
|
|
1138
|
+
currentOrgId = credentials.getOrgId();
|
|
1139
|
+
if (cached.orgId && cached.orgId !== currentOrgId) {
|
|
1140
|
+
return false;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
} catch (e) {
|
|
1144
|
+
this.logger.warn('services: error checking orgId', e);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
// Ensure cached environment matches current environment
|
|
1148
|
+
|
|
1149
|
+
const fedramp = !!this.webex.config?.fedramp;
|
|
1150
|
+
const u2cDiscoveryUrl = this.webex.config?.services?.discovery?.u2c;
|
|
1151
|
+
const currentEnv = {fedramp, u2cDiscoveryUrl};
|
|
1152
|
+
if (cached.env) {
|
|
1153
|
+
const sameEnv =
|
|
1154
|
+
cached.env.fedramp === currentEnv.fedramp &&
|
|
1155
|
+
cached.env.u2cDiscoveryUrl === currentEnv.u2cDiscoveryUrl;
|
|
1156
|
+
if (!sameEnv) {
|
|
1157
|
+
return false;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
const catalog = this._getCatalog();
|
|
1162
|
+
const groups: Array<ServiceGroup> = ['preauth', 'signin', 'postauth'];
|
|
1163
|
+
|
|
1164
|
+
groups.forEach((serviceGroup) => {
|
|
1165
|
+
const cachedGroup = cached[serviceGroup];
|
|
1166
|
+
if (!cachedGroup) {
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
// Support legacy (hostMap) and new ({hostMap, meta}) shapes
|
|
1171
|
+
const hostMap: ServiceHostmap =
|
|
1172
|
+
cachedGroup && cachedGroup.hostMap ? cachedGroup.hostMap : cachedGroup;
|
|
1173
|
+
const meta: SelectionMeta | undefined = cachedGroup?.meta;
|
|
1174
|
+
|
|
1175
|
+
if (serviceGroup === 'preauth' && meta) {
|
|
1176
|
+
// For proximity-based selection, always fetch fresh to respect IP/region changes
|
|
1177
|
+
if (meta.selectionType === 'mode') {
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
const intended = this.getIntendedPreauthSelection(currentOrgId);
|
|
1182
|
+
const matches =
|
|
1183
|
+
intended &&
|
|
1184
|
+
intended.selectionType === meta.selectionType &&
|
|
1185
|
+
intended.selectionValue === meta.selectionValue;
|
|
1186
|
+
if (!matches) {
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
if (hostMap) {
|
|
1192
|
+
catalog.updateServiceGroups(serviceGroup, hostMap?.services, hostMap?.timestamp);
|
|
1193
|
+
}
|
|
1194
|
+
});
|
|
1195
|
+
|
|
1196
|
+
this.updateCredentialsConfig();
|
|
1197
|
+
|
|
1198
|
+
return true;
|
|
1199
|
+
} catch (e) {
|
|
1200
|
+
return false;
|
|
1201
|
+
}
|
|
1202
|
+
},
|
|
1203
|
+
|
|
1204
|
+
/**
|
|
1205
|
+
* Clear the catalog cache from the bounded storage (v2).
|
|
1206
|
+
* @returns {Promise<void>}
|
|
1207
|
+
*/
|
|
1208
|
+
clearCatalogCache(): Promise<void> {
|
|
1209
|
+
try {
|
|
1210
|
+
const ls = this._getLocalStorageSafe();
|
|
1211
|
+
if (ls) {
|
|
1212
|
+
ls.removeItem(CATALOG_CACHE_KEY_V2);
|
|
1213
|
+
}
|
|
1214
|
+
} catch (e) {
|
|
1215
|
+
this.logger.warn('services: error clearing catalog cache', e);
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
return Promise.resolve();
|
|
1219
|
+
},
|
|
1220
|
+
|
|
937
1221
|
/**
|
|
938
1222
|
* @private
|
|
939
1223
|
* Simplified method wrapper for sending a request to get
|
|
@@ -1075,6 +1359,29 @@ const Services = WebexPlugin.extend({
|
|
|
1075
1359
|
);
|
|
1076
1360
|
},
|
|
1077
1361
|
|
|
1362
|
+
/**
|
|
1363
|
+
* Await any in-flight credentials refresh, then flip `services.ready` so
|
|
1364
|
+
* `webex.ready` can fire. Closes the parallel-refresh window: if a credential
|
|
1365
|
+
* refresh is in flight when initial catalog collection settles, we must not
|
|
1366
|
+
* signal ready until the refresh has resolved - otherwise downstream
|
|
1367
|
+
* consumers may observe `canAuthorize`/token state that is about to change
|
|
1368
|
+
* under them.
|
|
1369
|
+
*
|
|
1370
|
+
* @private
|
|
1371
|
+
* @returns {Promise<void>}
|
|
1372
|
+
*/
|
|
1373
|
+
async _finalizeReady(): Promise<void> {
|
|
1374
|
+
const {credentials} = this.webex;
|
|
1375
|
+
|
|
1376
|
+
if (credentials && credentials.isRefreshing) {
|
|
1377
|
+
await new Promise<void>((resolve) => {
|
|
1378
|
+
credentials.once('change:isRefreshing', resolve);
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
this.ready = true;
|
|
1383
|
+
},
|
|
1384
|
+
|
|
1078
1385
|
/**
|
|
1079
1386
|
* Initializer
|
|
1080
1387
|
*
|
|
@@ -1086,14 +1393,48 @@ const Services = WebexPlugin.extend({
|
|
|
1086
1393
|
const catalog = new ServiceCatalog();
|
|
1087
1394
|
this._catalogs.set(this.webex, catalog);
|
|
1088
1395
|
|
|
1089
|
-
// Listen for configuration changes once.
|
|
1396
|
+
// Listen for configuration changes once. The config is not populated on the
|
|
1397
|
+
// webex instance until the `change:config` event fires, so any decision that
|
|
1398
|
+
// depends on config values (such as the gated-vs-ungated init below) must be
|
|
1399
|
+
// made from within this handler rather than synchronously in `initialize()`.
|
|
1090
1400
|
this.listenToOnce(this.webex, 'change:config', () => {
|
|
1091
1401
|
this.initConfig();
|
|
1402
|
+
|
|
1403
|
+
// Feature flag: when enabled, `webex.ready` is blocked until the initial
|
|
1404
|
+
// catalog collection has settled AND any in-flight credentials refresh has
|
|
1405
|
+
// completed. When disabled (the default), preserves the pre-existing
|
|
1406
|
+
// behavior where `webex.ready` fires as soon as `webex.loaded` does and
|
|
1407
|
+
// the catalog is collected out-of-band.
|
|
1408
|
+
const waitForCatalogInit = this.webex.config?.services?.waitForCatalogInit === true;
|
|
1409
|
+
|
|
1410
|
+
if (waitForCatalogInit) {
|
|
1411
|
+
this._initializeCatalogsGated(catalog);
|
|
1412
|
+
} else {
|
|
1413
|
+
// Not gating - immediately mark ready so we do not block webex.ready.
|
|
1414
|
+
this.ready = true;
|
|
1415
|
+
this._initializeCatalogsUngated(catalog);
|
|
1416
|
+
}
|
|
1092
1417
|
});
|
|
1418
|
+
},
|
|
1093
1419
|
|
|
1420
|
+
/**
|
|
1421
|
+
* Original (pre-verified-ready) initialization path. Runs on `webex.ready`
|
|
1422
|
+
* and collects catalogs opportunistically without blocking anything.
|
|
1423
|
+
*
|
|
1424
|
+
* @private
|
|
1425
|
+
* @param {ServiceCatalog} catalog
|
|
1426
|
+
* @returns {void}
|
|
1427
|
+
*/
|
|
1428
|
+
_initializeCatalogsUngated(catalog: ServiceCatalog): void {
|
|
1094
1429
|
// wait for webex instance to be ready before attempting
|
|
1095
1430
|
// to update the service catalogs
|
|
1096
|
-
this.listenToOnce(this.webex, 'ready', () => {
|
|
1431
|
+
this.listenToOnce(this.webex, 'ready', async () => {
|
|
1432
|
+
const warmed = await this._loadCatalogFromCache();
|
|
1433
|
+
if (warmed) {
|
|
1434
|
+
catalog.isReady = true;
|
|
1435
|
+
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1097
1438
|
const {supertoken} = this.webex.credentials;
|
|
1098
1439
|
// Validate if the supertoken exists.
|
|
1099
1440
|
if (supertoken && supertoken.access_token) {
|
|
@@ -1119,6 +1460,80 @@ const Services = WebexPlugin.extend({
|
|
|
1119
1460
|
}
|
|
1120
1461
|
});
|
|
1121
1462
|
},
|
|
1463
|
+
|
|
1464
|
+
/**
|
|
1465
|
+
* Verified-ready initialization path. Blocks `webex.ready` until the initial
|
|
1466
|
+
* catalog fetch has settled (or timed out) AND any in-flight credentials
|
|
1467
|
+
* refresh has completed. Also handles the fresh-login case where OAuth
|
|
1468
|
+
* completes after `loaded` fires.
|
|
1469
|
+
*
|
|
1470
|
+
* @private
|
|
1471
|
+
* @param {ServiceCatalog} catalog
|
|
1472
|
+
* @returns {void}
|
|
1473
|
+
*/
|
|
1474
|
+
_initializeCatalogsGated(catalog: ServiceCatalog): void {
|
|
1475
|
+
// Wait for storage to be loaded before attempting to update the service
|
|
1476
|
+
// catalogs. We listen for 'loaded' instead of 'ready' because `services.ready`
|
|
1477
|
+
// now blocks `webex.ready` - listening to 'ready' would deadlock.
|
|
1478
|
+
this.listenToOnce(this.webex, 'loaded', async () => {
|
|
1479
|
+
const warmed = await this._loadCatalogFromCache();
|
|
1480
|
+
if (warmed) {
|
|
1481
|
+
catalog.isReady = true;
|
|
1482
|
+
await this._finalizeReady();
|
|
1483
|
+
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1486
|
+
const {supertoken} = this.webex.credentials;
|
|
1487
|
+
|
|
1488
|
+
// Race init against a hard timeout so a hung request never leaves
|
|
1489
|
+
// `services.ready` false forever - that would stall `webex.ready` and
|
|
1490
|
+
// leave the app on a permanent spinner.
|
|
1491
|
+
const timeout = new Promise<never>((_, reject) => {
|
|
1492
|
+
setTimeout(
|
|
1493
|
+
() => reject(new Error(`services: init timed out after ${SERVICES_INIT_TIMEOUT_MS}ms`)),
|
|
1494
|
+
SERVICES_INIT_TIMEOUT_MS
|
|
1495
|
+
);
|
|
1496
|
+
});
|
|
1497
|
+
|
|
1498
|
+
// Validate if the supertoken exists.
|
|
1499
|
+
if (supertoken && supertoken.access_token) {
|
|
1500
|
+
Promise.race([this.initServiceCatalogs(), timeout])
|
|
1501
|
+
.then(() => {
|
|
1502
|
+
catalog.isReady = true;
|
|
1503
|
+
})
|
|
1504
|
+
.catch((error) => {
|
|
1505
|
+
this.initFailed = true;
|
|
1506
|
+
this.logger.error(
|
|
1507
|
+
`services: failed to init initial services when credentials available, ${error?.message}`
|
|
1508
|
+
);
|
|
1509
|
+
})
|
|
1510
|
+
.finally(() => this._finalizeReady());
|
|
1511
|
+
} else {
|
|
1512
|
+
const {email} = this.webex.config;
|
|
1513
|
+
|
|
1514
|
+
Promise.race([this.collectPreauthCatalog(email ? {email} : undefined), timeout])
|
|
1515
|
+
.catch((error) => {
|
|
1516
|
+
this.initFailed = true;
|
|
1517
|
+
this.logger.error(
|
|
1518
|
+
`services: failed to init initial services when no credentials available, ${error?.message}`
|
|
1519
|
+
);
|
|
1520
|
+
})
|
|
1521
|
+
.finally(() => this._finalizeReady());
|
|
1522
|
+
|
|
1523
|
+
// Handle fresh login: 'loaded' fires before OAuth completes, so listen
|
|
1524
|
+
// for `canAuthorize` flipping true and then collect the postauth catalog.
|
|
1525
|
+
this.listenToOnce(this.webex, 'change:canAuthorize', () => {
|
|
1526
|
+
if (this.webex.canAuthorize && !catalog.status.postauth.ready) {
|
|
1527
|
+
this.initServiceCatalogs().catch((error) => {
|
|
1528
|
+
this.logger.error(
|
|
1529
|
+
`services: failed to init service catalogs after auth, ${error?.message}`
|
|
1530
|
+
);
|
|
1531
|
+
});
|
|
1532
|
+
}
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
});
|
|
1536
|
+
},
|
|
1122
1537
|
});
|
|
1123
1538
|
/* eslint-enable no-underscore-dangle */
|
|
1124
1539
|
|
|
@@ -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,10 +404,13 @@ describe('webex-core', () => {
|
|
|
404
404
|
assert.isTrue(catalog.isReady);
|
|
405
405
|
});
|
|
406
406
|
|
|
407
|
-
it('should call services#initServiceCatalogs() on webex ready', () => {
|
|
407
|
+
it('should call services#initServiceCatalogs() on webex ready', async () => {
|
|
408
|
+
services._loadCatalogFromCache = sinon.stub().resolves(false);
|
|
408
409
|
services.initServiceCatalogs = sinon.stub().resolves();
|
|
409
410
|
services.initialize();
|
|
410
411
|
webex.trigger('ready');
|
|
412
|
+
// Wait for the async 'ready' handler to complete
|
|
413
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
411
414
|
assert.called(services.initServiceCatalogs);
|
|
412
415
|
assert.isTrue(catalog.isReady);
|
|
413
416
|
});
|
|
@@ -427,6 +430,39 @@ describe('webex-core', () => {
|
|
|
427
430
|
done();
|
|
428
431
|
}, 2000);
|
|
429
432
|
});
|
|
433
|
+
|
|
434
|
+
it('blocks webex.ready until services.ready flips when waitForCatalogInit is enabled', async () => {
|
|
435
|
+
const gatedWebex = new WebexCore({
|
|
436
|
+
credentials: {supertoken: webexUser.token},
|
|
437
|
+
config: {services: {waitForCatalogInit: true}},
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// Before init settles, webex.ready must be false because services.ready
|
|
441
|
+
// is a dependency and starts false in the gated path.
|
|
442
|
+
assert.isFalse(gatedWebex.internal.services.ready, 'services.ready should start false');
|
|
443
|
+
assert.isFalse(gatedWebex.ready, 'webex.ready should not fire while services.ready is false');
|
|
444
|
+
|
|
445
|
+
// Wait up to 30s for services init to complete and flip ready.
|
|
446
|
+
await new Promise((resolve, reject) => {
|
|
447
|
+
if (gatedWebex.internal.services.ready) {
|
|
448
|
+
resolve();
|
|
449
|
+
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
const timer = setTimeout(
|
|
453
|
+
() => reject(new Error('timed out waiting for services.ready')),
|
|
454
|
+
30_000
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
gatedWebex.internal.services.once('change:ready', () => {
|
|
458
|
+
clearTimeout(timer);
|
|
459
|
+
resolve();
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
assert.isTrue(gatedWebex.internal.services.ready, 'services.ready should flip true after init settles');
|
|
464
|
+
assert.isTrue(gatedWebex.ready, 'webex.ready should fire once services.ready flips');
|
|
465
|
+
});
|
|
430
466
|
});
|
|
431
467
|
|
|
432
468
|
describe('#initServiceCatalogs()', () => {
|
|
@@ -697,7 +733,11 @@ describe('webex-core', () => {
|
|
|
697
733
|
|
|
698
734
|
it('updates query.email to be emailhash-ed using SHA256', (done) => {
|
|
699
735
|
catalog.updateServiceUrls = sinon.stub().returns({}); // returns `this`
|
|
700
|
-
services._fetchNewServiceHostmap = sinon.stub().resolves(
|
|
736
|
+
services._fetchNewServiceHostmap = sinon.stub().resolves({
|
|
737
|
+
serviceLinks: {},
|
|
738
|
+
hostCatalog: {},
|
|
739
|
+
format: 'hostmap',
|
|
740
|
+
});
|
|
701
741
|
|
|
702
742
|
services
|
|
703
743
|
.updateServices({
|
|
@@ -827,9 +867,12 @@ describe('webex-core', () => {
|
|
|
827
867
|
|
|
828
868
|
const getActivationRequest = (requestStub, useUserOnboarding = false) => {
|
|
829
869
|
const expectedService = useUserOnboarding ? 'user-onboarding' : 'license';
|
|
830
|
-
const expectedResource = useUserOnboarding
|
|
870
|
+
const expectedResource = useUserOnboarding
|
|
871
|
+
? 'api/v1/users/activations'
|
|
872
|
+
: 'users/activations';
|
|
831
873
|
const requests = requestStub.args.filter(
|
|
832
|
-
([request]) =>
|
|
874
|
+
([request]) =>
|
|
875
|
+
request.service === expectedService && request.resource === expectedResource
|
|
833
876
|
);
|
|
834
877
|
|
|
835
878
|
assert.strictEqual(requests.length, 1);
|
|
@@ -908,7 +951,7 @@ describe('webex-core', () => {
|
|
|
908
951
|
assert.equal(Object.keys(unauthServices.list(false, 'postauth')).length, 0);
|
|
909
952
|
}));
|
|
910
953
|
|
|
911
|
-
it
|
|
954
|
+
it('validates new user with activationOptions suppressEmail true', () =>
|
|
912
955
|
unauthServices
|
|
913
956
|
.validateUser({
|
|
914
957
|
email: `Collabctg+webex-js-sdk-${uuid.v4()}@gmail.com`,
|
|
@@ -1254,13 +1297,19 @@ describe('webex-core', () => {
|
|
|
1254
1297
|
);
|
|
1255
1298
|
|
|
1256
1299
|
it('resolves to an authed u2c hostmap when no params specified', () => {
|
|
1257
|
-
assert.typeOf(fullRemoteHM, '
|
|
1258
|
-
assert.
|
|
1300
|
+
assert.typeOf(fullRemoteHM, 'object');
|
|
1301
|
+
assert.property(fullRemoteHM, 'serviceLinks');
|
|
1302
|
+
assert.property(fullRemoteHM, 'hostCatalog');
|
|
1303
|
+
assert.equal(fullRemoteHM.format, 'hostmap');
|
|
1304
|
+
assert.isAbove(Object.keys(fullRemoteHM.serviceLinks).length, 0);
|
|
1259
1305
|
});
|
|
1260
1306
|
|
|
1261
1307
|
it('resolves to a limited u2c hostmap when params specified', () => {
|
|
1262
|
-
assert.typeOf(limitedRemoteHM, '
|
|
1263
|
-
assert.
|
|
1308
|
+
assert.typeOf(limitedRemoteHM, 'object');
|
|
1309
|
+
assert.property(limitedRemoteHM, 'serviceLinks');
|
|
1310
|
+
assert.property(limitedRemoteHM, 'hostCatalog');
|
|
1311
|
+
assert.equal(limitedRemoteHM.format, 'hostmap');
|
|
1312
|
+
assert.isAbove(Object.keys(limitedRemoteHM.serviceLinks).length, 0);
|
|
1264
1313
|
});
|
|
1265
1314
|
|
|
1266
1315
|
it('rejects if the params provided are invalid', () =>
|