From 0611f3b3e2d5e4e742abbf17d6e17c1bd0c95541 Mon Sep 17 00:00:00 2001 From: Matto Date: Mon, 27 Sep 2021 23:41:24 +1000 Subject: [PATCH] Read config from remote config server Signed-off-by: Matto --- .changeset/giant-years-help.md | 8 + packages/backend-common/src/config.ts | 10 +- packages/cli/package.json | 3 +- packages/cli/src/lib/config.ts | 16 +- packages/config-loader/api-report.md | 34 +++- packages/config-loader/package.json | 10 +- packages/config-loader/src/index.ts | 2 +- packages/config-loader/src/loader.test.ts | 125 +++++++++++- packages/config-loader/src/loader.ts | 232 +++++++++++++++++++--- packages/integration/api-report.md | 5 + packages/integration/src/index.ts | 1 + yarn.lock | 69 ++++++- 12 files changed, 461 insertions(+), 54 deletions(-) create mode 100644 .changeset/giant-years-help.md diff --git a/.changeset/giant-years-help.md b/.changeset/giant-years-help.md new file mode 100644 index 0000000000..f0ca12ef19 --- /dev/null +++ b/.changeset/giant-years-help.md @@ -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 diff --git a/packages/backend-common/src/config.ts b/packages/backend-common/src/config.ts index 96907870cb..afdb4fd0f7 100644 --- a/packages/backend-common/src/config.ts +++ b/packages/backend-common/src/config.ts @@ -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 { 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( diff --git a/packages/cli/package.json b/packages/cli/package.json index 0dc4dc3cf7..47475780a6 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -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", diff --git a/packages/cli/src/lib/config.ts b/packages/cli/src/lib/config.ts index a93f040122..218771beb6 100644 --- a/packages/cli/src/lib/config.ts +++ b/packages/cli/src/lib/config.ts @@ -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 diff --git a/packages/config-loader/api-report.md b/packages/config-loader/api-report.md index bb3ae50d23..bb36bbe750 100644 --- a/packages/config-loader/api-report.md +++ b/packages/config-loader/api-report.md @@ -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; // @public (undocumented) export type LoadConfigOptions = { configRoot: string; - configPaths: string[]; + configTargets: ConfigTarget[]; env?: string; experimentalEnvFunc?: EnvFunc; - watch?: { - onChange: (configs: AppConfig[]) => void; - stopSignal?: Promise; - }; + 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 = ( value: T, @@ -73,4 +89,12 @@ export type TransformFunc = ( 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; +}; ``` diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index 2f2878799e..d20c77bedc 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -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" diff --git a/packages/config-loader/src/index.ts b/packages/config-loader/src/index.ts index 0e66c59ec4..e0eafc00bc 100644 --- a/packages/config-loader/src/index.ts +++ b/packages/config-loader/src/index.ts @@ -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'; diff --git a/packages/config-loader/src/loader.test.ts b/packages/config-loader/src/loader.test.ts index 3f51e76936..190904a870 100644 --- a/packages/config-loader/src/loader.test.ts +++ b/packages/config-loader/src/loader.test.ts @@ -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(); + const stopSignal = defer(); + + 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(); await loadConfig({ configRoot: '/root', - configPaths: [], + configTargets: [], watch: { onChange: () => { expect('not').toBe('called'); diff --git a/packages/config-loader/src/loader.ts b/packages/config-loader/src/loader.ts index 315b44c4ee..fb8d0aab33 100644 --- a/packages/config-loader/src/loader.ts +++ b/packages/config-loader/src/loader.ts @@ -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; +}; + +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; - }; + 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 { - 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]; } diff --git a/packages/integration/api-report.md b/packages/integration/api-report.md index 01f84383fe..e07f61eea6 100644 --- a/packages/integration/api-report.md +++ b/packages/integration/api-report.md @@ -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) // diff --git a/packages/integration/src/index.ts b/packages/integration/src/index.ts index 3d5f336e8d..b8992f52d2 100644 --- a/packages/integration/src/index.ts +++ b/packages/integration/src/index.ts @@ -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'; diff --git a/yarn.lock b/yarn.lock index 7657498ff7..87991e1bb5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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==