Read config from remote config server

Signed-off-by: Matto <muhamadto@gmail.com>
This commit is contained in:
Matto
2021-09-27 23:41:24 +10:00
parent 2325563877
commit 0611f3b3e2
12 changed files with 461 additions and 54 deletions
+8
View File
@@ -0,0 +1,8 @@
---
'@backstage/backend-common': patch
'@backstage/cli': patch
'@backstage/config-loader': patch
'@backstage/integration': patch
---
Reading app config from a remote server
+7 -3
View File
@@ -19,7 +19,8 @@ import parseArgs from 'minimist';
import { Logger } from 'winston';
import { findPaths } from '@backstage/cli-common';
import { Config, ConfigReader, JsonValue } from '@backstage/config';
import { loadConfig } from '@backstage/config-loader';
import { ConfigTarget, loadConfig } from '@backstage/config-loader';
import { isValidUrl } from '@backstage/integration';
class ObservableConfigProxy implements Config {
private config: Config = new ConfigReader({});
@@ -117,7 +118,10 @@ export async function loadBackendConfig(options: {
argv: string[];
}): Promise<Config> {
const args = parseArgs(options.argv);
const configPaths: string[] = [args.config ?? []].flat();
const configTargets: ConfigTarget[] = [args.config ?? []]
.flat()
.map(arg => (isValidUrl(arg) ? { url: arg } : { path: resolvePath(arg) }));
const config = new ObservableConfigProxy(options.logger);
@@ -126,7 +130,7 @@ export async function loadBackendConfig(options: {
const configs = await loadConfig({
configRoot: paths.targetRoot,
configPaths: configPaths.map(opt => resolvePath(opt)),
configTargets: configTargets,
watch: {
onChange(newConfigs) {
options.logger.info(
+2 -1
View File
@@ -31,8 +31,10 @@
"@babel/core": "^7.4.4",
"@babel/plugin-transform-modules-commonjs": "^7.4.4",
"@backstage/cli-common": "^0.1.3",
"@backstage/integration": "^0.6.5",
"@backstage/config": "^0.1.10",
"@backstage/config-loader": "^0.6.8",
"@backstage/backend-common": "^0.9.4",
"@hot-loader/react-dom": "^16.13.0",
"@lerna/package-graph": "^4.0.0",
"@lerna/project": "^4.0.0",
@@ -117,7 +119,6 @@
"yn": "^4.0.0"
},
"devDependencies": {
"@backstage/backend-common": "^0.9.4",
"@backstage/config": "^0.1.10",
"@backstage/core-components": "^0.5.0",
"@backstage/core-plugin-api": "^0.1.8",
+13 -3
View File
@@ -14,9 +14,14 @@
* limitations under the License.
*/
import { loadConfig, loadConfigSchema } from '@backstage/config-loader';
import {
ConfigTarget,
loadConfig,
loadConfigSchema,
} from '@backstage/config-loader';
import { ConfigReader } from '@backstage/config';
import { paths } from './paths';
import { isValidUrl } from '@backstage/integration';
type Options = {
args: string[];
@@ -26,7 +31,12 @@ type Options = {
};
export async function loadCliConfig(options: Options) {
const configPaths = options.args.map(arg => paths.resolveTarget(arg));
const configTargets: ConfigTarget[] = [];
options.args.forEach(arg => {
if (!isValidUrl(arg)) {
configTargets.push({ path: paths.resolveTarget(arg) });
}
});
// Consider all packages in the monorepo when loading in config
const { Project } = require('@lerna/project');
@@ -46,7 +56,7 @@ export async function loadCliConfig(options: Options) {
? async name => process.env[name] || 'x'
: undefined,
configRoot: paths.targetRoot,
configPaths,
configTargets: configTargets,
});
// printing to stderr to not clobber stdout in case the cli command
+29 -5
View File
@@ -23,6 +23,17 @@ export type ConfigSchemaProcessingOptions = {
withFilteredKeys?: boolean;
};
// Warning: (ae-missing-release-tag) "ConfigTarget" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type ConfigTarget =
| {
path: string;
}
| {
url: string;
};
// @public
export type ConfigVisibility = 'frontend' | 'backend' | 'secret';
@@ -35,13 +46,11 @@ export function loadConfig(options: LoadConfigOptions): Promise<AppConfig[]>;
// @public (undocumented)
export type LoadConfigOptions = {
configRoot: string;
configPaths: string[];
configTargets: ConfigTarget[];
env?: string;
experimentalEnvFunc?: EnvFunc;
watch?: {
onChange: (configs: AppConfig[]) => void;
stopSignal?: Promise<void>;
};
remote?: Remote;
watch?: Watch;
};
// @public
@@ -66,6 +75,13 @@ export function readEnvConfig(env: {
[name: string]: string | undefined;
}): AppConfig[];
// Warning: (ae-missing-release-tag) "Remote" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type Remote = {
reloadIntervalSeconds: number;
};
// @public
export type TransformFunc<T extends number | string | boolean> = (
value: T,
@@ -73,4 +89,12 @@ export type TransformFunc<T extends number | string | boolean> = (
visibility: ConfigVisibility;
},
) => T | undefined;
// Warning: (ae-missing-release-tag) "Watch" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type Watch = {
onChange: (configs: AppConfig[]) => void;
stopSignal?: Promise<void>;
};
```
+8 -2
View File
@@ -30,8 +30,10 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/integration": "^0.6.5",
"@backstage/cli-common": "^0.1.3",
"@backstage/config": "^0.1.9",
"@backstage/backend-common": "^0.9.4",
"@types/json-schema": "^7.0.6",
"ajv": "^7.0.3",
"chokidar": "^3.5.2",
@@ -39,8 +41,10 @@
"json-schema": "^0.3.0",
"json-schema-merge-allof": "^0.8.1",
"typescript-json-schema": "^0.50.1",
"uuid": "^8.3.2",
"yaml": "^1.9.2",
"yup": "^0.29.3"
"yup": "^0.29.3",
"node-fetch": "2.6.5"
},
"devDependencies": {
"@types/jest": "^26.0.7",
@@ -48,7 +52,9 @@
"@types/mock-fs": "^4.10.0",
"@types/node": "^14.14.32",
"@types/yup": "^0.29.8",
"mock-fs": "^5.1.0"
"mock-fs": "^5.1.0",
"fetch-mock-jest": "1.5.1",
"fetch-mock": "^9.11.0"
},
"files": [
"dist"
+1 -1
View File
@@ -30,4 +30,4 @@ export type {
TransformFunc,
} from './lib';
export { loadConfig } from './loader';
export type { LoadConfigOptions } from './loader';
export type { ConfigTarget, LoadConfigOptions, Watch, Remote } from './loader';
+117 -8
View File
@@ -18,12 +18,33 @@ import { AppConfig } from '@backstage/config';
import { loadConfig } from './loader';
import mockFs from 'mock-fs';
import fs from 'fs-extra';
import { v4 as uuidv4 } from 'uuid';
const fetchMock = require('fetch-mock').sandbox();
const nodeFetch = require('node-fetch');
nodeFetch.default = fetchMock;
describe('loadConfig', () => {
beforeEach(() => {
process.env.MY_SECRET = 'is-secret';
process.env.SUBSTITUTE_ME = 'substituted';
fetchMock.mock(
{
url: 'https://some.domain.io/app-config.yaml',
method: 'GET',
},
{
body: `app:
title: Remote Example App
sessionKey: 'abc123'
escaped: \$\${Escaped}
`,
headers: { ETag: uuidv4().toString() },
},
);
mockFs({
'/root/app-config.yaml': `
app:
@@ -66,6 +87,7 @@ describe('loadConfig', () => {
});
afterEach(() => {
fetchMock.restore();
mockFs.restore();
});
@@ -73,7 +95,7 @@ describe('loadConfig', () => {
await expect(
loadConfig({
configRoot: '/root',
configPaths: [],
configTargets: [],
env: 'production',
}),
).resolves.toEqual([
@@ -90,11 +112,37 @@ describe('loadConfig', () => {
]);
});
it('load config from remote path', async () => {
const configUrl = 'https://some.domain.io/app-config.yaml';
await expect(
loadConfig({
configRoot: '/root',
configTargets: [{ url: configUrl }],
env: 'production',
remote: {
reloadIntervalSeconds: 30,
},
}),
).resolves.toEqual([
{
context: configUrl,
data: {
app: {
title: 'Remote Example App',
sessionKey: 'abc123',
escaped: '${Escaped}',
},
},
},
]);
});
it('loads config with secrets', async () => {
await expect(
loadConfig({
configRoot: '/root',
configPaths: ['/root/app-config.yaml'],
configTargets: [{ path: '/root/app-config.yaml' }],
env: 'production',
}),
).resolves.toEqual([
@@ -115,9 +163,9 @@ describe('loadConfig', () => {
await expect(
loadConfig({
configRoot: '/root',
configPaths: [
'/root/app-config.yaml',
'/root/app-config.development.yaml',
configTargets: [
{ path: '/root/app-config.yaml' },
{ path: '/root/app-config.development.yaml' },
],
env: 'development',
}),
@@ -155,7 +203,7 @@ describe('loadConfig', () => {
await expect(
loadConfig({
configRoot: '/root',
configPaths: ['/root/app-config.substitute.yaml'],
configTargets: [{ path: '/root/app-config.substitute.yaml' }],
env: 'development',
}),
).resolves.toEqual([
@@ -180,7 +228,7 @@ describe('loadConfig', () => {
await expect(
loadConfig({
configRoot: '/root',
configPaths: [],
configTargets: [],
watch: {
onChange: onChange.resolve,
stopSignal: stopSignal.promise,
@@ -218,12 +266,73 @@ describe('loadConfig', () => {
stopSignal.resolve();
});
it('watches remote config urls', async () => {
const onChange = defer<AppConfig[]>();
const stopSignal = defer<void>();
const configUrl = 'https://some.domain.io/app-config.yaml';
await expect(
loadConfig({
configRoot: '/root',
configTargets: [{ url: configUrl }],
watch: {
onChange: onChange.resolve,
stopSignal: stopSignal.promise,
},
remote: {
reloadIntervalSeconds: 1,
},
}),
).resolves.toEqual([
{
context: configUrl,
data: {
app: {
title: 'Remote Example App',
sessionKey: 'abc123',
escaped: '${Escaped}',
},
},
},
]);
fetchMock.mock(
{
url: 'https://some.domain.io/app-config.yaml',
},
{
body: `app:
title: NEW ReMOTe ExaMPLe App
sessionKey: 'abc123'
escaped: \$\${Escaped}
`,
headers: { ETag: uuidv4().toString() },
},
{ overwriteRoutes: true },
);
await expect(onChange.promise).resolves.toEqual([
{
context: configUrl,
data: {
app: {
title: 'NEW ReMOTe ExaMPLe App',
sessionKey: 'abc123',
escaped: '${Escaped}',
},
},
},
]);
stopSignal.resolve();
});
it('stops watching config files', async () => {
const stopSignal = defer<void>();
await loadConfig({
configRoot: '/root',
configPaths: [],
configTargets: [],
watch: {
onChange: () => {
expect('not').toBe('called');
+204 -28
View File
@@ -17,23 +17,67 @@
import fs from 'fs-extra';
import yaml from 'yaml';
import chokidar from 'chokidar';
import { resolve as resolvePath, dirname, isAbsolute, basename } from 'path';
import { basename, dirname, isAbsolute, resolve as resolvePath } from 'path';
import { AppConfig } from '@backstage/config';
import {
applyConfigTransforms,
readEnvConfig,
createIncludeTransform,
createSubstitutionTransform,
EnvFunc,
readEnvConfig,
} from './lib';
import { EnvFunc } from './lib/transform/types';
import fetch from 'node-fetch';
import { isValidUrl } from '@backstage/integration';
export type ConfigTarget = { path: string } | { url: string };
export type Watch = {
/**
* A listener that is called when a config file is changed.
*/
onChange: (configs: AppConfig[]) => void;
/**
* An optional signal that stops the watcher once the promise resolves.
*/
stopSignal?: Promise<void>;
};
export type Remote = {
/**
* An optional remote config reloading period, in seconds
*/
reloadIntervalSeconds: number;
};
export type RemoteConfigProp = {
/**
* URL of the remote config
*/
url: string;
/**
* Contents of the remote config
*/
content: string | null;
/**
* An optional new ETag header value. Used when checking for updated config.
*/
newETag?: string;
/**
* An optional old ETag header value. Used when checking for updated config
*/
oldETag?: string;
};
/** @public */
export type LoadConfigOptions = {
// The root directory of the config loading context. Used to find default configs.
configRoot: string;
// Absolute paths to load config files from. Configs from earlier paths have lower priority.
configPaths: string[];
// Paths to load config files from. Configs from earlier paths have lower priority.
configTargets: ConfigTarget[];
/** @deprecated This option has been removed */
env?: string;
@@ -45,22 +89,19 @@ export type LoadConfigOptions = {
*/
experimentalEnvFunc?: EnvFunc;
/**
* An optional remote config
*/
remote?: Remote;
/**
* An optional configuration that enables watching of config files.
*/
watch?: {
/**
* A listener that is called when a config file is changed.
*/
onChange: (configs: AppConfig[]) => void;
/**
* An optional signal that stops the watcher once the promise resolves.
*/
stopSignal?: Promise<void>;
};
watch?: Watch;
};
const HTTP_RESPONSE_HEADER_ETAG = 'ETag';
/**
* Load configuration data.
*
@@ -69,12 +110,30 @@ export type LoadConfigOptions = {
export async function loadConfig(
options: LoadConfigOptions,
): Promise<AppConfig[]> {
const { configRoot, experimentalEnvFunc: envFunc, watch } = options;
const configPaths = options.configPaths.slice();
const { configRoot, experimentalEnvFunc: envFunc, watch, remote } = options;
const configPaths: string[] = options.configTargets
.slice()
.filter((e): e is { path: string } => e.hasOwnProperty('path'))
.map(configTarget => configTarget.path);
let configUrls: string[] = options.configTargets
.slice()
.filter((e): e is { url: string } => e.hasOwnProperty('url'))
.map(configTarget => configTarget.url);
const remoteConfigProps: RemoteConfigProp[] = [];
if (remote === undefined && configUrls.length > 0) {
console.warn(
`Remote config detected, however, this feature is turned off. Remote config will be ignored.`,
);
configUrls = [];
}
// If no paths are provided, we default to reading
// `app-config.yaml` and, if it exists, `app-config.local.yaml`
if (configPaths.length === 0) {
if (configPaths.length === 0 && configUrls.length === 0) {
configPaths.push(resolvePath(configRoot, 'app-config.yaml'));
const localConfig = resolvePath(configRoot, 'app-config.local.yaml');
@@ -99,6 +158,7 @@ export async function loadConfig(
const input = yaml.parse(await readFile(configPath));
const substitutionTransform = createSubstitutionTransform(env);
const data = await applyConfigTransforms(dir, input, [
createIncludeTransform(env, readFile, substitutionTransform),
substitutionTransform,
@@ -110,7 +170,56 @@ export async function loadConfig(
return configs;
};
let fileConfigs;
const loadRemoteConfigFiles = async () => {
const configs: AppConfig[] = [];
const readConfigFromUrl = async (remoteConfigProp: RemoteConfigProp) => {
const response = await fetch(remoteConfigProp.url);
if (!response.ok) {
throw new Error(
`Could not read config file at ${remoteConfigProp.url}`,
);
}
remoteConfigProp.oldETag = remoteConfigProp.newETag ?? undefined;
remoteConfigProp.newETag =
response.headers.get(HTTP_RESPONSE_HEADER_ETAG) ?? undefined;
remoteConfigProp.content = await response.text();
return remoteConfigProp;
};
for (let i = 0; i < configUrls.length; i++) {
const remoteConfigProp = await readConfigFromUrl({
url: configUrls[i],
content: null,
});
if (!isValidUrl(remoteConfigProp.url)) {
throw new Error(
`Config load path is not valid: '${remoteConfigProp.url}'`,
);
}
const dir = configRoot;
if (!remoteConfigProp.content) {
throw new Error(`Config is not valid`);
}
const input = yaml.parse(remoteConfigProp.content);
const substitutionTransform = createSubstitutionTransform(env);
const data = await applyConfigTransforms(dir, input, [
substitutionTransform,
]);
configs.push({ data, context: remoteConfigProp.url });
remoteConfigProps.push(remoteConfigProp);
}
return configs;
};
let fileConfigs: AppConfig[];
try {
fileConfigs = await loadConfigFiles();
} catch (error) {
@@ -119,15 +228,23 @@ export async function loadConfig(
);
}
let remoteConfigs: AppConfig[] = [];
if (remote) {
try {
remoteConfigs = await loadRemoteConfigFiles();
} catch (error) {
throw new Error(`Failed to read remote configuration file, ${error}`);
}
}
const envConfigs = await readEnvConfig(process.env);
// Set up config file watching if requested by the caller
if (watch) {
let currentSerializedConfig = JSON.stringify(fileConfigs);
const watchConfigFile = (watchProp: Watch) => {
const watcher = chokidar.watch(configPaths, {
usePolling: process.env.NODE_ENV === 'test',
});
let currentSerializedConfig = JSON.stringify(fileConfigs);
watcher.on('change', async () => {
try {
const newConfigs = await loadConfigFiles();
@@ -138,18 +255,77 @@ export async function loadConfig(
}
currentSerializedConfig = newSerializedConfig;
watch.onChange([...newConfigs, ...envConfigs]);
watchProp.onChange([...remoteConfigs, ...newConfigs, ...envConfigs]);
} catch (error) {
console.error(`Failed to reload configuration files, ${error}`);
}
});
if (watch.stopSignal) {
watch.stopSignal.then(() => {
if (watchProp.stopSignal) {
watchProp.stopSignal.then(() => {
watcher.close();
});
}
};
const watchRemoteConfig = (watchProp: Watch, remoteProp: Remote) => {
const hasConfigChanged = async (remoteConfigProp: RemoteConfigProp) => {
const requestProps = { method: 'HEAD' };
const { headers } = await fetch(remoteConfigProp.url, requestProps);
remoteConfigProp.oldETag = remoteConfigProp.newETag ?? undefined;
remoteConfigProp.newETag =
headers.get(HTTP_RESPONSE_HEADER_ETAG) ?? undefined;
return (
remoteConfigProp.oldETag !== undefined &&
remoteConfigProp.newETag !== undefined &&
remoteConfigProp.oldETag !== remoteConfigProp.newETag
);
};
let handle: NodeJS.Timeout | undefined;
try {
handle = setInterval(async () => {
console.info(`Checking for config update`);
for (const remoteConfigProp of remoteConfigProps) {
if (await hasConfigChanged(remoteConfigProp)) {
console.info(`Remote config change, reloading config ...`);
const newRemoteConfigs = await loadRemoteConfigFiles();
watchProp.onChange([
...newRemoteConfigs,
...fileConfigs,
...envConfigs,
]);
console.info(`Remote config reloaded`);
break;
}
}
}, remoteProp.reloadIntervalSeconds * 1000);
} catch (error) {
console.error(`Failed to reload configuration files, ${error}`);
}
if (watchProp.stopSignal) {
watchProp.stopSignal.then(() => {
if (handle !== undefined) {
console.info(`Stopping remote config watch`);
clearInterval(handle);
handle = undefined;
}
});
}
};
// Set up config file watching if requested by the caller
if (watch) {
watchConfigFile(watch);
}
return [...fileConfigs, ...envConfigs];
if (watch && remote) {
watchRemoteConfig(watch, remote);
}
return remote
? [...remoteConfigs, ...fileConfigs, ...envConfigs]
: [...fileConfigs, ...envConfigs];
}
+5
View File
@@ -347,6 +347,11 @@ export type GoogleGcsIntegrationConfig = {
privateKey?: string;
};
// Warning: (ae-missing-release-tag) "isValidUrl" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
export function isValidUrl(url: string): boolean;
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// Warning: (ae-missing-release-tag) "readAwsS3IntegrationConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
+1
View File
@@ -27,6 +27,7 @@ export * from './gitlab';
export * from './googleGcs';
export * from './awsS3';
export { defaultScmResolveUrl } from './helpers';
export { isValidUrl } from './helpers';
export { ScmIntegrations } from './ScmIntegrations';
export type { ScmIntegration, ScmIntegrationsGroup } from './types';
export type { ScmIntegrationRegistry } from './registry';
+66 -3
View File
@@ -11336,6 +11336,11 @@ core-js@^2.5.0:
resolved "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
core-js@^3.0.0:
version "3.18.3"
resolved "https://registry.npmjs.org/core-js/-/core-js-3.18.3.tgz#86a0bba2d8ec3df860fefcc07a8d119779f01509"
integrity sha512-tReEhtMReZaPFVw7dajMx0vlsz3oOb8ajgPoHVYGxr8ErnZ6PcYEvvmjGmXlfpnxpkYSdOQttjB+MvVbCGfvLw==
core-js@^3.0.4, core-js@^3.6.5, core-js@^3.8.2:
version "3.15.0"
resolved "https://registry.npmjs.org/core-js/-/core-js-3.15.0.tgz#db9554ebce0b6fd90dc9b1f2465c841d2d055044"
@@ -14037,6 +14042,29 @@ fetch-blob@2.1.2:
resolved "https://registry.npmjs.org/fetch-blob/-/fetch-blob-2.1.2.tgz#a7805db1361bd44c1ef62bb57fb5fe8ea173ef3c"
integrity sha512-YKqtUDwqLyfyMnmbw8XD6Q8j9i/HggKtPEI+pZ1+8bvheBu78biSmNaXWusx1TauGqtUUGx/cBb1mKdq2rLYow==
fetch-mock-jest@1.5.1:
version "1.5.1"
resolved "https://registry.npmjs.org/fetch-mock-jest/-/fetch-mock-jest-1.5.1.tgz#0e13df990d286d9239e284f12b279ed509bf53cd"
integrity sha512-+utwzP8C+Pax1GSka3nFXILWMY3Er2L+s090FOgqVNrNCPp0fDqgXnAHAJf12PLHi0z4PhcTaZNTz8e7K3fjqQ==
dependencies:
fetch-mock "^9.11.0"
fetch-mock@^9.11.0:
version "9.11.0"
resolved "https://registry.npmjs.org/fetch-mock/-/fetch-mock-9.11.0.tgz#371c6fb7d45584d2ae4a18ee6824e7ad4b637a3f"
integrity sha512-PG1XUv+x7iag5p/iNHD4/jdpxL9FtVSqRMUQhPab4hVDt80T1MH5ehzVrL2IdXO9Q2iBggArFvPqjUbHFuI58Q==
dependencies:
"@babel/core" "^7.0.0"
"@babel/runtime" "^7.0.0"
core-js "^3.0.0"
debug "^4.1.1"
glob-to-regexp "^0.4.0"
is-subset "^0.1.1"
lodash.isequal "^4.5.0"
path-to-regexp "^2.2.1"
querystring "^0.2.0"
whatwg-url "^6.5.0"
fetch-readablestream@^0.2.0:
version "0.2.0"
resolved "https://registry.npmjs.org/fetch-readablestream/-/fetch-readablestream-0.2.0.tgz#eaa6d1a76b12de2d4731a343393c6ccdcfe2c795"
@@ -14867,7 +14895,7 @@ glob-to-regexp@^0.3.0:
resolved "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab"
integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=
glob-to-regexp@^0.4.1:
glob-to-regexp@^0.4.0, glob-to-regexp@^0.4.1:
version "0.4.1"
resolved "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
@@ -16851,6 +16879,11 @@ is-subdir@^1.1.1:
dependencies:
better-path-resolve "1.0.0"
is-subset@^0.1.1:
version "0.1.1"
resolved "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz#8a59117d932de1de00f245fcdd39ce43f1e939a6"
integrity sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY=
is-svg@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/is-svg/-/is-svg-3.0.0.tgz#9321dbd29c212e5ca99c4fa9794c714bcafa2f75"
@@ -18719,7 +18752,7 @@ lodash.isempty@^4.4.0:
resolved "https://registry.npmjs.org/lodash.isempty/-/lodash.isempty-4.4.0.tgz#6f86cbedd8be4ec987be9aaf33c9684db1b31e7e"
integrity sha1-b4bL7di+TsmHvpqvM8loTbGzHn4=
lodash.isequal@^4.0.0:
lodash.isequal@^4.0.0, lodash.isequal@^4.5.0:
version "4.5.0"
resolved "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA=
@@ -20192,6 +20225,13 @@ node-fetch@2.6.1, node-fetch@^2.3.0, node-fetch@^2.6.0, node-fetch@^2.6.1:
resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
node-fetch@2.6.5:
version "2.6.5"
resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.5.tgz#42735537d7f080a7e5f78b6c549b7146be1742fd"
integrity sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ==
dependencies:
whatwg-url "^5.0.0"
node-forge@^0.10.0:
version "0.10.0"
resolved "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3"
@@ -21526,6 +21566,11 @@ path-to-regexp@^1.7.0:
dependencies:
isarray "0.0.1"
path-to-regexp@^2.2.1:
version "2.4.0"
resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-2.4.0.tgz#35ce7f333d5616f1c1e1bfe266c3aba2e5b2e704"
integrity sha512-G6zHoVqC6GGTQkZwF4lkuEyMbVOjoBKAEybQUypI1WTkqinCOrq2x6U2+phkJ1XsEMTy4LjtwPI7HW+NVrRR2w==
path-type@^1.0.0:
version "1.1.0"
resolved "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441"
@@ -26576,6 +26621,11 @@ tr46@^2.0.2:
dependencies:
punycode "^2.1.1"
tr46@~0.0.3:
version "0.0.3"
resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=
"traverse@>=0.3.0 <0.4":
version "0.3.9"
resolved "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9"
@@ -27702,6 +27752,11 @@ web-namespaces@^1.0.0:
resolved "https://registry.npmjs.org/web-namespaces/-/web-namespaces-1.1.4.tgz#bc98a3de60dadd7faefc403d1076d529f5e030ec"
integrity sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw==
webidl-conversions@^3.0.0:
version "3.0.1"
resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=
webidl-conversions@^4.0.2:
version "4.0.2"
resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
@@ -27954,7 +28009,15 @@ whatwg-mimetype@^2.1.0, whatwg-mimetype@^2.2.0, whatwg-mimetype@^2.3.0:
resolved "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
whatwg-url@^6.4.1:
whatwg-url@^5.0.0:
version "5.0.0"
resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0=
dependencies:
tr46 "~0.0.3"
webidl-conversions "^3.0.0"
whatwg-url@^6.4.1, whatwg-url@^6.5.0:
version "6.5.0"
resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8"
integrity sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==