From 6b836f77d60ee9521761025d3e6ab6697d7cf016 Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Sat, 14 Sep 2024 15:56:30 +0800 Subject: [PATCH 01/24] resolve conflicts --- src/AzureAppConfigurationImpl.ts | 2 + src/AzureAppConfigurationOptions.ts | 8 + src/ConfigurationClientManager.ts | 302 ++++++++++++++++++++++++++++ src/ConfigurationClientWrapper.ts | 16 ++ src/load.ts | 2 +- src/requestTracing/utils.ts | 8 + 6 files changed, 337 insertions(+), 1 deletion(-) create mode 100644 src/ConfigurationClientManager.ts create mode 100644 src/ConfigurationClientWrapper.ts diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index 977872a8..668bd6af 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -35,6 +35,7 @@ import { AzureKeyVaultKeyValueAdapter } from "./keyvault/AzureKeyVaultKeyValueAd import { RefreshTimer } from "./refresh/RefreshTimer.js"; import { getConfigurationSettingWithTrace, listConfigurationSettingsWithTrace, requestTracingEnabled } from "./requestTracing/utils.js"; import { KeyFilter, LabelFilter, SettingSelector } from "./types.js"; +import { ConfigurationClientManager } from "./ConfigurationClientManager.js"; type PagedSettingSelector = SettingSelector & { /** @@ -56,6 +57,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { */ #sortedTrimKeyPrefixes: string[] | undefined; readonly #requestTracingEnabled: boolean; + #clientManager: ConfigurationClientManager; #client: AppConfigurationClient; #clientEndpoint: string | undefined; #options: AzureAppConfigurationOptions | undefined; diff --git a/src/AzureAppConfigurationOptions.ts b/src/AzureAppConfigurationOptions.ts index f88ad67c..d4e7cdde 100644 --- a/src/AzureAppConfigurationOptions.ts +++ b/src/AzureAppConfigurationOptions.ts @@ -11,6 +11,14 @@ export const MaxRetries = 2; export const MaxRetryDelayInMs = 60000; export interface AzureAppConfigurationOptions { + /** + * Specifies whether enable replica discovery or not. + * + * @remarks + * If not specified, the default value is true. + */ + replicaDiscoveryEnabled?: boolean; + /** * Specify what key-values to include in the configuration provider. * diff --git a/src/ConfigurationClientManager.ts b/src/ConfigurationClientManager.ts new file mode 100644 index 00000000..6c0dd8b3 --- /dev/null +++ b/src/ConfigurationClientManager.ts @@ -0,0 +1,302 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { AppConfigurationClient, AppConfigurationClientOptions } from "@azure/app-configuration"; +import { ConfigurationClientWrapper } from "./ConfigurationClientWrapper" +import { TokenCredential } from "@azure/identity"; +import { AzureAppConfigurationOptions } from "./AzureAppConfigurationOptions"; +import { getClientOptions } from "./load"; + +const TCP_ORIGIN = "_origin._tcp"; +const ALT = "_alt"; +const EndpointSection = "Endpoint"; +const IdSection = "Id"; +const SecretSection = "Secret"; +const AzConfigDomainLabel = ".azconfig." +const AppConfigDomainLabel = ".appconfig." +const FallbackClientRefreshExpireInterval = 60 * 60 * 1000; // 1 hour in milliseconds +const MinimalClientRefreshInterval = 30 * 1000; // 30 seconds in milliseconds +const MaxBackoffDuration = 10 * 60 * 1000; // 10 minutes in milliseconds +const MinBackoffDuration = 30 * 1000; // 30 seconds in milliseconds +const dns = require('dns').promises; + +interface IConfigurationClientManager { + getClients(): ConfigurationClientWrapper[]; + refreshClients(): Promise; +} + +export class ConfigurationClientManager { + #isFailoverable: boolean; + #endpoint: string; + #secret : string; + #id : string; + #credential: TokenCredential; + #clientOptions: AppConfigurationClientOptions | undefined; + #validDomain: string; + #staticClients: ConfigurationClientWrapper[]; + #dynamicClients: ConfigurationClientWrapper[]; + #lastFallbackClientRefreshTime: number; + #lastFallbackClientRefreshAttempt: number; + + + constructor ( + connectionStringOrEndpoint?: string | URL, + credentialOrOptions?: TokenCredential | AzureAppConfigurationOptions, + appConfigOptions?: AzureAppConfigurationOptions + ) { + let staticClient: AppConfigurationClient; + let options: AzureAppConfigurationOptions; + + if (typeof connectionStringOrEndpoint === "string") { + const connectionString = connectionStringOrEndpoint; + options = credentialOrOptions as AzureAppConfigurationOptions; + this.#clientOptions = getClientOptions(options); + staticClient = new AppConfigurationClient(connectionString, this.#clientOptions); + this.#secret = parseConnectionString(connectionString, SecretSection); + this.#id = parseConnectionString(connectionString, IdSection); + // TODO: need to check if it's CDN or not + this.#endpoint = parseConnectionString(connectionString, EndpointSection); + + } else if (connectionStringOrEndpoint instanceof URL) { + const credential = credentialOrOptions as TokenCredential; + options = appConfigOptions as AzureAppConfigurationOptions; + this.#clientOptions = getClientOptions(options); + staticClient = new AppConfigurationClient(connectionStringOrEndpoint.toString(), credential, this.#clientOptions); + this.#endpoint = connectionStringOrEndpoint.toString(); + this.#credential = credential; + } else { + throw new Error("Invalid endpoint URL."); + } + + this.#staticClients = [new ConfigurationClientWrapper(this.#endpoint, staticClient)]; + this.#validDomain = getValidDomain(this.#endpoint); + + } + + async getClients() { + if (!this.#isFailoverable) { + return this.#staticClients; + } + + const currentTime = Date.now(); + if (this.#isFallbackClientDiscoveryDue(currentTime)) { + this.#lastFallbackClientRefreshAttempt = currentTime; + await this.#discoverFallbackClients(this.#endpoint); + } + + // Filter static clients where BackoffEndTime is less than or equal to now + let availableClients = this.#staticClients.filter(client => client.backoffEndTime <= currentTime); + // If there are dynamic clients, filter and concatenate them + if (this.#dynamicClients && this.#dynamicClients.length > 0) { + availableClients = availableClients.concat( + this.#dynamicClients + .filter(client => client.backoffEndTime <= currentTime)); + } + + return availableClients + } + + async refreshClients() { + const currentTime = Date.now(); + if (this.#isFailoverable && + currentTime > new Date(this.#lastFallbackClientRefreshAttempt + MinimalClientRefreshInterval).getTime()) { + this.#lastFallbackClientRefreshAttempt = currentTime; + const url = new URL(this.#endpoint); + await this.#discoverFallbackClients(url.hostname); + } + } + + async #discoverFallbackClients(host) { + const timeout = setTimeout(() => { + }, 10000); // 10 seconds + const srvResults = querySrvTargetHost(host); + + try { + const result = await Promise.race([srvResults, timeout]); + + if (result === timeout) { + throw new Error("SRV record query timed out."); + } + + const srvTargetHosts = result as string[]; + // Shuffle the list of SRV target hosts + for (let i = srvTargetHosts.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [srvTargetHosts[i], srvTargetHosts[j]] = [srvTargetHosts[j], srvTargetHosts[i]]; + } + + const newDynamicClients: ConfigurationClientWrapper[] = []; + for (const host of srvTargetHosts) { + if (isValidEndpoint(host, this.#validDomain)) { + const targetEndpoint = `https://${host}`; + if (targetEndpoint.toLowerCase() === this.#endpoint.toLowerCase()) { + continue; + } + const client = this.#newConfigurationClient(targetEndpoint); + newDynamicClients.push(new ConfigurationClientWrapper(targetEndpoint, client)); + } + } + + this.#dynamicClients = newDynamicClients; + this.#lastFallbackClientRefreshTime = Date.now(); + } catch (err) { + console.warn(`Fail to build fallback clients, ${err.message}`); + } + } + + #newConfigurationClient(endpoint) { + if (this.#credential) { + return new AppConfigurationClient(endpoint, this.#credential, this.#clientOptions); + } + + const connectionStr = buildConnectionString(endpoint, this.#secret, this.#id); + return new AppConfigurationClient(connectionStr, this.#clientOptions); + } + + #isFallbackClientDiscoveryDue(dateTime) { + return dateTime >= this.#lastFallbackClientRefreshAttempt + MinimalClientRefreshInterval + && (!this.#dynamicClients + || this.#dynamicClients.every(client => dateTime < client.backoffEndTime) + || dateTime >= this.#lastFallbackClientRefreshTime + FallbackClientRefreshExpireInterval); + } +} + +/** + * Query SRV records and return target hosts. + * @param {string} host - The host to query. + * @returns {Promise} - A promise that resolves to an array of target hosts. + */ +async function querySrvTargetHost(host) { + const results: string[] = []; + + try { + // Look up SRV records for the origin host + const originRecords = await dns.resolveSrv(`${TCP_ORIGIN}.${host}`); + if (originRecords.length === 0) { + return results; + } + + // Add the first origin record to results + const originHost = originRecords[0].name + results.push(originHost); + + // Look up SRV records for alternate hosts + let index = 0; + while (true) { + const currentAlt = `${ALT}${index}`; + try { + const altRecords = await dns.resolveSrv(`_${currentAlt}._tcp.${originHost}`); + if (altRecords.length === 0) { + break; // No more alternate records, exit loop + } + + altRecords.forEach(record => { + const altHost = record.name; + if (altHost) { + results.push(altHost); + } + }); + index++; + } catch (err) { + if (err.code === 'ENOTFOUND') { + break; // No more alternate records, exit loop + } else { + throw new Error(`Failed to lookup alternate SRV records: ${err.message}`); + } + } + } + } catch (err) { + throw new Error(`Failed to lookup origin SRV records: ${err.message}`); + } + + return results; +} + +/** + * Parses the connection string to extract the value associated with a specific token. + * + * @param {string} connectionString - The connection string containing tokens. + * @param {string} token - The token whose value needs to be extracted. + * @returns {string} The value associated with the token, or an empty string if not found. + * @throws {Error} If the connection string is empty or the token is not found. + */ +function parseConnectionString(connectionString, token) { + if (!connectionString) { + throw new Error("connectionString is empty"); + } + + // Token format is "token=" + const searchToken = `${token}=`; + const startIndex = connectionString.indexOf(searchToken); + if (startIndex === -1) { + throw new Error(`Token ${token} not found in connectionString`); + } + + // Move startIndex to the beginning of the token value + const valueStartIndex = startIndex + searchToken.length; + const endIndex = connectionString.indexOf(';', valueStartIndex); + const valueEndIndex = endIndex === -1 ? connectionString.length : endIndex; + + // Extract and return the token value + return connectionString.substring(valueStartIndex, valueEndIndex); +} + +/** + * Builds a connection string from the given endpoint, secret, and id. + * Returns an empty string if either secret or id is empty. + * + * @param {string} endpoint - The endpoint to include in the connection string. + * @param {string} secret - The secret to include in the connection string. + * @param {string} id - The ID to include in the connection string. + * @returns {string} - The formatted connection string or an empty string if invalid input. + */ +function buildConnectionString(endpoint, secret, id) { + if (!secret || !id) { + return ''; + } + + return `${EndpointSection}=${endpoint};${IdSection}=${id};${SecretSection}=${secret}`; +} + +/** + * Extracts a valid domain from the given endpoint URL based on trusted domain labels. + * + * @param {string} endpoint - The endpoint URL. + * @returns {string} - The valid domain or an empty string if no valid domain is found. + */ +function getValidDomain(endpoint) { + try { + const url = new URL(endpoint); + const trustedDomainLabels = [AzConfigDomainLabel, AppConfigDomainLabel]; + const host = url.hostname.toLowerCase(); + + for (const label of trustedDomainLabels) { + const index = host.lastIndexOf(label); + if (index !== -1) { + return host.substring(index); + } + } + } catch (error) { + console.error("Error parsing URL:", error.message); + } + + return ""; +} + +/** + * Checks if the given host ends with the valid domain. + * + * @param {string} host - The host to be validated. + * @param {string} validDomain - The valid domain to check against. + * @returns {boolean} - True if the host ends with the valid domain, false otherwise. + */ +function isValidEndpoint(host, validDomain) { + if (!validDomain) { + return false; + } + + return host.toLowerCase().endsWith(validDomain.toLowerCase()); +} + + + diff --git a/src/ConfigurationClientWrapper.ts b/src/ConfigurationClientWrapper.ts new file mode 100644 index 00000000..0f3d619d --- /dev/null +++ b/src/ConfigurationClientWrapper.ts @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { AppConfigurationClient } from "@azure/app-configuration"; + +export class ConfigurationClientWrapper { + endpoint: string; + client: AppConfigurationClient; + backoffEndTime: number; + failedAttempts: number = 0; + + constructor(endpoint: string, client: AppConfigurationClient) { + this.endpoint = endpoint; + this.client = client; + } +} \ No newline at end of file diff --git a/src/load.ts b/src/load.ts index ce3d39c2..7367d53e 100644 --- a/src/load.ts +++ b/src/load.ts @@ -86,7 +86,7 @@ function instanceOfTokenCredential(obj: unknown) { return obj && typeof obj === "object" && "getToken" in obj && typeof obj.getToken === "function"; } -function getClientOptions(options?: AzureAppConfigurationOptions): AppConfigurationClientOptions | undefined { +export function getClientOptions(options?: AzureAppConfigurationOptions): AppConfigurationClientOptions | undefined { // user-agent let userAgentPrefix = RequestTracing.USER_AGENT_PREFIX; // Default UA for JavaScript Provider const userAgentOptions = options?.clientOptions?.userAgentOptions; diff --git a/src/requestTracing/utils.ts b/src/requestTracing/utils.ts index de335737..04c9eab9 100644 --- a/src/requestTracing/utils.ts +++ b/src/requestTracing/utils.ts @@ -165,3 +165,11 @@ function isWebWorker() { return workerGlobalScopeDefined && importScriptsAsGlobalFunction && isNavigatorDefinedAsExpected; } + +export function isFailoverableEnv() { + if (isBrowser() || isWebWorker()) { + return false; + } + + return true; +} From 71504163b74ae5f6672c7934b9f39ada2c53c776 Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Mon, 30 Sep 2024 16:26:44 +0800 Subject: [PATCH 02/24] resolve conflicts --- src/AzureAppConfigurationImpl.ts | 195 +++++++++++++++++++----------- src/ConfigurationClientManager.ts | 62 +++++++--- src/ConfigurationClientWrapper.ts | 35 +++++- src/load.ts | 63 ++-------- src/requestTracing/constants.ts | 1 + src/requestTracing/utils.ts | 19 ++- test/clientOptions.test.ts | 4 + test/featureFlag.test.ts | 4 +- test/json.test.ts | 4 +- test/keyvault.test.ts | 10 +- test/load.test.ts | 4 +- test/refresh.test.ts | 24 +++- test/requestTracing.test.ts | 51 ++++++-- test/utils/testHelper.ts | 27 ++++- 14 files changed, 338 insertions(+), 165 deletions(-) diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index 668bd6af..b600f527 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -36,6 +36,7 @@ import { RefreshTimer } from "./refresh/RefreshTimer.js"; import { getConfigurationSettingWithTrace, listConfigurationSettingsWithTrace, requestTracingEnabled } from "./requestTracing/utils.js"; import { KeyFilter, LabelFilter, SettingSelector } from "./types.js"; import { ConfigurationClientManager } from "./ConfigurationClientManager.js"; +import { updateClientBackoffStatus } from "./ConfigurationClientWrapper.js"; type PagedSettingSelector = SettingSelector & { /** @@ -62,6 +63,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { #clientEndpoint: string | undefined; #options: AzureAppConfigurationOptions | undefined; #isInitialLoadCompleted: boolean = false; + #isFailoverRequest: boolean = false; // Refresh #refreshInterval: number = DEFAULT_REFRESH_INTERVAL_IN_MS; @@ -80,13 +82,11 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { #featureFlagSelectors: PagedSettingSelector[] = []; constructor( - client: AppConfigurationClient, - clientEndpoint: string | undefined, - options: AzureAppConfigurationOptions | undefined + clientManager: ConfigurationClientManager, + options: AzureAppConfigurationOptions | undefined, ) { - this.#client = client; - this.#clientEndpoint = clientEndpoint; this.#options = options; + this.#clientManager = clientManager; // Enable request tracing if not opt-out this.#requestTracingEnabled = requestTracingEnabled(); @@ -199,34 +199,70 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { return { requestTracingEnabled: this.#requestTracingEnabled, initialLoadCompleted: this.#isInitialLoadCompleted, - appConfigOptions: this.#options + appConfigOptions: this.#options, + isFailoverRequest: this.#isFailoverRequest }; } - async #loadSelectedKeyValues(): Promise { - const loadedSettings: ConfigurationSetting[] = []; + async #executeWithFailoverPolicy(funcToExecute) { + const clients = await this.#clientManager.getClients(); + if (clients.length === 0) { + this.#clientManager.refreshClients(); + throw new Error("No client is available to connect to the target App Configuration store."); + } - // validate selectors - const selectors = getValidKeyValueSelectors(this.#options?.selectors); + for (const client of clients) { + let successful = false; + try { + const result = await funcToExecute(client.client); + this.#isFailoverRequest = false; + successful = true; + updateClientBackoffStatus(client, successful); + return result; + } catch (error) { + if (isFailoverableError(error)) { + updateClientBackoffStatus(client, successful); + this.#isFailoverRequest = true; + continue; + } - for (const selector of selectors) { - const listOptions: ListConfigurationSettingsOptions = { - keyFilter: selector.keyFilter, - labelFilter: selector.labelFilter - }; + throw error; + } + } - const settings = listConfigurationSettingsWithTrace( - this.#requestTraceOptions, - this.#client, - listOptions - ); + this.#clientManager.refreshClients(); + throw new Error("All app configuration clients failed to get settings."); + } - for await (const setting of settings) { - if (!isFeatureFlag(setting)) { // exclude feature flags - loadedSettings.push(setting); + async #loadSelectedKeyValues(): Promise { + // validate selectors + const selectors = getValidKeyValueSelectors(this.#options?.selectors); + let loadedSettings: ConfigurationSetting[] = []; + + const funcToExecute = async (client) => { + const loadedSettings: ConfigurationSetting[] = []; + for (const selector of selectors) { + const listOptions: ListConfigurationSettingsOptions = { + keyFilter: selector.keyFilter, + labelFilter: selector.labelFilter + }; + + const settings = listConfigurationSettingsWithTrace( + this.#requestTraceOptions, + client, + listOptions + ); + + for await (const setting of settings) { + if (!isFeatureFlag(setting)) { // exclude feature flags + loadedSettings.push(setting); + } } } - } + return loadedSettings; + }; + + loadedSettings = await this.#executeWithFailoverPolicy(funcToExecute); return loadedSettings; } @@ -281,30 +317,43 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { } async #loadFeatureFlags() { - const featureFlagSettings: ConfigurationSetting[] = []; - for (const selector of this.#featureFlagSelectors) { - const listOptions: ListConfigurationSettingsOptions = { - keyFilter: `${featureFlagPrefix}${selector.keyFilter}`, - labelFilter: selector.labelFilter - }; + // Temporary map to store feature flags, key is the key of the setting, value is the raw value of the setting + const funcToExecute = async (client) => { + const featureFlagSettings: ConfigurationSetting[] = []; + const selectors = JSON.parse( + JSON.stringify(this.#featureFlagSelectors) + ); - const pageEtags: string[] = []; - const pageIterator = listConfigurationSettingsWithTrace( - this.#requestTraceOptions, - this.#client, - listOptions - ).byPage(); - for await (const page of pageIterator) { - pageEtags.push(page.etag ?? ""); - for (const setting of page.items) { - if (isFeatureFlag(setting)) { - featureFlagSettings.push(setting); + for (const selector of selectors) { + const listOptions: ListConfigurationSettingsOptions = { + keyFilter: `${featureFlagPrefix}${selector.keyFilter}`, + labelFilter: selector.labelFilter + }; + + const pageEtags: string[] = []; + const pageIterator = listConfigurationSettingsWithTrace( + this.#requestTraceOptions, + client, + listOptions + ).byPage(); + for await (const page of pageIterator) { + pageEtags.push(page.etag ?? ""); + for (const setting of page.items) { + if (isFeatureFlag(setting)) { + featureFlagSettings.push(setting); + } } } + selector.pageEtags = pageEtags; } - selector.pageEtags = pageEtags; + + this.#featureFlagSelectors = selectors; + return featureFlagSettings; } + let featureFlagSettings: ConfigurationSetting[] = []; + featureFlagSettings = await this.#executeWithFailoverPolicy(funcToExecute); + // parse feature flags const featureFlags = await Promise.all( featureFlagSettings.map(setting => this.#parseFeatureFlag(setting)) @@ -456,30 +505,32 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { } // check if any feature flag is changed - let needRefresh = false; - for (const selector of this.#featureFlagSelectors) { - const listOptions: ListConfigurationSettingsOptions = { - keyFilter: `${featureFlagPrefix}${selector.keyFilter}`, - labelFilter: selector.labelFilter, - pageEtags: selector.pageEtags - }; - const pageIterator = listConfigurationSettingsWithTrace( - this.#requestTraceOptions, - this.#client, - listOptions - ).byPage(); - - for await (const page of pageIterator) { - if (page._response.status === 200) { // created or changed - needRefresh = true; - break; + const funcToExecute = async (client) => { + const needRefresh = false; + for (const selector of this.#featureFlagSelectors) { + const listOptions: ListConfigurationSettingsOptions = { + keyFilter: `${featureFlagPrefix}${selector.keyFilter}`, + labelFilter: selector.labelFilter, + pageEtags: selector.pageEtags + }; + + const pageIterator = listConfigurationSettingsWithTrace( + this.#requestTraceOptions, + client, + listOptions + ).byPage(); + + for await (const page of pageIterator) { + if (page._response.status === 200) { // created or changed + return true; + } } } - - if (needRefresh) { - break; // short-circuit if result from any of the selectors is changed - } - } + return needRefresh; + }; + + let needRefresh: boolean; + needRefresh = await this.#executeWithFailoverPolicy(funcToExecute); if (needRefresh) { try { @@ -542,14 +593,18 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { * Get a configuration setting by key and label. If the setting is not found, return undefine instead of throwing an error. */ async #getConfigurationSetting(configurationSettingId: ConfigurationSettingId, customOptions?: GetConfigurationSettingOptions): Promise { - let response: GetConfigurationSettingResponse | undefined; - try { - response = await getConfigurationSettingWithTrace( + const funcToExecute = async (client) => { + return getConfigurationSettingWithTrace( this.#requestTraceOptions, - this.#client, + client, configurationSettingId, customOptions ); + }; + + let response: GetConfigurationSettingResponse | undefined; + try { + response = await this.#executeWithFailoverPolicy(funcToExecute); } catch (error) { if (isRestError(error) && error.statusCode === 404) { response = undefined; @@ -796,3 +851,7 @@ function getValidFeatureFlagSelectors(selectors?: SettingSelector[]): SettingSel return getValidSelectors(selectors); } } + +function isFailoverableError(error: any): boolean { + return isRestError(error) && (error.statusCode === 408 || error.statusCode === 429 || (error.statusCode !== undefined && error.statusCode >= 500)); +} diff --git a/src/ConfigurationClientManager.ts b/src/ConfigurationClientManager.ts index 6c0dd8b3..1e5a8c57 100644 --- a/src/ConfigurationClientManager.ts +++ b/src/ConfigurationClientManager.ts @@ -4,11 +4,13 @@ import { AppConfigurationClient, AppConfigurationClientOptions } from "@azure/app-configuration"; import { ConfigurationClientWrapper } from "./ConfigurationClientWrapper" import { TokenCredential } from "@azure/identity"; -import { AzureAppConfigurationOptions } from "./AzureAppConfigurationOptions"; -import { getClientOptions } from "./load"; +import { AzureAppConfigurationOptions, MaxRetries, MaxRetryDelayInMs } from "./AzureAppConfigurationOptions"; +import { isFailoverableEnv } from "./requestTracing/utils"; +import * as RequestTracing from "./requestTracing/constants"; const TCP_ORIGIN = "_origin._tcp"; const ALT = "_alt"; +const TCP = "_tcp"; const EndpointSection = "Endpoint"; const IdSection = "Id"; const SecretSection = "Secret"; @@ -16,9 +18,7 @@ const AzConfigDomainLabel = ".azconfig." const AppConfigDomainLabel = ".appconfig." const FallbackClientRefreshExpireInterval = 60 * 60 * 1000; // 1 hour in milliseconds const MinimalClientRefreshInterval = 30 * 1000; // 30 seconds in milliseconds -const MaxBackoffDuration = 10 * 60 * 1000; // 10 minutes in milliseconds -const MinBackoffDuration = 30 * 1000; // 30 seconds in milliseconds -const dns = require('dns').promises; +const SrvQueryTimeout = 5000; // 5 seconds interface IConfigurationClientManager { getClients(): ConfigurationClientWrapper[]; @@ -26,7 +26,7 @@ interface IConfigurationClientManager { } export class ConfigurationClientManager { - #isFailoverable: boolean; + isFailoverable: boolean; #endpoint: string; #secret : string; #id : string; @@ -35,9 +35,8 @@ export class ConfigurationClientManager { #validDomain: string; #staticClients: ConfigurationClientWrapper[]; #dynamicClients: ConfigurationClientWrapper[]; - #lastFallbackClientRefreshTime: number; - #lastFallbackClientRefreshAttempt: number; - + #lastFallbackClientRefreshTime: number = 0; + #lastFallbackClientRefreshAttempt: number = 0; constructor ( connectionStringOrEndpoint?: string | URL, @@ -65,23 +64,24 @@ export class ConfigurationClientManager { this.#endpoint = connectionStringOrEndpoint.toString(); this.#credential = credential; } else { - throw new Error("Invalid endpoint URL."); + throw new Error("A connection string or an endpoint with credential must be specified to create a client."); } this.#staticClients = [new ConfigurationClientWrapper(this.#endpoint, staticClient)]; this.#validDomain = getValidDomain(this.#endpoint); - + this.isFailoverable = (options?.replicaDiscoveryEnabled ?? true) && isFailoverableEnv(); } async getClients() { - if (!this.#isFailoverable) { + if (!this.isFailoverable) { return this.#staticClients; } const currentTime = Date.now(); if (this.#isFallbackClientDiscoveryDue(currentTime)) { this.#lastFallbackClientRefreshAttempt = currentTime; - await this.#discoverFallbackClients(this.#endpoint); + const host = new URL(this.#endpoint).hostname; + await this.#discoverFallbackClients(host); } // Filter static clients where BackoffEndTime is less than or equal to now @@ -98,7 +98,7 @@ export class ConfigurationClientManager { async refreshClients() { const currentTime = Date.now(); - if (this.#isFailoverable && + if (this.isFailoverable && currentTime > new Date(this.#lastFallbackClientRefreshAttempt + MinimalClientRefreshInterval).getTime()) { this.#lastFallbackClientRefreshAttempt = currentTime; const url = new URL(this.#endpoint); @@ -108,8 +108,8 @@ export class ConfigurationClientManager { async #discoverFallbackClients(host) { const timeout = setTimeout(() => { - }, 10000); // 10 seconds - const srvResults = querySrvTargetHost(host); + }, SrvQueryTimeout); + const srvResults = await querySrvTargetHost(host); try { const result = await Promise.race([srvResults, timeout]); @@ -168,6 +168,13 @@ export class ConfigurationClientManager { */ async function querySrvTargetHost(host) { const results: string[] = []; + let dns; + + if (isFailoverableEnv()) { + dns = require('dns/promises'); + } else { + return results; + } try { // Look up SRV records for the origin host @@ -185,7 +192,7 @@ async function querySrvTargetHost(host) { while (true) { const currentAlt = `${ALT}${index}`; try { - const altRecords = await dns.resolveSrv(`_${currentAlt}._tcp.${originHost}`); + const altRecords = await dns.resolveSrv(`${currentAlt}.${TCP}.${originHost}`); if (altRecords.length === 0) { break; // No more alternate records, exit loop } @@ -298,5 +305,26 @@ function isValidEndpoint(host, validDomain) { return host.toLowerCase().endsWith(validDomain.toLowerCase()); } +export function getClientOptions(options?: AzureAppConfigurationOptions): AppConfigurationClientOptions | undefined { + // user-agent + let userAgentPrefix = RequestTracing.USER_AGENT_PREFIX; // Default UA for JavaScript Provider + const userAgentOptions = options?.clientOptions?.userAgentOptions; + if (userAgentOptions?.userAgentPrefix) { + userAgentPrefix = `${userAgentOptions.userAgentPrefix} ${userAgentPrefix}`; // Prepend if UA prefix specified by user + } + // retry options + const defaultRetryOptions = { + maxRetries: MaxRetries, + maxRetryDelayInMs: MaxRetryDelayInMs, + } + const retryOptions = Object.assign({}, defaultRetryOptions, options?.clientOptions?.retryOptions); + + return Object.assign({}, options?.clientOptions, { + retryOptions, + userAgentOptions: { + userAgentPrefix + } + }); +} diff --git a/src/ConfigurationClientWrapper.ts b/src/ConfigurationClientWrapper.ts index 0f3d619d..a7a4b9e5 100644 --- a/src/ConfigurationClientWrapper.ts +++ b/src/ConfigurationClientWrapper.ts @@ -3,14 +3,47 @@ import { AppConfigurationClient } from "@azure/app-configuration"; +const MaxBackoffDuration = 10 * 60 * 1000; // 10 minutes in milliseconds +const MinBackoffDuration = 30 * 1000; // 30 seconds in milliseconds +const MAX_SAFE_EXPONENTIAL = 30; // Used to avoid overflow. bitwise operations in JavaScript are limited to 32 bits. It overflows at 2^31 - 1. +const JITTER_RATIO = 0.25; + export class ConfigurationClientWrapper { endpoint: string; client: AppConfigurationClient; - backoffEndTime: number; + backoffEndTime: number = 0; // Timestamp failedAttempts: number = 0; constructor(endpoint: string, client: AppConfigurationClient) { this.endpoint = endpoint; this.client = client; } +} + +export function updateClientBackoffStatus(clientWrapper: ConfigurationClientWrapper, successfull: boolean) { + if (successfull) { + clientWrapper.failedAttempts = 0; + clientWrapper.backoffEndTime = Date.now(); + } else { + clientWrapper.failedAttempts += 1; + clientWrapper.backoffEndTime = Date.now() + calculateBackoffDuration(clientWrapper.failedAttempts); + } +} + +export function calculateBackoffDuration(failedAttempts: number) { + if (failedAttempts <= 1) { + return MinBackoffDuration; + } + + // exponential: minBackoff * 2^(failedAttempts-1) + const exponential = Math.min(failedAttempts - 1, MAX_SAFE_EXPONENTIAL); + let calculatedBackoffDuration = MinBackoffDuration * (1 << exponential); + if (calculatedBackoffDuration > MaxBackoffDuration) { + calculatedBackoffDuration = MaxBackoffDuration; + } + + // jitter: random value between [-1, 1) * jitterRatio * calculatedBackoffMs + const jitter = JITTER_RATIO * (Math.random() * 2 - 1); + + return calculatedBackoffDuration * (1 + jitter); } \ No newline at end of file diff --git a/src/load.ts b/src/load.ts index 7367d53e..cfe95042 100644 --- a/src/load.ts +++ b/src/load.ts @@ -1,12 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { AppConfigurationClient, AppConfigurationClientOptions } from "@azure/app-configuration"; import { TokenCredential } from "@azure/identity"; import { AzureAppConfiguration } from "./AzureAppConfiguration.js"; import { AzureAppConfigurationImpl } from "./AzureAppConfigurationImpl.js"; -import { AzureAppConfigurationOptions, MaxRetries, MaxRetryDelayInMs } from "./AzureAppConfigurationOptions.js"; -import * as RequestTracing from "./requestTracing/constants.js"; +import { AzureAppConfigurationOptions } from "./AzureAppConfigurationOptions.js"; +import { ConfigurationClientManager } from "./ConfigurationClientManager.js"; const MIN_DELAY_FOR_UNHANDLED_ERROR: number = 5000; // 5 seconds @@ -31,23 +30,20 @@ export async function load( appConfigOptions?: AzureAppConfigurationOptions ): Promise { const startTimestamp = Date.now(); - let client: AppConfigurationClient; - let clientEndpoint: string | undefined; let options: AzureAppConfigurationOptions | undefined; + let clientManager: ConfigurationClientManager; // input validation if (typeof connectionStringOrEndpoint === "string" && !instanceOfTokenCredential(credentialOrOptions)) { const connectionString = connectionStringOrEndpoint; options = credentialOrOptions as AzureAppConfigurationOptions; - const clientOptions = getClientOptions(options); - client = new AppConfigurationClient(connectionString, clientOptions); - clientEndpoint = getEndpoint(connectionStringOrEndpoint); + clientManager = new ConfigurationClientManager(connectionString, options); } else if ((connectionStringOrEndpoint instanceof URL || typeof connectionStringOrEndpoint === "string") && instanceOfTokenCredential(credentialOrOptions)) { + let endpoint = connectionStringOrEndpoint; // ensure string is a valid URL. - if (typeof connectionStringOrEndpoint === "string") { + if (typeof endpoint === "string") { try { - const endpointUrl = new URL(connectionStringOrEndpoint); - clientEndpoint = endpointUrl.toString(); + endpoint = new URL(endpoint); } catch (error) { if (error.code === "ERR_INVALID_URL") { throw new Error("Invalid endpoint URL.", { cause: error }); @@ -55,19 +51,16 @@ export async function load( throw error; } } - } else { - clientEndpoint = connectionStringOrEndpoint.toString(); } const credential = credentialOrOptions as TokenCredential; options = appConfigOptions; - const clientOptions = getClientOptions(options); - client = new AppConfigurationClient(clientEndpoint, credential, clientOptions); + clientManager = new ConfigurationClientManager(endpoint, credential, options); } else { throw new Error("A connection string or an endpoint with credential must be specified to create a client."); } try { - const appConfiguration = new AzureAppConfigurationImpl(client, clientEndpoint, options); + const appConfiguration = new AzureAppConfigurationImpl(clientManager, options); await appConfiguration.load(); return appConfiguration; } catch (error) { @@ -85,41 +78,3 @@ export async function load( function instanceOfTokenCredential(obj: unknown) { return obj && typeof obj === "object" && "getToken" in obj && typeof obj.getToken === "function"; } - -export function getClientOptions(options?: AzureAppConfigurationOptions): AppConfigurationClientOptions | undefined { - // user-agent - let userAgentPrefix = RequestTracing.USER_AGENT_PREFIX; // Default UA for JavaScript Provider - const userAgentOptions = options?.clientOptions?.userAgentOptions; - if (userAgentOptions?.userAgentPrefix) { - userAgentPrefix = `${userAgentOptions.userAgentPrefix} ${userAgentPrefix}`; // Prepend if UA prefix specified by user - } - - // retry options - const defaultRetryOptions = { - maxRetries: MaxRetries, - maxRetryDelayInMs: MaxRetryDelayInMs, - }; - const retryOptions = Object.assign({}, defaultRetryOptions, options?.clientOptions?.retryOptions); - - return Object.assign({}, options?.clientOptions, { - retryOptions, - userAgentOptions: { - userAgentPrefix - } - }); -} - -function getEndpoint(connectionString: string): string | undefined { - const parts = connectionString.split(";"); - const endpointPart = parts.find(part => part.startsWith("Endpoint=")); - - if (endpointPart) { - let endpoint = endpointPart.split("=")[1]; - if (!endpoint.endsWith("/")) { - endpoint += "/"; - } - return endpoint; - } - - return undefined; -} diff --git a/src/requestTracing/constants.ts b/src/requestTracing/constants.ts index d46cdfda..7d43e0a1 100644 --- a/src/requestTracing/constants.ts +++ b/src/requestTracing/constants.ts @@ -43,6 +43,7 @@ export enum RequestType { STARTUP = "Startup", WATCH = "Watch" } +export const FAILOVER_REQUEST_TYPE = "FailoverRequest"; // Tag names export const KEY_VAULT_CONFIGURED_TAG = "UsesKeyVault"; diff --git a/src/requestTracing/utils.ts b/src/requestTracing/utils.ts index 04c9eab9..35449337 100644 --- a/src/requestTracing/utils.ts +++ b/src/requestTracing/utils.ts @@ -19,7 +19,8 @@ import { REQUEST_TYPE_KEY, RequestType, SERVICE_FABRIC_ENV_VAR, - CORRELATION_CONTEXT_HEADER_NAME + CORRELATION_CONTEXT_HEADER_NAME, + FAILOVER_REQUEST_TYPE } from "./constants"; // Utils @@ -28,17 +29,18 @@ export function listConfigurationSettingsWithTrace( requestTracingEnabled: boolean; initialLoadCompleted: boolean; appConfigOptions: AzureAppConfigurationOptions | undefined; + isFailoverRequest: boolean; }, client: AppConfigurationClient, listOptions: ListConfigurationSettingsOptions ) { - const { requestTracingEnabled, initialLoadCompleted, appConfigOptions } = requestTracingOptions; + const { requestTracingEnabled, initialLoadCompleted, appConfigOptions, isFailoverRequest } = requestTracingOptions; const actualListOptions = { ...listOptions }; if (requestTracingEnabled) { actualListOptions.requestOptions = { customHeaders: { - [CORRELATION_CONTEXT_HEADER_NAME]: createCorrelationContextHeader(appConfigOptions, initialLoadCompleted) + [CORRELATION_CONTEXT_HEADER_NAME]: createCorrelationContextHeader(appConfigOptions, initialLoadCompleted, isFailoverRequest) } }; } @@ -51,18 +53,19 @@ export function getConfigurationSettingWithTrace( requestTracingEnabled: boolean; initialLoadCompleted: boolean; appConfigOptions: AzureAppConfigurationOptions | undefined; + isFailoverRequest: boolean; }, client: AppConfigurationClient, configurationSettingId: ConfigurationSettingId, getOptions?: GetConfigurationSettingOptions, ) { - const { requestTracingEnabled, initialLoadCompleted, appConfigOptions } = requestTracingOptions; + const { requestTracingEnabled, initialLoadCompleted, appConfigOptions, isFailoverRequest } = requestTracingOptions; const actualGetOptions = { ...getOptions }; if (requestTracingEnabled) { actualGetOptions.requestOptions = { customHeaders: { - [CORRELATION_CONTEXT_HEADER_NAME]: createCorrelationContextHeader(appConfigOptions, initialLoadCompleted) + [CORRELATION_CONTEXT_HEADER_NAME]: createCorrelationContextHeader(appConfigOptions, initialLoadCompleted, isFailoverRequest) } }; } @@ -70,7 +73,7 @@ export function getConfigurationSettingWithTrace( return client.getConfigurationSetting(configurationSettingId, actualGetOptions); } -export function createCorrelationContextHeader(options: AzureAppConfigurationOptions | undefined, isInitialLoadCompleted: boolean): string { +export function createCorrelationContextHeader(options: AzureAppConfigurationOptions | undefined, isInitialLoadCompleted: boolean, isFailoverRequest: boolean): string { /* RequestType: 'Startup' during application starting up, 'Watch' after startup completed. Host: identify with defined envs @@ -100,6 +103,10 @@ export function createCorrelationContextHeader(options: AzureAppConfigurationOpt contextParts.push(tag); } + if (isFailoverRequest) { + contextParts.push(FAILOVER_REQUEST_TYPE); + } + return contextParts.join(","); } diff --git a/test/clientOptions.test.ts b/test/clientOptions.test.ts index 62e1b21c..4342334f 100644 --- a/test/clientOptions.test.ts +++ b/test/clientOptions.test.ts @@ -30,6 +30,7 @@ describe("custom client options", function () { this.timeout(15000); const fakeEndpoint = "https://azure.azconfig.io"; + const replicaDiscoveryEnabled = false; beforeEach(() => { // Thus here mock it to reply 500, in which case the retry mechanism works. nock(fakeEndpoint).persist().get(() => true).reply(500); @@ -43,6 +44,7 @@ describe("custom client options", function () { const countPolicy = new HttpRequestCountPolicy(); const loadPromise = () => { return load(createMockedConnectionString(fakeEndpoint), { + replicaDiscoveryEnabled: replicaDiscoveryEnabled, clientOptions: { additionalPolicies: [{ policy: countPolicy, @@ -65,6 +67,7 @@ describe("custom client options", function () { const countPolicy = new HttpRequestCountPolicy(); const loadWithMaxRetries = (maxRetries: number) => { return load(createMockedConnectionString(fakeEndpoint), { + replicaDiscoveryEnabled: replicaDiscoveryEnabled, clientOptions: { additionalPolicies: [{ policy: countPolicy, @@ -103,6 +106,7 @@ describe("custom client options", function () { const countPolicy = new HttpRequestCountPolicy(); const loadPromise = () => { return load(createMockedConnectionString(fakeEndpoint), { + replicaDiscoveryEnabled: replicaDiscoveryEnabled, clientOptions: { additionalPolicies: [{ policy: countPolicy, diff --git a/test/featureFlag.test.ts b/test/featureFlag.test.ts index 1bf92d7f..faa11f33 100644 --- a/test/featureFlag.test.ts +++ b/test/featureFlag.test.ts @@ -4,7 +4,7 @@ import * as chai from "chai"; import * as chaiAsPromised from "chai-as-promised"; import { load } from "./exportedApi.js"; -import { createMockedConnectionString, createMockedEndpoint, createMockedFeatureFlag, createMockedKeyValue, mockAppConfigurationClientListConfigurationSettings, restoreMocks } from "./utils/testHelper.js"; +import { createMockedConnectionString, createMockedEndpoint, createMockedFeatureFlag, createMockedKeyValue, mockAppConfigurationClientListConfigurationSettings, mockConfigurationManagerGetClients, restoreMocks } from "./utils/testHelper.js"; chai.use(chaiAsPromised); const expect = chai.expect; @@ -201,7 +201,9 @@ const mockedKVs = [{ describe("feature flags", function () { this.timeout(10000); + const isFailoverable = false; before(() => { + mockConfigurationManagerGetClients(isFailoverable); mockAppConfigurationClientListConfigurationSettings(mockedKVs); }); diff --git a/test/json.test.ts b/test/json.test.ts index c139d53b..52b7c71f 100644 --- a/test/json.test.ts +++ b/test/json.test.ts @@ -6,13 +6,15 @@ import * as chaiAsPromised from "chai-as-promised"; chai.use(chaiAsPromised); const expect = chai.expect; import { load } from "./exportedApi.js"; -import { mockAppConfigurationClientListConfigurationSettings, restoreMocks, createMockedConnectionString, createMockedKeyVaultReference, createMockedJsonKeyValue } from "./utils/testHelper.js"; +import { mockAppConfigurationClientListConfigurationSettings, restoreMocks, createMockedConnectionString, createMockedKeyVaultReference, createMockedJsonKeyValue, mockConfigurationManagerGetClients } from "./utils/testHelper.js"; const jsonKeyValue = createMockedJsonKeyValue("json.settings.logging", '{"Test":{"Level":"Debug"},"Prod":{"Level":"Warning"}}'); const keyVaultKeyValue = createMockedKeyVaultReference("TestKey", "https://fake-vault-name.vault.azure.net/secrets/fakeSecretName"); +const isFailoverable = false; describe("json", function () { beforeEach(() => { + mockConfigurationManagerGetClients(isFailoverable); }); afterEach(() => { diff --git a/test/keyvault.test.ts b/test/keyvault.test.ts index cf01235c..3e8b5ee6 100644 --- a/test/keyvault.test.ts +++ b/test/keyvault.test.ts @@ -9,6 +9,7 @@ import { load } from "./exportedApi.js"; import { sinon, createMockedConnectionString, createMockedTokenCredential, mockAppConfigurationClientListConfigurationSettings, mockSecretClientGetSecret, restoreMocks, createMockedKeyVaultReference } from "./utils/testHelper.js"; import { KeyVaultSecret, SecretClient } from "@azure/keyvault-secrets"; +const replicaDiscoveryEnabled = false; const mockedData = [ // key, secretUri, value ["TestKey", "https://fake-vault-name.vault.azure.net/secrets/fakeSecretName", "SecretValue"], @@ -39,11 +40,14 @@ describe("key vault reference", function () { }); it("require key vault options to resolve reference", async () => { - return expect(load(createMockedConnectionString())).eventually.rejectedWith("Configure keyVaultOptions to resolve Key Vault Reference(s)."); + return expect(load(createMockedConnectionString(), { + replicaDiscoveryEnabled: replicaDiscoveryEnabled + })).eventually.rejectedWith("Configure keyVaultOptions to resolve Key Vault Reference(s)."); }); it("should resolve key vault reference with credential", async () => { const settings = await load(createMockedConnectionString(), { + replicaDiscoveryEnabled: replicaDiscoveryEnabled, keyVaultOptions: { credential: createMockedTokenCredential() } @@ -55,6 +59,7 @@ describe("key vault reference", function () { it("should resolve key vault reference with secret resolver", async () => { const settings = await load(createMockedConnectionString(), { + replicaDiscoveryEnabled: replicaDiscoveryEnabled, keyVaultOptions: { secretResolver: (kvrUrl) => { return "SecretResolver::" + kvrUrl.toString(); @@ -75,6 +80,7 @@ describe("key vault reference", function () { const client2 = new SecretClient("https://fake-vault-name2.vault.azure.net", createMockedTokenCredential()); sinon.stub(client2, "getSecret").returns(Promise.resolve({value: "SecretValueViaClient2" } as KeyVaultSecret)); const settings = await load(createMockedConnectionString(), { + replicaDiscoveryEnabled: replicaDiscoveryEnabled, keyVaultOptions: { secretClients: [ client1, @@ -89,6 +95,7 @@ describe("key vault reference", function () { it("should throw error when secret clients not provided for all key vault references", async () => { const loadKeyVaultPromise = load(createMockedConnectionString(), { + replicaDiscoveryEnabled: replicaDiscoveryEnabled, keyVaultOptions: { secretClients: [ new SecretClient("https://fake-vault-name.vault.azure.net", createMockedTokenCredential()), @@ -100,6 +107,7 @@ describe("key vault reference", function () { it("should fallback to use default credential when corresponding secret client not provided", async () => { const settings = await load(createMockedConnectionString(), { + replicaDiscoveryEnabled: replicaDiscoveryEnabled, keyVaultOptions: { secretClients: [ new SecretClient("https://fake-vault-name.vault.azure.net", createMockedTokenCredential()), diff --git a/test/load.test.ts b/test/load.test.ts index ce22b1a8..0e3e7275 100644 --- a/test/load.test.ts +++ b/test/load.test.ts @@ -6,7 +6,7 @@ import * as chaiAsPromised from "chai-as-promised"; chai.use(chaiAsPromised); const expect = chai.expect; import { load } from "./exportedApi.js"; -import { mockAppConfigurationClientListConfigurationSettings, restoreMocks, createMockedConnectionString, createMockedEndpoint, createMockedTokenCredential, createMockedKeyValue } from "./utils/testHelper.js"; +import { mockAppConfigurationClientListConfigurationSettings, restoreMocks, createMockedConnectionString, createMockedEndpoint, createMockedTokenCredential, createMockedKeyValue, mockConfigurationManagerGetClients } from "./utils/testHelper.js"; const mockedKVs = [{ key: "app.settings.fontColor", @@ -79,7 +79,9 @@ const mockedKVs = [{ describe("load", function () { this.timeout(10000); + const isFailoverable = false; before(() => { + mockConfigurationManagerGetClients(isFailoverable); mockAppConfigurationClientListConfigurationSettings(mockedKVs); }); diff --git a/test/refresh.test.ts b/test/refresh.test.ts index fcffaee5..ac28bbce 100644 --- a/test/refresh.test.ts +++ b/test/refresh.test.ts @@ -10,6 +10,7 @@ import { mockAppConfigurationClientListConfigurationSettings, mockAppConfigurati import * as uuid from "uuid"; let mockedKVs: any[] = []; +const replicaDiscoveryEnabled = false; function updateSetting(key: string, value: any) { const setting = mockedKVs.find(elem => elem.key === key); @@ -42,7 +43,9 @@ describe("dynamic refresh", function () { it("should throw error when refresh is not enabled but refresh is called", async () => { const connectionString = createMockedConnectionString(); - const settings = await load(connectionString); + const settings = await load(connectionString, { + replicaDiscoveryEnabled: replicaDiscoveryEnabled + }); const refreshCall = settings.refresh(); return expect(refreshCall).eventually.rejectedWith("Refresh is not enabled for key-values or feature flags."); }); @@ -50,12 +53,14 @@ describe("dynamic refresh", function () { it("should only allow non-empty list of watched settings when refresh is enabled", async () => { const connectionString = createMockedConnectionString(); const loadWithEmptyWatchedSettings = load(connectionString, { + replicaDiscoveryEnabled: replicaDiscoveryEnabled, refreshOptions: { enabled: true, watchedSettings: [] } }); const loadWithUndefinedWatchedSettings = load(connectionString, { + replicaDiscoveryEnabled: replicaDiscoveryEnabled, refreshOptions: { enabled: true } @@ -69,6 +74,7 @@ describe("dynamic refresh", function () { it("should not allow refresh interval less than 1 second", async () => { const connectionString = createMockedConnectionString(); const loadWithInvalidRefreshInterval = load(connectionString, { + replicaDiscoveryEnabled: replicaDiscoveryEnabled, refreshOptions: { enabled: true, watchedSettings: [ @@ -83,6 +89,7 @@ describe("dynamic refresh", function () { it("should not allow '*' in key or label", async () => { const connectionString = createMockedConnectionString(); const loadWithInvalidKey = load(connectionString, { + replicaDiscoveryEnabled: replicaDiscoveryEnabled, refreshOptions: { enabled: true, watchedSettings: [ @@ -91,6 +98,7 @@ describe("dynamic refresh", function () { } }); const loadWithInvalidKey2 = load(connectionString, { + replicaDiscoveryEnabled: replicaDiscoveryEnabled, refreshOptions: { enabled: true, watchedSettings: [ @@ -124,13 +132,16 @@ describe("dynamic refresh", function () { it("should throw error when calling onRefresh when refresh is not enabled", async () => { const connectionString = createMockedConnectionString(); - const settings = await load(connectionString); + const settings = await load(connectionString, { + replicaDiscoveryEnabled: replicaDiscoveryEnabled, + }); expect(() => settings.onRefresh(() => { })).throws("Refresh is not enabled for key-values or feature flags."); }); it("should only update values after refreshInterval", async () => { const connectionString = createMockedConnectionString(); const settings = await load(connectionString, { + replicaDiscoveryEnabled: replicaDiscoveryEnabled, refreshOptions: { enabled: true, refreshIntervalInMs: 2000, @@ -159,6 +170,7 @@ describe("dynamic refresh", function () { it("should update values when watched setting is deleted", async () => { const connectionString = createMockedConnectionString(); const settings = await load(connectionString, { + replicaDiscoveryEnabled: replicaDiscoveryEnabled, refreshOptions: { enabled: true, refreshIntervalInMs: 2000, @@ -185,6 +197,7 @@ describe("dynamic refresh", function () { it("should not update values when unwatched setting changes", async () => { const connectionString = createMockedConnectionString(); const settings = await load(connectionString, { + replicaDiscoveryEnabled: replicaDiscoveryEnabled, refreshOptions: { enabled: true, refreshIntervalInMs: 2000, @@ -206,6 +219,7 @@ describe("dynamic refresh", function () { it("should watch multiple settings if specified", async () => { const connectionString = createMockedConnectionString(); const settings = await load(connectionString, { + replicaDiscoveryEnabled: replicaDiscoveryEnabled, refreshOptions: { enabled: true, refreshIntervalInMs: 2000, @@ -231,6 +245,7 @@ describe("dynamic refresh", function () { it("should execute callbacks on successful refresh", async () => { const connectionString = createMockedConnectionString(); const settings = await load(connectionString, { + replicaDiscoveryEnabled: replicaDiscoveryEnabled, refreshOptions: { enabled: true, refreshIntervalInMs: 2000, @@ -260,6 +275,7 @@ describe("dynamic refresh", function () { it("should not include watched settings into configuration if not specified in selectors", async () => { const connectionString = createMockedConnectionString(); const settings = await load(connectionString, { + replicaDiscoveryEnabled: replicaDiscoveryEnabled, selectors: [ { keyFilter: "app.settings.fontColor" } ], @@ -279,6 +295,7 @@ describe("dynamic refresh", function () { it("should refresh when watched setting is added", async () => { const connectionString = createMockedConnectionString(); const settings = await load(connectionString, { + replicaDiscoveryEnabled: replicaDiscoveryEnabled, refreshOptions: { enabled: true, refreshIntervalInMs: 2000, @@ -301,6 +318,7 @@ describe("dynamic refresh", function () { it("should not refresh when watched setting keeps not existing", async () => { const connectionString = createMockedConnectionString(); const settings = await load(connectionString, { + replicaDiscoveryEnabled: replicaDiscoveryEnabled, refreshOptions: { enabled: true, refreshIntervalInMs: 2000, @@ -342,6 +360,7 @@ describe("dynamic refresh feature flags", function () { const connectionString = createMockedConnectionString(); const settings = await load(connectionString, { + replicaDiscoveryEnabled: replicaDiscoveryEnabled, featureFlagOptions: { enabled: true, selectors: [{ @@ -392,6 +411,7 @@ describe("dynamic refresh feature flags", function () { const connectionString = createMockedConnectionString(); const settings = await load(connectionString, { + replicaDiscoveryEnabled: replicaDiscoveryEnabled, featureFlagOptions: { enabled: true, selectors: [{ diff --git a/test/requestTracing.test.ts b/test/requestTracing.test.ts index d4e7edcf..5da1ff80 100644 --- a/test/requestTracing.test.ts +++ b/test/requestTracing.test.ts @@ -25,6 +25,7 @@ class HttpRequestHeadersPolicy { describe("request tracing", function () { this.timeout(15000); + const replicaDiscoveryEnabled = false; const fakeEndpoint = "https://127.0.0.1"; // sufficient to test the request it sends out const headerPolicy = new HttpRequestHeadersPolicy(); const position: "perCall" | "perRetry" = "perCall"; @@ -46,7 +47,9 @@ describe("request tracing", function () { it("should have correct user agent prefix", async () => { try { - await load(createMockedConnectionString(fakeEndpoint), { clientOptions }); + await load(createMockedConnectionString(fakeEndpoint), { + replicaDiscoveryEnabled: replicaDiscoveryEnabled, + clientOptions }); } catch (e) { /* empty */ } expect(headerPolicy.headers).not.undefined; expect(headerPolicy.headers.get("User-Agent")).satisfy((ua: string) => ua.startsWith("javascript-appconfiguration-provider")); @@ -55,6 +58,7 @@ describe("request tracing", function () { it("should have request type in correlation-context header", async () => { try { await load(createMockedConnectionString(fakeEndpoint), { + replicaDiscoveryEnabled: replicaDiscoveryEnabled, clientOptions }); } catch (e) { /* empty */ } @@ -65,6 +69,7 @@ describe("request tracing", function () { it("should have key vault tag in correlation-context header", async () => { try { await load(createMockedConnectionString(fakeEndpoint), { + replicaDiscoveryEnabled: replicaDiscoveryEnabled, clientOptions, keyVaultOptions: { credential: createMockedTokenCredential() @@ -81,6 +86,7 @@ describe("request tracing", function () { process.env.NODE_ENV = "development"; try { await load(createMockedConnectionString(fakeEndpoint), { + replicaDiscoveryEnabled: replicaDiscoveryEnabled, clientOptions }); } catch (e) { /* empty */ } @@ -95,6 +101,7 @@ describe("request tracing", function () { process.env.WEBSITE_SITE_NAME = "website-name"; try { await load(createMockedConnectionString(fakeEndpoint), { + replicaDiscoveryEnabled: replicaDiscoveryEnabled, clientOptions }); } catch (e) { /* empty */ } @@ -110,6 +117,7 @@ describe("request tracing", function () { process.env.AZURE_APP_CONFIGURATION_TRACING_DISABLED = indicator; try { await load(createMockedConnectionString(fakeEndpoint), { + replicaDiscoveryEnabled: replicaDiscoveryEnabled, clientOptions }); } catch (e) { /* empty */ } @@ -129,6 +137,7 @@ describe("request tracing", function () { }].map(createMockedKeyValue)); const settings = await load(createMockedConnectionString(fakeEndpoint), { + replicaDiscoveryEnabled: replicaDiscoveryEnabled, clientOptions, refreshOptions: { enabled: true, @@ -187,7 +196,9 @@ describe("request tracing", function () { (global as any).importScripts = function importScripts() { }; try { - await load(createMockedConnectionString(fakeEndpoint), { clientOptions }); + await load(createMockedConnectionString(fakeEndpoint), { + replicaDiscoveryEnabled: replicaDiscoveryEnabled, + clientOptions }); } catch (e) { /* empty */ } expect(headerPolicy.headers).not.undefined; const correlationContext = headerPolicy.headers.get("Correlation-Context"); @@ -205,7 +216,9 @@ describe("request tracing", function () { (global as any).importScripts = function importScripts() { }; try { - await load(createMockedConnectionString(fakeEndpoint), { clientOptions }); + await load(createMockedConnectionString(fakeEndpoint), { + replicaDiscoveryEnabled: replicaDiscoveryEnabled, + clientOptions }); } catch (e) { /* empty */ } expect(headerPolicy.headers).not.undefined; const correlationContext = headerPolicy.headers.get("Correlation-Context"); @@ -223,7 +236,9 @@ describe("request tracing", function () { (global as any).importScripts = function importScripts() { }; try { - await load(createMockedConnectionString(fakeEndpoint), { clientOptions }); + await load(createMockedConnectionString(fakeEndpoint), { + replicaDiscoveryEnabled: replicaDiscoveryEnabled, + clientOptions }); } catch (e) { /* empty */ } expect(headerPolicy.headers).not.undefined; const correlationContext = headerPolicy.headers.get("Correlation-Context"); @@ -241,7 +256,9 @@ describe("request tracing", function () { (global as any).importScripts = function importScripts() { }; try { - await load(createMockedConnectionString(fakeEndpoint), { clientOptions }); + await load(createMockedConnectionString(fakeEndpoint), { + replicaDiscoveryEnabled: replicaDiscoveryEnabled, + clientOptions }); } catch (e) { /* empty */ } expect(headerPolicy.headers).not.undefined; const correlationContext = headerPolicy.headers.get("Correlation-Context"); @@ -259,7 +276,9 @@ describe("request tracing", function () { (global as any).importScripts = undefined; try { - await load(createMockedConnectionString(fakeEndpoint), { clientOptions }); + await load(createMockedConnectionString(fakeEndpoint), { + replicaDiscoveryEnabled: replicaDiscoveryEnabled, + clientOptions }); } catch (e) { /* empty */ } expect(headerPolicy.headers).not.undefined; const correlationContext = headerPolicy.headers.get("Correlation-Context"); @@ -297,7 +316,9 @@ describe("request tracing", function () { (global as any).document = new (global as any).Document(); try { - await load(createMockedConnectionString(fakeEndpoint), { clientOptions }); + await load(createMockedConnectionString(fakeEndpoint), { + replicaDiscoveryEnabled: replicaDiscoveryEnabled, + clientOptions }); } catch (e) { /* empty */ } expect(headerPolicy.headers).not.undefined; const correlationContext = headerPolicy.headers.get("Correlation-Context"); @@ -312,7 +333,9 @@ describe("request tracing", function () { (global as any).document = undefined; // not an instance of Document try { - await load(createMockedConnectionString(fakeEndpoint), { clientOptions }); + await load(createMockedConnectionString(fakeEndpoint), { + replicaDiscoveryEnabled: replicaDiscoveryEnabled, + clientOptions }); } catch (e) { /* empty */ } expect(headerPolicy.headers).not.undefined; const correlationContext = headerPolicy.headers.get("Correlation-Context"); @@ -327,7 +350,9 @@ describe("request tracing", function () { (global as any).document = {}; // Not an instance of Document try { - await load(createMockedConnectionString(fakeEndpoint), { clientOptions }); + await load(createMockedConnectionString(fakeEndpoint), { + replicaDiscoveryEnabled: replicaDiscoveryEnabled, + clientOptions }); } catch (e) { /* empty */ } expect(headerPolicy.headers).not.undefined; const correlationContext = headerPolicy.headers.get("Correlation-Context"); @@ -342,7 +367,9 @@ describe("request tracing", function () { (global as any).document = new (global as any).Document(); try { - await load(createMockedConnectionString(fakeEndpoint), { clientOptions }); + await load(createMockedConnectionString(fakeEndpoint), { + replicaDiscoveryEnabled: replicaDiscoveryEnabled, + clientOptions }); } catch (e) { /* empty */ } expect(headerPolicy.headers).not.undefined; const correlationContext = headerPolicy.headers.get("Correlation-Context"); @@ -357,7 +384,9 @@ describe("request tracing", function () { (global as any).document = new (global as any).Document(); try { - await load(createMockedConnectionString(fakeEndpoint), { clientOptions }); + await load(createMockedConnectionString(fakeEndpoint), { + replicaDiscoveryEnabled: replicaDiscoveryEnabled, + clientOptions }); } catch (e) { /* empty */ } expect(headerPolicy.headers).not.undefined; const correlationContext = headerPolicy.headers.get("Correlation-Context"); diff --git a/test/utils/testHelper.ts b/test/utils/testHelper.ts index 6e787dd7..6c635b3e 100644 --- a/test/utils/testHelper.ts +++ b/test/utils/testHelper.ts @@ -2,7 +2,7 @@ // Licensed under the MIT license. import * as sinon from "sinon"; -import { AppConfigurationClient, ConfigurationSetting } from "@azure/app-configuration"; +import { AppConfigurationClient, AppConfigurationClientOptions, ConfigurationSetting } from "@azure/app-configuration"; import { ClientSecretCredential } from "@azure/identity"; import { KeyVaultSecret, SecretClient } from "@azure/keyvault-secrets"; import * as uuid from "uuid"; @@ -10,6 +10,8 @@ import { RestError } from "@azure/core-rest-pipeline"; import { promisify } from "util"; const sleepInMs = promisify(setTimeout); import * as crypto from "crypto"; +import { ConfigurationClientManager } from "../../src/ConfigurationClientManager"; +import { ConfigurationClientWrapper } from "../../src/ConfigurationClientWrapper"; const TEST_CLIENT_ID = "00000000-0000-0000-0000-000000000000"; const TEST_TENANT_ID = "00000000-0000-0000-0000-000000000000"; @@ -94,6 +96,26 @@ function mockAppConfigurationClientListConfigurationSettings(...pages: Configura }); } +function mockConfigurationManagerGetClients(isFailoverable: boolean, clientOptions?: AppConfigurationClientOptions) { + // Stub the getClients method on the class prototype + sinon.stub(ConfigurationClientManager.prototype, "getClients").callsFake(async () => { + let clients: ConfigurationClientWrapper[] = []; + const fakeEndpoint = createMockedEndpoint("fake"); + const fakeStaticClientWrapper = new ConfigurationClientWrapper(fakeEndpoint, new AppConfigurationClient(createMockedConnectionString(fakeEndpoint), clientOptions)); + clients.push(fakeStaticClientWrapper); + + if (!isFailoverable) { + return clients; + } + + const fakeReplicaEndpoint = createMockedEndpoint(`fake-replica`); + const fakeDynamicClientWrapper = new ConfigurationClientWrapper(fakeReplicaEndpoint, new AppConfigurationClient(createMockedConnectionString(fakeReplicaEndpoint), clientOptions)); + clients.push(fakeDynamicClientWrapper); + + return clients; + }); +} + function mockAppConfigurationClientGetConfigurationSetting(kvList) { sinon.stub(AppConfigurationClient.prototype, "getConfigurationSetting").callsFake((settingId, options) => { const found = kvList.find(elem => elem.key === settingId.key && elem.label === settingId.label); @@ -198,6 +220,7 @@ export { sinon, mockAppConfigurationClientListConfigurationSettings, mockAppConfigurationClientGetConfigurationSetting, + mockConfigurationManagerGetClients, mockSecretClientGetSecret, restoreMocks, @@ -210,4 +233,4 @@ export { createMockedFeatureFlag, sleepInMs -}; +} From 19edd021a5e94c96b900f57ded79b211fb2bae1d Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Tue, 8 Oct 2024 13:25:09 +0800 Subject: [PATCH 03/24] resolve conflicts --- src/AzureAppConfigurationImpl.ts | 8 ++---- src/AzureAppConfigurationOptions.ts | 2 +- src/ConfigurationClientManager.ts | 44 +++++++++++++++-------------- src/ConfigurationClientWrapper.ts | 2 +- test/utils/testHelper.ts | 6 ++-- 5 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index b600f527..173c4ccf 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -83,7 +83,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { constructor( clientManager: ConfigurationClientManager, - options: AzureAppConfigurationOptions | undefined, + options: AzureAppConfigurationOptions | undefined, ) { this.#options = options; this.#clientManager = clientManager; @@ -344,7 +344,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { } } } - selector.pageEtags = pageEtags; + selector.pageEtags = pageEtags; } this.#featureFlagSelectors = selectors; @@ -528,10 +528,8 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { } return needRefresh; }; - - let needRefresh: boolean; - needRefresh = await this.#executeWithFailoverPolicy(funcToExecute); + const needRefresh: boolean = await this.#executeWithFailoverPolicy(funcToExecute); if (needRefresh) { try { await this.#loadFeatureFlags(); diff --git a/src/AzureAppConfigurationOptions.ts b/src/AzureAppConfigurationOptions.ts index d4e7cdde..9cef1102 100644 --- a/src/AzureAppConfigurationOptions.ts +++ b/src/AzureAppConfigurationOptions.ts @@ -13,7 +13,7 @@ export const MaxRetryDelayInMs = 60000; export interface AzureAppConfigurationOptions { /** * Specifies whether enable replica discovery or not. - * + * * @remarks * If not specified, the default value is true. */ diff --git a/src/ConfigurationClientManager.ts b/src/ConfigurationClientManager.ts index 1e5a8c57..84ff6ac6 100644 --- a/src/ConfigurationClientManager.ts +++ b/src/ConfigurationClientManager.ts @@ -2,7 +2,7 @@ // Licensed under the MIT license. import { AppConfigurationClient, AppConfigurationClientOptions } from "@azure/app-configuration"; -import { ConfigurationClientWrapper } from "./ConfigurationClientWrapper" +import { ConfigurationClientWrapper } from "./ConfigurationClientWrapper"; import { TokenCredential } from "@azure/identity"; import { AzureAppConfigurationOptions, MaxRetries, MaxRetryDelayInMs } from "./AzureAppConfigurationOptions"; import { isFailoverableEnv } from "./requestTracing/utils"; @@ -14,18 +14,18 @@ const TCP = "_tcp"; const EndpointSection = "Endpoint"; const IdSection = "Id"; const SecretSection = "Secret"; -const AzConfigDomainLabel = ".azconfig." -const AppConfigDomainLabel = ".appconfig." +const AzConfigDomainLabel = ".azconfig."; +const AppConfigDomainLabel = ".appconfig."; const FallbackClientRefreshExpireInterval = 60 * 60 * 1000; // 1 hour in milliseconds const MinimalClientRefreshInterval = 30 * 1000; // 30 seconds in milliseconds const SrvQueryTimeout = 5000; // 5 seconds interface IConfigurationClientManager { - getClients(): ConfigurationClientWrapper[]; + getClients(): Promise; refreshClients(): Promise; } -export class ConfigurationClientManager { +export class ConfigurationClientManager implements IConfigurationClientManager { isFailoverable: boolean; #endpoint: string; #secret : string; @@ -55,7 +55,7 @@ export class ConfigurationClientManager { this.#id = parseConnectionString(connectionString, IdSection); // TODO: need to check if it's CDN or not this.#endpoint = parseConnectionString(connectionString, EndpointSection); - + } else if (connectionStringOrEndpoint instanceof URL) { const credential = credentialOrOptions as TokenCredential; options = appConfigOptions as AzureAppConfigurationOptions; @@ -69,7 +69,7 @@ export class ConfigurationClientManager { this.#staticClients = [new ConfigurationClientWrapper(this.#endpoint, staticClient)]; this.#validDomain = getValidDomain(this.#endpoint); - this.isFailoverable = (options?.replicaDiscoveryEnabled ?? true) && isFailoverableEnv(); + this.isFailoverable = (options?.replicaDiscoveryEnabled ?? true) && isFailoverableEnv(); } async getClients() { @@ -93,7 +93,7 @@ export class ConfigurationClientManager { .filter(client => client.backoffEndTime <= currentTime)); } - return availableClients + return availableClients; } async refreshClients() { @@ -155,7 +155,7 @@ export class ConfigurationClientManager { #isFallbackClientDiscoveryDue(dateTime) { return dateTime >= this.#lastFallbackClientRefreshAttempt + MinimalClientRefreshInterval - && (!this.#dynamicClients + && (!this.#dynamicClients || this.#dynamicClients.every(client => dateTime < client.backoffEndTime) || dateTime >= this.#lastFallbackClientRefreshTime + FallbackClientRefreshExpireInterval); } @@ -171,7 +171,7 @@ async function querySrvTargetHost(host) { let dns; if (isFailoverableEnv()) { - dns = require('dns/promises'); + dns = require("dns/promises"); } else { return results; } @@ -184,16 +184,18 @@ async function querySrvTargetHost(host) { } // Add the first origin record to results - const originHost = originRecords[0].name + const originHost = originRecords[0].name; results.push(originHost); - + // Look up SRV records for alternate hosts let index = 0; - while (true) { + let moreAltRecordsExist = true; + while (moreAltRecordsExist) { const currentAlt = `${ALT}${index}`; try { const altRecords = await dns.resolveSrv(`${currentAlt}.${TCP}.${originHost}`); if (altRecords.length === 0) { + moreAltRecordsExist = false; break; // No more alternate records, exit loop } @@ -205,7 +207,7 @@ async function querySrvTargetHost(host) { }); index++; } catch (err) { - if (err.code === 'ENOTFOUND') { + if (err.code === "ENOTFOUND") { break; // No more alternate records, exit loop } else { throw new Error(`Failed to lookup alternate SRV records: ${err.message}`); @@ -221,7 +223,7 @@ async function querySrvTargetHost(host) { /** * Parses the connection string to extract the value associated with a specific token. - * + * * @param {string} connectionString - The connection string containing tokens. * @param {string} token - The token whose value needs to be extracted. * @returns {string} The value associated with the token, or an empty string if not found. @@ -241,7 +243,7 @@ function parseConnectionString(connectionString, token) { // Move startIndex to the beginning of the token value const valueStartIndex = startIndex + searchToken.length; - const endIndex = connectionString.indexOf(';', valueStartIndex); + const endIndex = connectionString.indexOf(";", valueStartIndex); const valueEndIndex = endIndex === -1 ? connectionString.length : endIndex; // Extract and return the token value @@ -251,7 +253,7 @@ function parseConnectionString(connectionString, token) { /** * Builds a connection string from the given endpoint, secret, and id. * Returns an empty string if either secret or id is empty. - * + * * @param {string} endpoint - The endpoint to include in the connection string. * @param {string} secret - The secret to include in the connection string. * @param {string} id - The ID to include in the connection string. @@ -259,7 +261,7 @@ function parseConnectionString(connectionString, token) { */ function buildConnectionString(endpoint, secret, id) { if (!secret || !id) { - return ''; + return ""; } return `${EndpointSection}=${endpoint};${IdSection}=${id};${SecretSection}=${secret}`; @@ -276,7 +278,7 @@ function getValidDomain(endpoint) { const url = new URL(endpoint); const trustedDomainLabels = [AzConfigDomainLabel, AppConfigDomainLabel]; const host = url.hostname.toLowerCase(); - + for (const label of trustedDomainLabels) { const index = host.lastIndexOf(label); if (index !== -1) { @@ -292,7 +294,7 @@ function getValidDomain(endpoint) { /** * Checks if the given host ends with the valid domain. - * + * * @param {string} host - The host to be validated. * @param {string} validDomain - The valid domain to check against. * @returns {boolean} - True if the host ends with the valid domain, false otherwise. @@ -317,7 +319,7 @@ export function getClientOptions(options?: AzureAppConfigurationOptions): AppCon const defaultRetryOptions = { maxRetries: MaxRetries, maxRetryDelayInMs: MaxRetryDelayInMs, - } + }; const retryOptions = Object.assign({}, defaultRetryOptions, options?.clientOptions?.retryOptions); return Object.assign({}, options?.clientOptions, { diff --git a/src/ConfigurationClientWrapper.ts b/src/ConfigurationClientWrapper.ts index a7a4b9e5..d7ad7830 100644 --- a/src/ConfigurationClientWrapper.ts +++ b/src/ConfigurationClientWrapper.ts @@ -46,4 +46,4 @@ export function calculateBackoffDuration(failedAttempts: number) { const jitter = JITTER_RATIO * (Math.random() * 2 - 1); return calculatedBackoffDuration * (1 + jitter); -} \ No newline at end of file +} diff --git a/test/utils/testHelper.ts b/test/utils/testHelper.ts index 6c635b3e..3ff2e3af 100644 --- a/test/utils/testHelper.ts +++ b/test/utils/testHelper.ts @@ -99,7 +99,7 @@ function mockAppConfigurationClientListConfigurationSettings(...pages: Configura function mockConfigurationManagerGetClients(isFailoverable: boolean, clientOptions?: AppConfigurationClientOptions) { // Stub the getClients method on the class prototype sinon.stub(ConfigurationClientManager.prototype, "getClients").callsFake(async () => { - let clients: ConfigurationClientWrapper[] = []; + const clients: ConfigurationClientWrapper[] = []; const fakeEndpoint = createMockedEndpoint("fake"); const fakeStaticClientWrapper = new ConfigurationClientWrapper(fakeEndpoint, new AppConfigurationClient(createMockedConnectionString(fakeEndpoint), clientOptions)); clients.push(fakeStaticClientWrapper); @@ -108,7 +108,7 @@ function mockConfigurationManagerGetClients(isFailoverable: boolean, clientOptio return clients; } - const fakeReplicaEndpoint = createMockedEndpoint(`fake-replica`); + const fakeReplicaEndpoint = createMockedEndpoint("fake-replica"); const fakeDynamicClientWrapper = new ConfigurationClientWrapper(fakeReplicaEndpoint, new AppConfigurationClient(createMockedConnectionString(fakeReplicaEndpoint), clientOptions)); clients.push(fakeDynamicClientWrapper); @@ -233,4 +233,4 @@ export { createMockedFeatureFlag, sleepInMs -} +}; From ac4ab59b46ca77c488a97a27a9ca765c8aeb43cf Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Thu, 10 Oct 2024 14:33:45 +0800 Subject: [PATCH 04/24] add tests --- src/ConfigurationClientManager.ts | 4 +- test/failover.test.ts | 118 ++++++++++++++++++++++++++++++ test/utils/testHelper.ts | 52 +++++++++++++ 3 files changed, 172 insertions(+), 2 deletions(-) create mode 100644 test/failover.test.ts diff --git a/src/ConfigurationClientManager.ts b/src/ConfigurationClientManager.ts index 84ff6ac6..355f0268 100644 --- a/src/ConfigurationClientManager.ts +++ b/src/ConfigurationClientManager.ts @@ -273,7 +273,7 @@ function buildConnectionString(endpoint, secret, id) { * @param {string} endpoint - The endpoint URL. * @returns {string} - The valid domain or an empty string if no valid domain is found. */ -function getValidDomain(endpoint) { +export function getValidDomain(endpoint) { try { const url = new URL(endpoint); const trustedDomainLabels = [AzConfigDomainLabel, AppConfigDomainLabel]; @@ -299,7 +299,7 @@ function getValidDomain(endpoint) { * @param {string} validDomain - The valid domain to check against. * @returns {boolean} - True if the host ends with the valid domain, false otherwise. */ -function isValidEndpoint(host, validDomain) { +export function isValidEndpoint(host, validDomain) { if (!validDomain) { return false; } diff --git a/test/failover.test.ts b/test/failover.test.ts new file mode 100644 index 00000000..b5e97fcd --- /dev/null +++ b/test/failover.test.ts @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import * as chai from "chai"; +import * as chaiAsPromised from "chai-as-promised"; +chai.use(chaiAsPromised); +const expect = chai.expect; +import { load } from "./exportedApi"; +import { createMockedConnectionString, createMockedEndpoint, createMockedFeatureFlag, createMockedKeyValue, mockAppConfigurationClientListConfigurationSettingsWithFailure, mockConfigurationManagerGetClients, restoreMocks } from "./utils/testHelper"; +import { getValidDomain, isValidEndpoint } from "../src/ConfigurationClientManager"; + +const mockedKVs = [{ + key: "app.settings.fontColor", + value: "red", +}, { + key: "app.settings.fontSize", + value: "40", +}].map(createMockedKeyValue); + +const mockedFeatureFlags = [{ + key: "app.settings.fontColor", + value: "red", +}].map(createMockedKeyValue).concat([ + createMockedFeatureFlag("Beta", { enabled: true }), + createMockedFeatureFlag("Alpha_1", { enabled: true }), + createMockedFeatureFlag("Alpha_2", { enabled: false }), +]); + +describe("failover", function () { + this.timeout(15000); + + afterEach(() => { + restoreMocks(); + }); + + it("should failover to replica and load key values from config store", async () => { + const replicaDiscoveryEnabled = true; + const isFailoverable = true; + mockConfigurationManagerGetClients(isFailoverable); + mockAppConfigurationClientListConfigurationSettingsWithFailure(mockedKVs); + + const connectionString = createMockedConnectionString(); + const settings = await load(connectionString, { + replicaDiscoveryEnabled: replicaDiscoveryEnabled + }); + expect(settings).not.undefined; + expect(settings.get("app.settings.fontColor")).eq("red"); + expect(settings.get("app.settings.fontSize")).eq("40"); + }); + + it("should failover to replica and load feature flags from config store", async () => { + const replicaDiscoveryEnabled = true; + const isFailoverable = true; + mockConfigurationManagerGetClients(isFailoverable); + mockAppConfigurationClientListConfigurationSettingsWithFailure(mockedFeatureFlags); + + const connectionString = createMockedConnectionString(); + const settings = await load(connectionString, { + replicaDiscoveryEnabled: replicaDiscoveryEnabled, + featureFlagOptions: { + enabled: true, + selectors: [{ + keyFilter: "*" + }] + } + }); + expect(settings).not.undefined; + expect(settings.get("feature_management")).not.undefined; + expect(settings.get("feature_management").feature_flags).not.undefined; + }); + + it("should throw error when all clients failed", async () => { + const isFailoverable = false; + mockConfigurationManagerGetClients(isFailoverable); + mockAppConfigurationClientListConfigurationSettingsWithFailure(mockedKVs); + + const connectionString = createMockedConnectionString(); + return expect(load(connectionString)).eventually.rejectedWith("All app configuration clients failed to get settings."); + }); + + it("should validate endpoint", () => { + const fakeEndpoint = createMockedEndpoint("fake"); + const validDomain = getValidDomain(fakeEndpoint); + + expect(isValidEndpoint("azure.azconfig.io", validDomain)).to.be.true; + expect(isValidEndpoint("azure.privatelink.azconfig.io", validDomain)).to.be.true; + expect(isValidEndpoint("azure-replica.azconfig.io", validDomain)).to.be.true; + expect(isValidEndpoint("azure.badazconfig.io", validDomain)).to.be.false; + expect(isValidEndpoint("azure.azconfigbad.io", validDomain)).to.be.false; + expect(isValidEndpoint("azure.appconfig.azure.com", validDomain)).to.be.false; + expect(isValidEndpoint("azure.azconfig.bad.io", validDomain)).to.be.false; + + const fakeEndpoint2 = "https://foobar.appconfig.azure.com"; + const validDomain2 = getValidDomain(fakeEndpoint2); + + expect(isValidEndpoint("azure.appconfig.azure.com", validDomain2)).to.be.true; + expect(isValidEndpoint("azure.z1.appconfig.azure.com", validDomain2)).to.be.true; + expect(isValidEndpoint("azure-replia.z1.appconfig.azure.com", validDomain2)).to.be.true; // Note: Typo "azure-replia" + expect(isValidEndpoint("azure.privatelink.appconfig.azure.com", validDomain2)).to.be.true; + expect(isValidEndpoint("azconfig.appconfig.azure.com", validDomain2)).to.be.true; + expect(isValidEndpoint("azure.azconfig.io", validDomain2)).to.be.false; + expect(isValidEndpoint("azure.badappconfig.azure.com", validDomain2)).to.be.false; + expect(isValidEndpoint("azure.appconfigbad.azure.com", validDomain2)).to.be.false; + + const fakeEndpoint3 = "https://foobar.azconfig-test.io"; + const validDomain3 = getValidDomain(fakeEndpoint3); + + expect(isValidEndpoint("azure.azconfig-test.io", validDomain3)).to.be.false; + expect(isValidEndpoint("azure.azconfig.io", validDomain3)).to.be.false; + + const fakeEndpoint4 = "https://foobar.z1.appconfig-test.azure.com"; + const validDomain4 = getValidDomain(fakeEndpoint4); + + expect(isValidEndpoint("foobar.z2.appconfig-test.azure.com", validDomain4)).to.be.false; + expect(isValidEndpoint("foobar.appconfig-test.azure.com", validDomain4)).to.be.false; + expect(isValidEndpoint("foobar.appconfig.azure.com", validDomain4)).to.be.false; + }); +}); diff --git a/test/utils/testHelper.ts b/test/utils/testHelper.ts index 3ff2e3af..1695b868 100644 --- a/test/utils/testHelper.ts +++ b/test/utils/testHelper.ts @@ -131,6 +131,57 @@ function mockAppConfigurationClientGetConfigurationSetting(kvList) { }); } +function mockAppConfigurationClientListConfigurationSettingsWithFailure(...pages: ConfigurationSetting[][]) { + const stub = sinon.stub(AppConfigurationClient.prototype, "listConfigurationSettings"); + + // Configure the stub to throw an error on the first call and return mockedKVs on the second call + stub.onFirstCall().throws(new RestError("Internal Server Error", { statusCode: 500 })); + stub.callsFake((listOptions) => { + let kvs = _filterKVs(pages.flat(), listOptions); + const mockIterator: AsyncIterableIterator & { byPage(): AsyncIterableIterator } = { + [Symbol.asyncIterator](): AsyncIterableIterator { + kvs = _filterKVs(pages.flat(), listOptions); + return this; + }, + next() { + const value = kvs.shift(); + return Promise.resolve({ done: !value, value }); + }, + byPage(): AsyncIterableIterator { + let remainingPages; + const pageEtags = listOptions?.pageEtags ? [...listOptions.pageEtags] : undefined; // a copy of the original list + return { + [Symbol.asyncIterator](): AsyncIterableIterator { + remainingPages = [...pages]; + return this; + }, + next() { + const pageItems = remainingPages.shift(); + const pageEtag = pageEtags?.shift(); + if (pageItems === undefined) { + return Promise.resolve({ done: true, value: undefined }); + } else { + const items = _filterKVs(pageItems ?? [], listOptions); + const etag = _sha256(JSON.stringify(items)); + const statusCode = pageEtag === etag ? 304 : 200; + return Promise.resolve({ + done: false, + value: { + items, + etag, + _response: { status: statusCode } + } + }); + } + } + }; + } + }; + + return mockIterator as any; + }); +} + // uriValueList: [["", "value"], ...] function mockSecretClientGetSecret(uriValueList: [string, string][]) { const dict = new Map(); @@ -220,6 +271,7 @@ export { sinon, mockAppConfigurationClientListConfigurationSettings, mockAppConfigurationClientGetConfigurationSetting, + mockAppConfigurationClientListConfigurationSettingsWithFailure, mockConfigurationManagerGetClients, mockSecretClientGetSecret, restoreMocks, From 25dd2685af0a4fcb4ba2d5dcc0cd38e698dae588 Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Thu, 17 Oct 2024 14:51:55 +0800 Subject: [PATCH 05/24] resolve conflicts and update --- src/AzureAppConfigurationImpl.ts | 4 +--- src/ConfigurationClientManager.ts | 16 ++++++++-------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index 173c4ccf..759a57af 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -59,8 +59,6 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { #sortedTrimKeyPrefixes: string[] | undefined; readonly #requestTracingEnabled: boolean; #clientManager: ConfigurationClientManager; - #client: AppConfigurationClient; - #clientEndpoint: string | undefined; #options: AzureAppConfigurationOptions | undefined; #isInitialLoadCompleted: boolean = false; #isFailoverRequest: boolean = false; @@ -689,7 +687,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { } #createFeatureFlagReference(setting: ConfigurationSetting): string { - let featureFlagReference = `${this.#clientEndpoint}kv/${setting.key}`; + let featureFlagReference = `${this.#clientManager.endpoint}/kv/${setting.key}`; if (setting.label && setting.label.trim().length !== 0) { featureFlagReference += `?label=${setting.label}`; } diff --git a/src/ConfigurationClientManager.ts b/src/ConfigurationClientManager.ts index 355f0268..fc5ad553 100644 --- a/src/ConfigurationClientManager.ts +++ b/src/ConfigurationClientManager.ts @@ -27,7 +27,7 @@ interface IConfigurationClientManager { export class ConfigurationClientManager implements IConfigurationClientManager { isFailoverable: boolean; - #endpoint: string; + endpoint: string; #secret : string; #id : string; #credential: TokenCredential; @@ -54,21 +54,21 @@ export class ConfigurationClientManager implements IConfigurationClientManager { this.#secret = parseConnectionString(connectionString, SecretSection); this.#id = parseConnectionString(connectionString, IdSection); // TODO: need to check if it's CDN or not - this.#endpoint = parseConnectionString(connectionString, EndpointSection); + this.endpoint = parseConnectionString(connectionString, EndpointSection); } else if (connectionStringOrEndpoint instanceof URL) { const credential = credentialOrOptions as TokenCredential; options = appConfigOptions as AzureAppConfigurationOptions; this.#clientOptions = getClientOptions(options); staticClient = new AppConfigurationClient(connectionStringOrEndpoint.toString(), credential, this.#clientOptions); - this.#endpoint = connectionStringOrEndpoint.toString(); + this.endpoint = connectionStringOrEndpoint.toString(); this.#credential = credential; } else { throw new Error("A connection string or an endpoint with credential must be specified to create a client."); } - this.#staticClients = [new ConfigurationClientWrapper(this.#endpoint, staticClient)]; - this.#validDomain = getValidDomain(this.#endpoint); + this.#staticClients = [new ConfigurationClientWrapper(this.endpoint, staticClient)]; + this.#validDomain = getValidDomain(this.endpoint); this.isFailoverable = (options?.replicaDiscoveryEnabled ?? true) && isFailoverableEnv(); } @@ -80,7 +80,7 @@ export class ConfigurationClientManager implements IConfigurationClientManager { const currentTime = Date.now(); if (this.#isFallbackClientDiscoveryDue(currentTime)) { this.#lastFallbackClientRefreshAttempt = currentTime; - const host = new URL(this.#endpoint).hostname; + const host = new URL(this.endpoint).hostname; await this.#discoverFallbackClients(host); } @@ -101,7 +101,7 @@ export class ConfigurationClientManager implements IConfigurationClientManager { if (this.isFailoverable && currentTime > new Date(this.#lastFallbackClientRefreshAttempt + MinimalClientRefreshInterval).getTime()) { this.#lastFallbackClientRefreshAttempt = currentTime; - const url = new URL(this.#endpoint); + const url = new URL(this.endpoint); await this.#discoverFallbackClients(url.hostname); } } @@ -129,7 +129,7 @@ export class ConfigurationClientManager implements IConfigurationClientManager { for (const host of srvTargetHosts) { if (isValidEndpoint(host, this.#validDomain)) { const targetEndpoint = `https://${host}`; - if (targetEndpoint.toLowerCase() === this.#endpoint.toLowerCase()) { + if (targetEndpoint.toLowerCase() === this.endpoint.toLowerCase()) { continue; } const client = this.#newConfigurationClient(targetEndpoint); From 5c9b423b7136e08cd0a92defcec78ad698f46111 Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Thu, 17 Oct 2024 14:53:42 +0800 Subject: [PATCH 06/24] fix lint --- src/AzureAppConfigurationImpl.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index 759a57af..595c7574 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { AppConfigurationClient, ConfigurationSetting, ConfigurationSettingId, GetConfigurationSettingOptions, GetConfigurationSettingResponse, ListConfigurationSettingsOptions, featureFlagPrefix, isFeatureFlag } from "@azure/app-configuration"; +import { ConfigurationSetting, ConfigurationSettingId, GetConfigurationSettingOptions, GetConfigurationSettingResponse, ListConfigurationSettingsOptions, featureFlagPrefix, isFeatureFlag } from "@azure/app-configuration"; import { isRestError } from "@azure/core-rest-pipeline"; import { AzureAppConfiguration, ConfigurationObjectConstructionOptions } from "./AzureAppConfiguration.js"; import { AzureAppConfigurationOptions } from "./AzureAppConfigurationOptions.js"; @@ -347,7 +347,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { this.#featureFlagSelectors = selectors; return featureFlagSettings; - } + }; let featureFlagSettings: ConfigurationSetting[] = []; featureFlagSettings = await this.#executeWithFailoverPolicy(funcToExecute); From 0b8a8f6382916ded1216de9d4a4e4f95fe031526 Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Fri, 25 Oct 2024 11:01:38 +0800 Subject: [PATCH 07/24] resolve conflicts --- package-lock.json | 2592 +++++++++++++++++++++++++++++ src/ConfigurationClientManager.ts | 22 +- 2 files changed, 2605 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 498f59e1..0bc3bc29 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2524,6 +2524,7 @@ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -2869,6 +2870,7 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", "dev": true, + "license": "MIT", "dependencies": { "isarray": "0.0.1" } @@ -3085,6 +3087,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", "dev": true, + "license": "MIT", "bin": { "rollup": "dist/bin/rollup" }, @@ -3591,5 +3594,2594 @@ "url": "https://github.com/sponsors/sindresorhus" } } + }, + "dependencies": { + "@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true + }, + "@azure/abort-controller": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", + "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", + "requires": { + "tslib": "^2.2.0" + } + }, + "@azure/app-configuration": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@azure/app-configuration/-/app-configuration-1.6.1.tgz", + "integrity": "sha512-pk8zyG/8Nc6VN7uDA9QY19UFhTXneUbnB+5IcW9uuPyVDXU17TcXBI4xY1ZBm7hmhn0yh3CeZK4kOxa/tjsMqQ==", + "requires": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-client": "^1.5.0", + "@azure/core-http-compat": "^2.0.0", + "@azure/core-lro": "^2.5.1", + "@azure/core-paging": "^1.4.0", + "@azure/core-rest-pipeline": "^1.6.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.6.1", + "@azure/logger": "^1.0.0", + "tslib": "^2.2.0" + }, + "dependencies": { + "@azure/core-http-compat": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-2.0.1.tgz", + "integrity": "sha512-xpQZz/q7E0jSW4rckrTo2mDFDQgo6I69hBU4voMQi7REi6JRW5a+KfVkbJCFCWnkFmP6cAJ0IbuudTdf/MEBOQ==", + "requires": { + "@azure/abort-controller": "^1.0.4", + "@azure/core-client": "^1.3.0", + "@azure/core-rest-pipeline": "^1.3.0" + } + } + } + }, + "@azure/core-auth": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.5.0.tgz", + "integrity": "sha512-udzoBuYG1VBoHVohDTrvKjyzel34zt77Bhp7dQntVGGD0ehVq48owENbBG8fIgkHRNUBQH5k1r0hpoMu5L8+kw==", + "requires": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-util": "^1.1.0", + "tslib": "^2.2.0" + } + }, + "@azure/core-client": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.7.3.tgz", + "integrity": "sha512-kleJ1iUTxcO32Y06dH9Pfi9K4U+Tlb111WXEnbt7R/ne+NLRwppZiTGJuTD5VVoxTMK5NTbEtm5t2vcdNCFe2g==", + "requires": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.4.0", + "@azure/core-rest-pipeline": "^1.9.1", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.0.0", + "@azure/logger": "^1.0.0", + "tslib": "^2.2.0" + } + }, + "@azure/core-http-compat": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-1.3.0.tgz", + "integrity": "sha512-ZN9avruqbQ5TxopzG3ih3KRy52n8OAbitX3fnZT5go4hzu0J+KVPSzkL+Wt3hpJpdG8WIfg1sBD1tWkgUdEpBA==", + "requires": { + "@azure/abort-controller": "^1.0.4", + "@azure/core-client": "^1.3.0", + "@azure/core-rest-pipeline": "^1.3.0" + } + }, + "@azure/core-lro": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.5.4.tgz", + "integrity": "sha512-3GJiMVH7/10bulzOKGrrLeG/uCBH/9VtxqaMcB9lIqAeamI/xYQSHJL/KcsLDuH+yTjYpro/u6D/MuRe4dN70Q==", + "requires": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-util": "^1.2.0", + "@azure/logger": "^1.0.0", + "tslib": "^2.2.0" + } + }, + "@azure/core-paging": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.5.0.tgz", + "integrity": "sha512-zqWdVIt+2Z+3wqxEOGzR5hXFZ8MGKK52x4vFLw8n58pR6ZfKRx3EXYTxTaYxYHc/PexPUTyimcTWFJbji9Z6Iw==", + "requires": { + "tslib": "^2.2.0" + } + }, + "@azure/core-rest-pipeline": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.12.2.tgz", + "integrity": "sha512-wLLJQdL4v1yoqYtEtjKNjf8pJ/G/BqVomAWxcKOR1KbZJyCEnCv04yks7Y1NhJ3JzxbDs307W67uX0JzklFdCg==", + "requires": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.4.0", + "@azure/core-tracing": "^1.0.1", + "@azure/core-util": "^1.3.0", + "@azure/logger": "^1.0.0", + "form-data": "^4.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "tslib": "^2.2.0" + } + }, + "@azure/core-tracing": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.1.tgz", + "integrity": "sha512-I5CGMoLtX+pI17ZdiFJZgxMJApsK6jjfm85hpgp3oazCdq5Wxgh4wMr7ge/TTWW1B5WBuvIOI1fMU/FrOAMKrw==", + "requires": { + "tslib": "^2.2.0" + } + }, + "@azure/core-util": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.9.0.tgz", + "integrity": "sha512-AfalUQ1ZppaKuxPPMsFEUdX6GZPB3d9paR9d/TTL7Ow2De8cJaC7ibi7kWVlFAVPCYo31OcnGymc0R89DX8Oaw==", + "requires": { + "@azure/abort-controller": "^2.0.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@azure/identity": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.2.1.tgz", + "integrity": "sha512-U8hsyC9YPcEIzoaObJlRDvp7KiF0MGS7xcWbyJSVvXRkC/HXo1f0oYeBYmEvVgRfacw7GHf6D6yAoh9JHz6A5Q==", + "requires": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.5.0", + "@azure/core-client": "^1.4.0", + "@azure/core-rest-pipeline": "^1.1.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.3.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^3.11.1", + "@azure/msal-node": "^2.9.2", + "events": "^3.0.0", + "jws": "^4.0.0", + "open": "^8.0.0", + "stoppable": "^1.1.0", + "tslib": "^2.2.0" + } + }, + "@azure/keyvault-secrets": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@azure/keyvault-secrets/-/keyvault-secrets-4.7.0.tgz", + "integrity": "sha512-YvlFXRQ+SI5NT4GtSFbb6HGo6prW3yzDab8tr6vga2/SjDQew3wJsCAAr/xwZz6XshFXCYEX26CDKmPf+SJKJg==", + "requires": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-client": "^1.5.0", + "@azure/core-http-compat": "^1.3.0", + "@azure/core-lro": "^2.2.0", + "@azure/core-paging": "^1.1.1", + "@azure/core-rest-pipeline": "^1.8.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.0.0", + "@azure/logger": "^1.0.0", + "tslib": "^2.2.0" + } + }, + "@azure/logger": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.0.4.tgz", + "integrity": "sha512-ustrPY8MryhloQj7OWGe+HrYx+aoiOxzbXTtgblbV3xwCqpzUK36phH3XNHQKj3EPonyFUuDTfR3qFhTEAuZEg==", + "requires": { + "tslib": "^2.2.0" + } + }, + "@azure/msal-browser": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-3.13.0.tgz", + "integrity": "sha512-fD906nmJei3yE7la6DZTdUtXKvpwzJURkfsiz9747Icv4pit77cegSm6prJTKLQ1fw4iiZzrrWwxnhMLrTf5gQ==", + "requires": { + "@azure/msal-common": "14.9.0" + } + }, + "@azure/msal-common": { + "version": "14.9.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.9.0.tgz", + "integrity": "sha512-yzBPRlWPnTBeixxLNI3BBIgF5/bHpbhoRVuuDBnYjCyWRavaPUsKAHUDYLqpGkBLDciA6TCc6GOxN4/S3WiSxg==" + }, + "@azure/msal-node": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.9.2.tgz", + "integrity": "sha512-8tvi6Cos3m+0KmRbPjgkySXi+UQU/QiuVRFnrxIwt5xZlEEFa69O04RTaNESGgImyBBlYbo2mfE8/U8Bbdk1WQ==", + "requires": { + "@azure/msal-common": "14.12.0", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + }, + "dependencies": { + "@azure/msal-common": { + "version": "14.12.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.12.0.tgz", + "integrity": "sha512-IDDXmzfdwmDkv4SSmMEyAniJf6fDu3FJ7ncOjlxkDuT85uSnLEhZi3fGZpoR7T4XZpOMx9teM9GXBgrfJgyeBw==" + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + } + } + }, + "@babel/code-frame": { + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "dev": true, + "optional": true, + "requires": { + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "optional": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "optional": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "optional": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "optional": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "optional": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "optional": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "optional": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true, + "optional": true + }, + "@babel/highlight": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "dev": true, + "optional": true, + "requires": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "optional": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "optional": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "optional": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "optional": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "optional": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "optional": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "optional": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^3.3.0" + } + }, + "@eslint-community/regexpp": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.9.1.tgz", + "integrity": "sha512-Y27x+MBLjXa+0JWDhykM3+JE+il3kHKAEqabfEWq3SDhZjLYb6/BHL/JKFnH3fe207JaXkyDo685Oc2Glt6ifA==", + "dev": true + }, + "@eslint/eslintrc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", + "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + } + }, + "@eslint/js": { + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.51.0.tgz", + "integrity": "sha512-HxjQ8Qn+4SI3/AFv6sOrDB+g6PpUTDwSJiQqOrnneEk8L71161srI9gjzzZvYVbzHiVg/BvcH95+cK/zfIt4pg==", + "dev": true + }, + "@humanwhocodes/config-array": { + "version": "0.11.12", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.12.tgz", + "integrity": "sha512-NlGesA1usRNn6ctHCZ21M4/dKPgW9Nn1FypRdIKKgZOKzkVV4T1FlK5mBiLhHBCDmEbdQG0idrcXlbZfksJ+RA==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^2.0.0", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + } + }, + "@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true + }, + "@humanwhocodes/object-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.0.tgz", + "integrity": "sha512-9S9QrXY2K0L4AGDcSgTi9vgiCcG8VcBv4Mp7/1hDPYoswIy6Z6KO5blYto82BT8M0MZNRWmCFLpCs3HlpYGGdw==", + "dev": true + }, + "@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "requires": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true + }, + "ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true + }, + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "requires": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + } + }, + "strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "requires": { + "ansi-regex": "^6.0.1" + } + }, + "wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "requires": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + } + } + } + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true + }, + "@rollup/plugin-typescript": { + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-11.1.5.tgz", + "integrity": "sha512-rnMHrGBB0IUEv69Q8/JGRD/n4/n6b3nfpufUu26axhUcboUzv/twfZU8fIBbTOphRAe0v8EyxzeDpKXqGHfyDA==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^5.0.1", + "resolve": "^1.22.1" + } + }, + "@rollup/pluginutils": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.5.tgz", + "integrity": "sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q==", + "dev": true, + "requires": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + } + }, + "@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.0" + } + }, + "@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "dev": true, + "requires": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + }, + "dependencies": { + "@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + } + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", + "dev": true + }, + "@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==" + }, + "@types/estree": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.3.tgz", + "integrity": "sha512-CS2rOaoQ/eAgAfcTfq6amKG7bsN+EMcgGY4FAFQdvSj2y1ixvOZTUA9mOtCai7E1SYu283XNw7urKK30nP3wkQ==", + "dev": true + }, + "@types/json-schema": { + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.14.tgz", + "integrity": "sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==", + "dev": true + }, + "@types/mocha": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.4.tgz", + "integrity": "sha512-xKU7bUjiFTIttpWaIZ9qvgg+22O1nmbA+HRxdlR+u6TWsGfmFdXrheJoK4fFxrHNVIOBDvDNKZG+LYBpMHpX3w==", + "dev": true + }, + "@types/node": { + "version": "20.8.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.7.tgz", + "integrity": "sha512-21TKHHh3eUHIi2MloeptJWALuCu5H7HQTdTrWIFReA8ad+aggoX+lRes3ex7/FtpC+sVUpFMQ+QTfYr74mruiQ==", + "dev": true, + "requires": { + "undici-types": "~5.25.1" + } + }, + "@types/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-MMzuxN3GdFwskAnb6fz0orFvhfqi752yjaXylr0Rp4oDg5H0Zn1IuyRhDVvYOwAXoJirx2xuS16I3WjxnAIHiQ==", + "dev": true + }, + "@types/sinon": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.1.tgz", + "integrity": "sha512-Q2Go6TJetYn5Za1+RJA1Aik61Oa2FS8SuJ0juIqUuJ5dZR4wvhKfmSdIqWtQ3P6gljKWjW0/R7FZkA4oXVL6OA==", + "dev": true, + "requires": { + "@types/sinonjs__fake-timers": "*" + } + }, + "@types/sinonjs__fake-timers": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", + "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", + "dev": true + }, + "@types/uuid": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.7.tgz", + "integrity": "sha512-WUtIVRUZ9i5dYXefDEAI7sh9/O7jGvHg7Df/5O/gtH3Yabe5odI3UWopVR1qbPXQtvOxWu3mM4XxlYeZtMWF4g==", + "dev": true + }, + "@typescript-eslint/eslint-plugin": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.8.0.tgz", + "integrity": "sha512-GosF4238Tkes2SHPQ1i8f6rMtG6zlKwMEB0abqSJ3Npvos+doIlc/ATG+vX1G9coDF3Ex78zM3heXHLyWEwLUw==", + "dev": true, + "requires": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.8.0", + "@typescript-eslint/type-utils": "6.8.0", + "@typescript-eslint/utils": "6.8.0", + "@typescript-eslint/visitor-keys": "6.8.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + } + }, + "@typescript-eslint/parser": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.8.0.tgz", + "integrity": "sha512-5tNs6Bw0j6BdWuP8Fx+VH4G9fEPDxnVI7yH1IAPkQH5RUtvKwRoqdecAPdQXv4rSOADAaz1LFBZvZG7VbXivSg==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "6.8.0", + "@typescript-eslint/types": "6.8.0", + "@typescript-eslint/typescript-estree": "6.8.0", + "@typescript-eslint/visitor-keys": "6.8.0", + "debug": "^4.3.4" + } + }, + "@typescript-eslint/scope-manager": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.8.0.tgz", + "integrity": "sha512-xe0HNBVwCph7rak+ZHcFD6A+q50SMsFwcmfdjs9Kz4qDh5hWhaPhFjRs/SODEhroBI5Ruyvyz9LfwUJ624O40g==", + "dev": true, + "requires": { + "@typescript-eslint/types": "6.8.0", + "@typescript-eslint/visitor-keys": "6.8.0" + } + }, + "@typescript-eslint/type-utils": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.8.0.tgz", + "integrity": "sha512-RYOJdlkTJIXW7GSldUIHqc/Hkto8E+fZN96dMIFhuTJcQwdRoGN2rEWA8U6oXbLo0qufH7NPElUb+MceHtz54g==", + "dev": true, + "requires": { + "@typescript-eslint/typescript-estree": "6.8.0", + "@typescript-eslint/utils": "6.8.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + } + }, + "@typescript-eslint/types": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.8.0.tgz", + "integrity": "sha512-p5qOxSum7W3k+llc7owEStXlGmSl8FcGvhYt8Vjy7FqEnmkCVlM3P57XQEGj58oqaBWDQXbJDZxwUWMS/EAPNQ==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.8.0.tgz", + "integrity": "sha512-ISgV0lQ8XgW+mvv5My/+iTUdRmGspducmQcDw5JxznasXNnZn3SKNrTRuMsEXv+V/O+Lw9AGcQCfVaOPCAk/Zg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "6.8.0", + "@typescript-eslint/visitor-keys": "6.8.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + } + }, + "@typescript-eslint/utils": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.8.0.tgz", + "integrity": "sha512-dKs1itdE2qFG4jr0dlYLQVppqTE+Itt7GmIf/vX6CSvsW+3ov8PbWauVKyyfNngokhIO9sKZeRGCUo1+N7U98Q==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.8.0", + "@typescript-eslint/types": "6.8.0", + "@typescript-eslint/typescript-estree": "6.8.0", + "semver": "^7.5.4" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.8.0.tgz", + "integrity": "sha512-oqAnbA7c+pgOhW2OhGvxm0t1BULX5peQI/rLsNDpGM78EebV3C9IGbX5HNZabuZ6UQrYveCLjKo8Iy/lLlBkkg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "6.8.0", + "eslint-visitor-keys": "^3.4.1" + } + }, + "acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "dev": true + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "requires": {} + }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "requires": { + "debug": "4" + } + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "requires": { + "fill-range": "^7.1.1" + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true + }, + "chai": { + "version": "4.3.10", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", + "integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.0.8" + } + }, + "chai-as-promised": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.1.tgz", + "integrity": "sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==", + "dev": true, + "requires": { + "check-error": "^1.0.2" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "requires": { + "get-func-name": "^2.0.2" + } + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true + }, + "deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==" + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, + "diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "dotenv": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "dev": true + }, + "eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "eslint": { + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.51.0.tgz", + "integrity": "sha512-2WuxRZBrlwnXi+/vFSJyjMqrNjtJqiasMzehF0shoLaW7DzS3/9Yvrmq5JiT66+pNjiX4UBnLDiKHcWAr/OInA==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.2", + "@eslint/js": "8.51.0", + "@humanwhocodes/config-array": "^0.11.11", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + } + }, + "eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true + }, + "espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "requires": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + } + }, + "esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true + }, + "flat-cache": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.1.tgz", + "integrity": "sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==", + "dev": true, + "requires": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "dependencies": { + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "flatted": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "dev": true + }, + "foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + } + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "optional": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true + }, + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "globals": { + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + } + }, + "graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "has": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", + "integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, + "http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "requires": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + } + }, + "https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "requires": { + "agent-base": "6", + "debug": "4" + } + }, + "ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-core-module": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", + "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==" + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true + }, + "is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true + }, + "is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true + }, + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "requires": { + "is-docker": "^2.0.0" + } + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, + "requires": { + "@isaacs/cliui": "^8.0.2", + "@pkgjs/parseargs": "^0.11.0" + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "optional": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true + }, + "jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "requires": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "dependencies": { + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + } + } + }, + "just-extend": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", + "dev": true + }, + "jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "requires": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "requires": { + "json-buffer": "3.0.1" + } + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + } + }, + "loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "requires": { + "get-func-name": "^2.0.1" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "magic-string": { + "version": "0.30.5", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", + "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.4.15" + } + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "requires": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + } + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true + }, + "mocha": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", + "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", + "dev": true, + "requires": { + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.4", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "5.0.1", + "ms": "2.1.3", + "nanoid": "3.3.3", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "workerpool": "6.2.1", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "nanoid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", + "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "nise": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.4.tgz", + "integrity": "sha512-8+Ib8rRJ4L0o3kfmyVCL7gzrohyDe0cMFTBa2d364yIrEGMEoetznKJx899YxjybU6bL9SQkYPSBBs1gyYs8Xg==", + "dev": true, + "requires": { + "@sinonjs/commons": "^2.0.0", + "@sinonjs/fake-timers": "^10.0.2", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + }, + "dependencies": { + "@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + } + } + }, + "nock": { + "version": "13.3.6", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.3.6.tgz", + "integrity": "sha512-lT6YuktKroUFM+27mubf2uqQZVy2Jf+pfGzuh9N6VwdHlFoZqvi4zyxFTVR1w/ChPqGY6yxGehHp6C3wqCASCw==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "json-stringify-safe": "^5.0.1", + "propagate": "^2.0.0" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "requires": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + } + }, + "optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "requires": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "path-scurry": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", + "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "dev": true, + "requires": { + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz", + "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==", + "dev": true + } + } + }, + "path-to-regexp": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", + "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", + "dev": true, + "requires": { + "isarray": "0.0.1" + } + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, + "pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "dev": true + }, + "punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true + }, + "resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "requires": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rimraf": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz", + "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==", + "dev": true, + "requires": { + "glob": "^10.3.7" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "glob": { + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + } + }, + "minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "rollup": { + "version": "3.29.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", + "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", + "dev": true, + "requires": { + "fsevents": "~2.3.2" + } + }, + "rollup-plugin-dts": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/rollup-plugin-dts/-/rollup-plugin-dts-5.3.1.tgz", + "integrity": "sha512-gusMi+Z4gY/JaEQeXnB0RUdU82h1kF0WYzCWgVmV4p3hWXqelaKuCvcJawfeg+EKn2T1Ie+YWF2OiN1/L8bTVg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.5", + "magic-string": "^0.30.2" + } + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true + }, + "sinon": { + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-15.2.0.tgz", + "integrity": "sha512-nPS85arNqwBXaIsFCkolHjGIkFo+Oxu9vbgmBJizLAhqe6P2o3Qmj3KCUoRkfhHtvgDhZdWD3risLHAUJ8npjw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^10.3.0", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.1.0", + "nise": "^5.1.4", + "supports-color": "^7.2.0" + }, + "dependencies": { + "diff": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", + "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", + "dev": true + } + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "stoppable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", + "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==" + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "string-width-cjs": { + "version": "npm:string-width@4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-ansi-cjs": { + "version": "npm:strip-ansi@6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "ts-api-utils": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", + "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", + "dev": true, + "requires": {} + }, + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + }, + "typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "dev": true + }, + "undici-types": { + "version": "5.25.3", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz", + "integrity": "sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==", + "dev": true + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "workerpool": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", + "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "dev": true + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrap-ansi-cjs": { + "version": "npm:wrap-ansi@7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true + }, + "yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "requires": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + } + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true + } } } diff --git a/src/ConfigurationClientManager.ts b/src/ConfigurationClientManager.ts index fc5ad553..0429167f 100644 --- a/src/ConfigurationClientManager.ts +++ b/src/ConfigurationClientManager.ts @@ -11,9 +11,9 @@ import * as RequestTracing from "./requestTracing/constants"; const TCP_ORIGIN = "_origin._tcp"; const ALT = "_alt"; const TCP = "_tcp"; -const EndpointSection = "Endpoint"; -const IdSection = "Id"; -const SecretSection = "Secret"; +const Endpoint = "Endpoint"; +const Id = "Id"; +const Secret = "Secret"; const AzConfigDomainLabel = ".azconfig."; const AppConfigDomainLabel = ".appconfig."; const FallbackClientRefreshExpireInterval = 60 * 60 * 1000; // 1 hour in milliseconds @@ -51,10 +51,10 @@ export class ConfigurationClientManager implements IConfigurationClientManager { options = credentialOrOptions as AzureAppConfigurationOptions; this.#clientOptions = getClientOptions(options); staticClient = new AppConfigurationClient(connectionString, this.#clientOptions); - this.#secret = parseConnectionString(connectionString, SecretSection); - this.#id = parseConnectionString(connectionString, IdSection); + this.#secret = parseConnectionString(connectionString, Secret); + this.#id = parseConnectionString(connectionString, Id); // TODO: need to check if it's CDN or not - this.endpoint = parseConnectionString(connectionString, EndpointSection); + this.endpoint = parseConnectionString(connectionString, Endpoint); } else if (connectionStringOrEndpoint instanceof URL) { const credential = credentialOrOptions as TokenCredential; @@ -173,7 +173,7 @@ async function querySrvTargetHost(host) { if (isFailoverableEnv()) { dns = require("dns/promises"); } else { - return results; + throw new Error("Failover is not supported in the current environment."); } try { @@ -215,7 +215,11 @@ async function querySrvTargetHost(host) { } } } catch (err) { - throw new Error(`Failed to lookup origin SRV records: ${err.message}`); + if (err.code === "ENOTFOUND") { + return results; // No SRV records found, return empty array + } else { + throw new Error(`Failed to lookup SRV records: ${err.message}`); + } } return results; @@ -264,7 +268,7 @@ function buildConnectionString(endpoint, secret, id) { return ""; } - return `${EndpointSection}=${endpoint};${IdSection}=${id};${SecretSection}=${secret}`; + return `${Endpoint}=${endpoint};${Id}=${id};${Secret}=${secret}`; } /** From f0dbc814e70a4ebd668a3513e3bb89ae1efe5d73 Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Thu, 31 Oct 2024 17:33:36 +0800 Subject: [PATCH 08/24] resolve comments --- src/AzureAppConfigurationImpl.ts | 13 +++++++------ src/ConfigurationClientManager.ts | 31 ++++++------------------------- 2 files changed, 13 insertions(+), 31 deletions(-) diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index 595c7574..ffac36a5 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -203,23 +203,23 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { } async #executeWithFailoverPolicy(funcToExecute) { - const clients = await this.#clientManager.getClients(); - if (clients.length === 0) { + const clientWrappers = await this.#clientManager.getClients(); + if (clientWrappers.length === 0) { this.#clientManager.refreshClients(); throw new Error("No client is available to connect to the target App Configuration store."); } - for (const client of clients) { + for (const clientWrapper of clientWrappers) { let successful = false; try { - const result = await funcToExecute(client.client); + const result = await funcToExecute(clientWrapper.client); this.#isFailoverRequest = false; successful = true; - updateClientBackoffStatus(client, successful); + updateClientBackoffStatus(clientWrapper, successful); return result; } catch (error) { if (isFailoverableError(error)) { - updateClientBackoffStatus(client, successful); + updateClientBackoffStatus(clientWrapper, successful); this.#isFailoverRequest = true; continue; } @@ -318,6 +318,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { // Temporary map to store feature flags, key is the key of the setting, value is the raw value of the setting const funcToExecute = async (client) => { const featureFlagSettings: ConfigurationSetting[] = []; + // deep copy selectors to avoid modification if current client fails const selectors = JSON.parse( JSON.stringify(this.#featureFlagSelectors) ); diff --git a/src/ConfigurationClientManager.ts b/src/ConfigurationClientManager.ts index 0429167f..ffb85766 100644 --- a/src/ConfigurationClientManager.ts +++ b/src/ConfigurationClientManager.ts @@ -72,7 +72,7 @@ export class ConfigurationClientManager implements IConfigurationClientManager { this.isFailoverable = (options?.replicaDiscoveryEnabled ?? true) && isFailoverableEnv(); } - async getClients() { + async getClients() : Promise { if (!this.isFailoverable) { return this.#staticClients; } @@ -163,10 +163,8 @@ export class ConfigurationClientManager implements IConfigurationClientManager { /** * Query SRV records and return target hosts. - * @param {string} host - The host to query. - * @returns {Promise} - A promise that resolves to an array of target hosts. */ -async function querySrvTargetHost(host) { +async function querySrvTargetHost(host: string): Promise { const results: string[] = []; let dns; @@ -227,13 +225,8 @@ async function querySrvTargetHost(host) { /** * Parses the connection string to extract the value associated with a specific token. - * - * @param {string} connectionString - The connection string containing tokens. - * @param {string} token - The token whose value needs to be extracted. - * @returns {string} The value associated with the token, or an empty string if not found. - * @throws {Error} If the connection string is empty or the token is not found. */ -function parseConnectionString(connectionString, token) { +function parseConnectionString(connectionString, token: string): string { if (!connectionString) { throw new Error("connectionString is empty"); } @@ -257,13 +250,8 @@ function parseConnectionString(connectionString, token) { /** * Builds a connection string from the given endpoint, secret, and id. * Returns an empty string if either secret or id is empty. - * - * @param {string} endpoint - The endpoint to include in the connection string. - * @param {string} secret - The secret to include in the connection string. - * @param {string} id - The ID to include in the connection string. - * @returns {string} - The formatted connection string or an empty string if invalid input. */ -function buildConnectionString(endpoint, secret, id) { +function buildConnectionString(endpoint, secret, id: string): string { if (!secret || !id) { return ""; } @@ -273,11 +261,8 @@ function buildConnectionString(endpoint, secret, id) { /** * Extracts a valid domain from the given endpoint URL based on trusted domain labels. - * - * @param {string} endpoint - The endpoint URL. - * @returns {string} - The valid domain or an empty string if no valid domain is found. */ -export function getValidDomain(endpoint) { +export function getValidDomain(endpoint: string): string { try { const url = new URL(endpoint); const trustedDomainLabels = [AzConfigDomainLabel, AppConfigDomainLabel]; @@ -298,12 +283,8 @@ export function getValidDomain(endpoint) { /** * Checks if the given host ends with the valid domain. - * - * @param {string} host - The host to be validated. - * @param {string} validDomain - The valid domain to check against. - * @returns {boolean} - True if the host ends with the valid domain, false otherwise. */ -export function isValidEndpoint(host, validDomain) { +export function isValidEndpoint(host: string, validDomain: string): boolean { if (!validDomain) { return false; } From 232bb851370f4b56914f02d47a48642ae98761d5 Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Thu, 31 Oct 2024 17:54:30 +0800 Subject: [PATCH 09/24] update package-lock --- package-lock.json | 2589 --------------------------------------------- 1 file changed, 2589 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0bc3bc29..7cc0a774 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3594,2594 +3594,5 @@ "url": "https://github.com/sponsors/sindresorhus" } } - }, - "dependencies": { - "@aashutoshrathi/word-wrap": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", - "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", - "dev": true - }, - "@azure/abort-controller": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", - "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", - "requires": { - "tslib": "^2.2.0" - } - }, - "@azure/app-configuration": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@azure/app-configuration/-/app-configuration-1.6.1.tgz", - "integrity": "sha512-pk8zyG/8Nc6VN7uDA9QY19UFhTXneUbnB+5IcW9uuPyVDXU17TcXBI4xY1ZBm7hmhn0yh3CeZK4kOxa/tjsMqQ==", - "requires": { - "@azure/abort-controller": "^1.0.0", - "@azure/core-auth": "^1.3.0", - "@azure/core-client": "^1.5.0", - "@azure/core-http-compat": "^2.0.0", - "@azure/core-lro": "^2.5.1", - "@azure/core-paging": "^1.4.0", - "@azure/core-rest-pipeline": "^1.6.0", - "@azure/core-tracing": "^1.0.0", - "@azure/core-util": "^1.6.1", - "@azure/logger": "^1.0.0", - "tslib": "^2.2.0" - }, - "dependencies": { - "@azure/core-http-compat": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-2.0.1.tgz", - "integrity": "sha512-xpQZz/q7E0jSW4rckrTo2mDFDQgo6I69hBU4voMQi7REi6JRW5a+KfVkbJCFCWnkFmP6cAJ0IbuudTdf/MEBOQ==", - "requires": { - "@azure/abort-controller": "^1.0.4", - "@azure/core-client": "^1.3.0", - "@azure/core-rest-pipeline": "^1.3.0" - } - } - } - }, - "@azure/core-auth": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.5.0.tgz", - "integrity": "sha512-udzoBuYG1VBoHVohDTrvKjyzel34zt77Bhp7dQntVGGD0ehVq48owENbBG8fIgkHRNUBQH5k1r0hpoMu5L8+kw==", - "requires": { - "@azure/abort-controller": "^1.0.0", - "@azure/core-util": "^1.1.0", - "tslib": "^2.2.0" - } - }, - "@azure/core-client": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.7.3.tgz", - "integrity": "sha512-kleJ1iUTxcO32Y06dH9Pfi9K4U+Tlb111WXEnbt7R/ne+NLRwppZiTGJuTD5VVoxTMK5NTbEtm5t2vcdNCFe2g==", - "requires": { - "@azure/abort-controller": "^1.0.0", - "@azure/core-auth": "^1.4.0", - "@azure/core-rest-pipeline": "^1.9.1", - "@azure/core-tracing": "^1.0.0", - "@azure/core-util": "^1.0.0", - "@azure/logger": "^1.0.0", - "tslib": "^2.2.0" - } - }, - "@azure/core-http-compat": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-1.3.0.tgz", - "integrity": "sha512-ZN9avruqbQ5TxopzG3ih3KRy52n8OAbitX3fnZT5go4hzu0J+KVPSzkL+Wt3hpJpdG8WIfg1sBD1tWkgUdEpBA==", - "requires": { - "@azure/abort-controller": "^1.0.4", - "@azure/core-client": "^1.3.0", - "@azure/core-rest-pipeline": "^1.3.0" - } - }, - "@azure/core-lro": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.5.4.tgz", - "integrity": "sha512-3GJiMVH7/10bulzOKGrrLeG/uCBH/9VtxqaMcB9lIqAeamI/xYQSHJL/KcsLDuH+yTjYpro/u6D/MuRe4dN70Q==", - "requires": { - "@azure/abort-controller": "^1.0.0", - "@azure/core-util": "^1.2.0", - "@azure/logger": "^1.0.0", - "tslib": "^2.2.0" - } - }, - "@azure/core-paging": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.5.0.tgz", - "integrity": "sha512-zqWdVIt+2Z+3wqxEOGzR5hXFZ8MGKK52x4vFLw8n58pR6ZfKRx3EXYTxTaYxYHc/PexPUTyimcTWFJbji9Z6Iw==", - "requires": { - "tslib": "^2.2.0" - } - }, - "@azure/core-rest-pipeline": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.12.2.tgz", - "integrity": "sha512-wLLJQdL4v1yoqYtEtjKNjf8pJ/G/BqVomAWxcKOR1KbZJyCEnCv04yks7Y1NhJ3JzxbDs307W67uX0JzklFdCg==", - "requires": { - "@azure/abort-controller": "^1.0.0", - "@azure/core-auth": "^1.4.0", - "@azure/core-tracing": "^1.0.1", - "@azure/core-util": "^1.3.0", - "@azure/logger": "^1.0.0", - "form-data": "^4.0.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.0", - "tslib": "^2.2.0" - } - }, - "@azure/core-tracing": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.1.tgz", - "integrity": "sha512-I5CGMoLtX+pI17ZdiFJZgxMJApsK6jjfm85hpgp3oazCdq5Wxgh4wMr7ge/TTWW1B5WBuvIOI1fMU/FrOAMKrw==", - "requires": { - "tslib": "^2.2.0" - } - }, - "@azure/core-util": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.9.0.tgz", - "integrity": "sha512-AfalUQ1ZppaKuxPPMsFEUdX6GZPB3d9paR9d/TTL7Ow2De8cJaC7ibi7kWVlFAVPCYo31OcnGymc0R89DX8Oaw==", - "requires": { - "@azure/abort-controller": "^2.0.0", - "tslib": "^2.6.2" - }, - "dependencies": { - "@azure/abort-controller": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", - "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", - "requires": { - "tslib": "^2.6.2" - } - } - } - }, - "@azure/identity": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.2.1.tgz", - "integrity": "sha512-U8hsyC9YPcEIzoaObJlRDvp7KiF0MGS7xcWbyJSVvXRkC/HXo1f0oYeBYmEvVgRfacw7GHf6D6yAoh9JHz6A5Q==", - "requires": { - "@azure/abort-controller": "^1.0.0", - "@azure/core-auth": "^1.5.0", - "@azure/core-client": "^1.4.0", - "@azure/core-rest-pipeline": "^1.1.0", - "@azure/core-tracing": "^1.0.0", - "@azure/core-util": "^1.3.0", - "@azure/logger": "^1.0.0", - "@azure/msal-browser": "^3.11.1", - "@azure/msal-node": "^2.9.2", - "events": "^3.0.0", - "jws": "^4.0.0", - "open": "^8.0.0", - "stoppable": "^1.1.0", - "tslib": "^2.2.0" - } - }, - "@azure/keyvault-secrets": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@azure/keyvault-secrets/-/keyvault-secrets-4.7.0.tgz", - "integrity": "sha512-YvlFXRQ+SI5NT4GtSFbb6HGo6prW3yzDab8tr6vga2/SjDQew3wJsCAAr/xwZz6XshFXCYEX26CDKmPf+SJKJg==", - "requires": { - "@azure/abort-controller": "^1.0.0", - "@azure/core-auth": "^1.3.0", - "@azure/core-client": "^1.5.0", - "@azure/core-http-compat": "^1.3.0", - "@azure/core-lro": "^2.2.0", - "@azure/core-paging": "^1.1.1", - "@azure/core-rest-pipeline": "^1.8.0", - "@azure/core-tracing": "^1.0.0", - "@azure/core-util": "^1.0.0", - "@azure/logger": "^1.0.0", - "tslib": "^2.2.0" - } - }, - "@azure/logger": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.0.4.tgz", - "integrity": "sha512-ustrPY8MryhloQj7OWGe+HrYx+aoiOxzbXTtgblbV3xwCqpzUK36phH3XNHQKj3EPonyFUuDTfR3qFhTEAuZEg==", - "requires": { - "tslib": "^2.2.0" - } - }, - "@azure/msal-browser": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-3.13.0.tgz", - "integrity": "sha512-fD906nmJei3yE7la6DZTdUtXKvpwzJURkfsiz9747Icv4pit77cegSm6prJTKLQ1fw4iiZzrrWwxnhMLrTf5gQ==", - "requires": { - "@azure/msal-common": "14.9.0" - } - }, - "@azure/msal-common": { - "version": "14.9.0", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.9.0.tgz", - "integrity": "sha512-yzBPRlWPnTBeixxLNI3BBIgF5/bHpbhoRVuuDBnYjCyWRavaPUsKAHUDYLqpGkBLDciA6TCc6GOxN4/S3WiSxg==" - }, - "@azure/msal-node": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.9.2.tgz", - "integrity": "sha512-8tvi6Cos3m+0KmRbPjgkySXi+UQU/QiuVRFnrxIwt5xZlEEFa69O04RTaNESGgImyBBlYbo2mfE8/U8Bbdk1WQ==", - "requires": { - "@azure/msal-common": "14.12.0", - "jsonwebtoken": "^9.0.0", - "uuid": "^8.3.0" - }, - "dependencies": { - "@azure/msal-common": { - "version": "14.12.0", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.12.0.tgz", - "integrity": "sha512-IDDXmzfdwmDkv4SSmMEyAniJf6fDu3FJ7ncOjlxkDuT85uSnLEhZi3fGZpoR7T4XZpOMx9teM9GXBgrfJgyeBw==" - }, - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" - } - } - }, - "@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", - "dev": true, - "optional": true, - "requires": { - "@babel/highlight": "^7.22.13", - "chalk": "^2.4.2" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "optional": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "optional": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "optional": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true, - "optional": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "optional": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "optional": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "optional": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", - "dev": true, - "optional": true - }, - "@babel/highlight": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", - "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", - "dev": true, - "optional": true, - "requires": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "optional": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "optional": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "optional": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true, - "optional": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "optional": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "optional": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "optional": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^3.3.0" - } - }, - "@eslint-community/regexpp": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.9.1.tgz", - "integrity": "sha512-Y27x+MBLjXa+0JWDhykM3+JE+il3kHKAEqabfEWq3SDhZjLYb6/BHL/JKFnH3fe207JaXkyDo685Oc2Glt6ifA==", - "dev": true - }, - "@eslint/eslintrc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", - "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", - "dev": true, - "requires": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - } - }, - "@eslint/js": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.51.0.tgz", - "integrity": "sha512-HxjQ8Qn+4SI3/AFv6sOrDB+g6PpUTDwSJiQqOrnneEk8L71161srI9gjzzZvYVbzHiVg/BvcH95+cK/zfIt4pg==", - "dev": true - }, - "@humanwhocodes/config-array": { - "version": "0.11.12", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.12.tgz", - "integrity": "sha512-NlGesA1usRNn6ctHCZ21M4/dKPgW9Nn1FypRdIKKgZOKzkVV4T1FlK5mBiLhHBCDmEbdQG0idrcXlbZfksJ+RA==", - "dev": true, - "requires": { - "@humanwhocodes/object-schema": "^2.0.0", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - } - }, - "@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true - }, - "@humanwhocodes/object-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.0.tgz", - "integrity": "sha512-9S9QrXY2K0L4AGDcSgTi9vgiCcG8VcBv4Mp7/1hDPYoswIy6Z6KO5blYto82BT8M0MZNRWmCFLpCs3HlpYGGdw==", - "dev": true - }, - "@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "requires": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true - }, - "ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true - }, - "emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "requires": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - } - }, - "strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "requires": { - "ansi-regex": "^6.0.1" - } - }, - "wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "requires": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - } - } - } - }, - "@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true - }, - "@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - } - }, - "@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "optional": true - }, - "@rollup/plugin-typescript": { - "version": "11.1.5", - "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-11.1.5.tgz", - "integrity": "sha512-rnMHrGBB0IUEv69Q8/JGRD/n4/n6b3nfpufUu26axhUcboUzv/twfZU8fIBbTOphRAe0v8EyxzeDpKXqGHfyDA==", - "dev": true, - "requires": { - "@rollup/pluginutils": "^5.0.1", - "resolve": "^1.22.1" - } - }, - "@rollup/pluginutils": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.5.tgz", - "integrity": "sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q==", - "dev": true, - "requires": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^2.3.1" - } - }, - "@sinonjs/commons": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", - "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", - "dev": true, - "requires": { - "type-detect": "4.0.8" - } - }, - "@sinonjs/fake-timers": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", - "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", - "dev": true, - "requires": { - "@sinonjs/commons": "^3.0.0" - } - }, - "@sinonjs/samsam": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", - "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", - "dev": true, - "requires": { - "@sinonjs/commons": "^2.0.0", - "lodash.get": "^4.4.2", - "type-detect": "^4.0.8" - }, - "dependencies": { - "@sinonjs/commons": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", - "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", - "dev": true, - "requires": { - "type-detect": "4.0.8" - } - } - } - }, - "@sinonjs/text-encoding": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", - "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", - "dev": true - }, - "@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==" - }, - "@types/estree": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.3.tgz", - "integrity": "sha512-CS2rOaoQ/eAgAfcTfq6amKG7bsN+EMcgGY4FAFQdvSj2y1ixvOZTUA9mOtCai7E1SYu283XNw7urKK30nP3wkQ==", - "dev": true - }, - "@types/json-schema": { - "version": "7.0.14", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.14.tgz", - "integrity": "sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==", - "dev": true - }, - "@types/mocha": { - "version": "10.0.4", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.4.tgz", - "integrity": "sha512-xKU7bUjiFTIttpWaIZ9qvgg+22O1nmbA+HRxdlR+u6TWsGfmFdXrheJoK4fFxrHNVIOBDvDNKZG+LYBpMHpX3w==", - "dev": true - }, - "@types/node": { - "version": "20.8.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.7.tgz", - "integrity": "sha512-21TKHHh3eUHIi2MloeptJWALuCu5H7HQTdTrWIFReA8ad+aggoX+lRes3ex7/FtpC+sVUpFMQ+QTfYr74mruiQ==", - "dev": true, - "requires": { - "undici-types": "~5.25.1" - } - }, - "@types/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-MMzuxN3GdFwskAnb6fz0orFvhfqi752yjaXylr0Rp4oDg5H0Zn1IuyRhDVvYOwAXoJirx2xuS16I3WjxnAIHiQ==", - "dev": true - }, - "@types/sinon": { - "version": "17.0.1", - "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.1.tgz", - "integrity": "sha512-Q2Go6TJetYn5Za1+RJA1Aik61Oa2FS8SuJ0juIqUuJ5dZR4wvhKfmSdIqWtQ3P6gljKWjW0/R7FZkA4oXVL6OA==", - "dev": true, - "requires": { - "@types/sinonjs__fake-timers": "*" - } - }, - "@types/sinonjs__fake-timers": { - "version": "8.1.5", - "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", - "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", - "dev": true - }, - "@types/uuid": { - "version": "9.0.7", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.7.tgz", - "integrity": "sha512-WUtIVRUZ9i5dYXefDEAI7sh9/O7jGvHg7Df/5O/gtH3Yabe5odI3UWopVR1qbPXQtvOxWu3mM4XxlYeZtMWF4g==", - "dev": true - }, - "@typescript-eslint/eslint-plugin": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.8.0.tgz", - "integrity": "sha512-GosF4238Tkes2SHPQ1i8f6rMtG6zlKwMEB0abqSJ3Npvos+doIlc/ATG+vX1G9coDF3Ex78zM3heXHLyWEwLUw==", - "dev": true, - "requires": { - "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.8.0", - "@typescript-eslint/type-utils": "6.8.0", - "@typescript-eslint/utils": "6.8.0", - "@typescript-eslint/visitor-keys": "6.8.0", - "debug": "^4.3.4", - "graphemer": "^1.4.0", - "ignore": "^5.2.4", - "natural-compare": "^1.4.0", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - } - }, - "@typescript-eslint/parser": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.8.0.tgz", - "integrity": "sha512-5tNs6Bw0j6BdWuP8Fx+VH4G9fEPDxnVI7yH1IAPkQH5RUtvKwRoqdecAPdQXv4rSOADAaz1LFBZvZG7VbXivSg==", - "dev": true, - "requires": { - "@typescript-eslint/scope-manager": "6.8.0", - "@typescript-eslint/types": "6.8.0", - "@typescript-eslint/typescript-estree": "6.8.0", - "@typescript-eslint/visitor-keys": "6.8.0", - "debug": "^4.3.4" - } - }, - "@typescript-eslint/scope-manager": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.8.0.tgz", - "integrity": "sha512-xe0HNBVwCph7rak+ZHcFD6A+q50SMsFwcmfdjs9Kz4qDh5hWhaPhFjRs/SODEhroBI5Ruyvyz9LfwUJ624O40g==", - "dev": true, - "requires": { - "@typescript-eslint/types": "6.8.0", - "@typescript-eslint/visitor-keys": "6.8.0" - } - }, - "@typescript-eslint/type-utils": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.8.0.tgz", - "integrity": "sha512-RYOJdlkTJIXW7GSldUIHqc/Hkto8E+fZN96dMIFhuTJcQwdRoGN2rEWA8U6oXbLo0qufH7NPElUb+MceHtz54g==", - "dev": true, - "requires": { - "@typescript-eslint/typescript-estree": "6.8.0", - "@typescript-eslint/utils": "6.8.0", - "debug": "^4.3.4", - "ts-api-utils": "^1.0.1" - } - }, - "@typescript-eslint/types": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.8.0.tgz", - "integrity": "sha512-p5qOxSum7W3k+llc7owEStXlGmSl8FcGvhYt8Vjy7FqEnmkCVlM3P57XQEGj58oqaBWDQXbJDZxwUWMS/EAPNQ==", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.8.0.tgz", - "integrity": "sha512-ISgV0lQ8XgW+mvv5My/+iTUdRmGspducmQcDw5JxznasXNnZn3SKNrTRuMsEXv+V/O+Lw9AGcQCfVaOPCAk/Zg==", - "dev": true, - "requires": { - "@typescript-eslint/types": "6.8.0", - "@typescript-eslint/visitor-keys": "6.8.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - } - }, - "@typescript-eslint/utils": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.8.0.tgz", - "integrity": "sha512-dKs1itdE2qFG4jr0dlYLQVppqTE+Itt7GmIf/vX6CSvsW+3ov8PbWauVKyyfNngokhIO9sKZeRGCUo1+N7U98Q==", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.8.0", - "@typescript-eslint/types": "6.8.0", - "@typescript-eslint/typescript-estree": "6.8.0", - "semver": "^7.5.4" - } - }, - "@typescript-eslint/visitor-keys": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.8.0.tgz", - "integrity": "sha512-oqAnbA7c+pgOhW2OhGvxm0t1BULX5peQI/rLsNDpGM78EebV3C9IGbX5HNZabuZ6UQrYveCLjKo8Iy/lLlBkkg==", - "dev": true, - "requires": { - "@typescript-eslint/types": "6.8.0", - "eslint-visitor-keys": "^3.4.1" - } - }, - "acorn": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", - "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", - "dev": true - }, - "acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "requires": {} - }, - "agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "requires": { - "debug": "4" - } - }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "dev": true - }, - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true - }, - "assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "requires": { - "fill-range": "^7.1.1" - } - }, - "browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true - }, - "buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" - }, - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true - }, - "camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true - }, - "chai": { - "version": "4.3.10", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", - "integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==", - "dev": true, - "requires": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.3", - "deep-eql": "^4.1.3", - "get-func-name": "^2.0.2", - "loupe": "^2.3.6", - "pathval": "^1.1.1", - "type-detect": "^4.0.8" - } - }, - "chai-as-promised": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.1.tgz", - "integrity": "sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==", - "dev": true, - "requires": { - "check-error": "^1.0.2" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "check-error": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", - "dev": true, - "requires": { - "get-func-name": "^2.0.2" - } - }, - "chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, - "requires": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "fsevents": "~2.3.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "dependencies": { - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - } - } - }, - "cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "requires": { - "ms": "2.1.2" - } - }, - "decamelize": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", - "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", - "dev": true - }, - "deep-eql": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", - "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", - "dev": true, - "requires": { - "type-detect": "^4.0.0" - } - }, - "deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, - "define-lazy-prop": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==" - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" - }, - "diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", - "dev": true - }, - "dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "dotenv": { - "version": "16.3.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", - "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", - "dev": true - }, - "eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true - }, - "ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true - }, - "escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true - }, - "eslint": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.51.0.tgz", - "integrity": "sha512-2WuxRZBrlwnXi+/vFSJyjMqrNjtJqiasMzehF0shoLaW7DzS3/9Yvrmq5JiT66+pNjiX4UBnLDiKHcWAr/OInA==", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.51.0", - "@humanwhocodes/config-array": "^0.11.11", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - } - }, - "eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - } - }, - "eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true - }, - "espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "requires": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - } - }, - "esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - } - }, - "esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - } - }, - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true - }, - "estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true - }, - "events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "fast-glob": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", - "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "dependencies": { - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - } - } - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true - }, - "fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "requires": { - "flat-cache": "^3.0.4" - } - }, - "fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - } - }, - "flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "dev": true - }, - "flat-cache": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.1.tgz", - "integrity": "sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==", - "dev": true, - "requires": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "dependencies": { - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } - } - }, - "flatted": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", - "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", - "dev": true - }, - "foreground-child": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - } - }, - "form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, - "fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "optional": true - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true - }, - "get-func-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", - "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", - "dev": true - }, - "glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "requires": { - "is-glob": "^4.0.3" - } - }, - "globals": { - "version": "13.23.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", - "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", - "dev": true, - "requires": { - "type-fest": "^0.20.2" - } - }, - "globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - } - }, - "graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true - }, - "has": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", - "integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true - }, - "http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", - "requires": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - } - }, - "https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "requires": { - "agent-base": "6", - "debug": "4" - } - }, - "ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", - "dev": true - }, - "import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "is-core-module": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", - "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==" - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true - }, - "is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", - "dev": true - }, - "is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true - }, - "is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "requires": { - "is-docker": "^2.0.0" - } - }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "jackspeak": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", - "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", - "dev": true, - "requires": { - "@isaacs/cliui": "^8.0.2", - "@pkgjs/parseargs": "^0.11.0" - } - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "optional": true - }, - "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - }, - "json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "dev": true - }, - "jsonwebtoken": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", - "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", - "requires": { - "jws": "^3.2.2", - "lodash.includes": "^4.3.0", - "lodash.isboolean": "^3.0.3", - "lodash.isinteger": "^4.0.4", - "lodash.isnumber": "^3.0.3", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.once": "^4.0.0", - "ms": "^2.1.1", - "semver": "^7.5.4" - }, - "dependencies": { - "jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", - "requires": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", - "requires": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" - } - } - } - }, - "just-extend": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", - "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", - "dev": true - }, - "jwa": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", - "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", - "requires": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "jws": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", - "requires": { - "jwa": "^2.0.0", - "safe-buffer": "^5.0.1" - } - }, - "keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "requires": { - "json-buffer": "3.0.1" - } - }, - "levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, - "locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "requires": { - "p-locate": "^5.0.0" - } - }, - "lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", - "dev": true - }, - "lodash.includes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" - }, - "lodash.isboolean": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" - }, - "lodash.isinteger": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" - }, - "lodash.isnumber": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" - }, - "lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" - }, - "lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" - }, - "lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, - "lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" - }, - "log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, - "requires": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - } - }, - "loupe": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", - "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", - "dev": true, - "requires": { - "get-func-name": "^2.0.1" - } - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "requires": { - "yallist": "^4.0.0" - } - }, - "magic-string": { - "version": "0.30.5", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", - "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", - "dev": true, - "requires": { - "@jridgewell/sourcemap-codec": "^1.4.15" - } - }, - "merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true - }, - "micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "requires": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - } - }, - "mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" - }, - "mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "requires": { - "mime-db": "1.52.0" - } - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", - "dev": true - }, - "mocha": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", - "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", - "dev": true, - "requires": { - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.3", - "debug": "4.3.4", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "7.2.0", - "he": "1.2.0", - "js-yaml": "4.1.0", - "log-symbols": "4.1.0", - "minimatch": "5.0.1", - "ms": "2.1.3", - "nanoid": "3.3.3", - "serialize-javascript": "6.0.0", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "workerpool": "6.2.1", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" - }, - "dependencies": { - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0" - } - }, - "minimatch": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", - "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", - "dev": true, - "requires": { - "brace-expansion": "^2.0.1" - } - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, - "supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "nanoid": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", - "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", - "dev": true - }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true - }, - "nise": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.4.tgz", - "integrity": "sha512-8+Ib8rRJ4L0o3kfmyVCL7gzrohyDe0cMFTBa2d364yIrEGMEoetznKJx899YxjybU6bL9SQkYPSBBs1gyYs8Xg==", - "dev": true, - "requires": { - "@sinonjs/commons": "^2.0.0", - "@sinonjs/fake-timers": "^10.0.2", - "@sinonjs/text-encoding": "^0.7.1", - "just-extend": "^4.0.2", - "path-to-regexp": "^1.7.0" - }, - "dependencies": { - "@sinonjs/commons": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", - "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", - "dev": true, - "requires": { - "type-detect": "4.0.8" - } - } - } - }, - "nock": { - "version": "13.3.6", - "resolved": "https://registry.npmjs.org/nock/-/nock-13.3.6.tgz", - "integrity": "sha512-lT6YuktKroUFM+27mubf2uqQZVy2Jf+pfGzuh9N6VwdHlFoZqvi4zyxFTVR1w/ChPqGY6yxGehHp6C3wqCASCw==", - "dev": true, - "requires": { - "debug": "^4.1.0", - "json-stringify-safe": "^5.0.1", - "propagate": "^2.0.0" - } - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "open": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", - "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", - "requires": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" - } - }, - "optionator": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", - "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", - "dev": true, - "requires": { - "@aashutoshrathi/word-wrap": "^1.2.3", - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0" - } - }, - "p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "requires": { - "p-limit": "^3.0.2" - } - }, - "parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "requires": { - "callsites": "^3.0.0" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - }, - "path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "path-scurry": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", - "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", - "dev": true, - "requires": { - "lru-cache": "^9.1.1 || ^10.0.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "dependencies": { - "lru-cache": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz", - "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==", - "dev": true - } - } - }, - "path-to-regexp": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", - "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", - "dev": true, - "requires": { - "isarray": "0.0.1" - } - }, - "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true - }, - "pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", - "dev": true - }, - "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true - }, - "prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true - }, - "propagate": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", - "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", - "dev": true - }, - "punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", - "dev": true - }, - "queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true - }, - "randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "requires": { - "safe-buffer": "^5.1.0" - } - }, - "readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "requires": { - "picomatch": "^2.2.1" - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true - }, - "resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, - "requires": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - } - }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true - }, - "reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true - }, - "rimraf": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz", - "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==", - "dev": true, - "requires": { - "glob": "^10.3.7" - }, - "dependencies": { - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0" - } - }, - "glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", - "dev": true, - "requires": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - } - }, - "minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "requires": { - "brace-expansion": "^2.0.1" - } - } - } - }, - "rollup": { - "version": "3.29.5", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", - "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", - "dev": true, - "requires": { - "fsevents": "~2.3.2" - } - }, - "rollup-plugin-dts": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/rollup-plugin-dts/-/rollup-plugin-dts-5.3.1.tgz", - "integrity": "sha512-gusMi+Z4gY/JaEQeXnB0RUdU82h1kF0WYzCWgVmV4p3hWXqelaKuCvcJawfeg+EKn2T1Ie+YWF2OiN1/L8bTVg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.22.5", - "magic-string": "^0.30.2" - } - }, - "run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "requires": { - "queue-microtask": "^1.2.2" - } - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - }, - "semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "requires": { - "lru-cache": "^6.0.0" - } - }, - "serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", - "dev": true, - "requires": { - "randombytes": "^2.1.0" - } - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true - }, - "signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true - }, - "sinon": { - "version": "15.2.0", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-15.2.0.tgz", - "integrity": "sha512-nPS85arNqwBXaIsFCkolHjGIkFo+Oxu9vbgmBJizLAhqe6P2o3Qmj3KCUoRkfhHtvgDhZdWD3risLHAUJ8npjw==", - "dev": true, - "requires": { - "@sinonjs/commons": "^3.0.0", - "@sinonjs/fake-timers": "^10.3.0", - "@sinonjs/samsam": "^8.0.0", - "diff": "^5.1.0", - "nise": "^5.1.4", - "supports-color": "^7.2.0" - }, - "dependencies": { - "diff": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", - "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", - "dev": true - } - } - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "stoppable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", - "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==" - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "string-width-cjs": { - "version": "npm:string-width@4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "strip-ansi-cjs": { - "version": "npm:strip-ansi@6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true - }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "ts-api-utils": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", - "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", - "dev": true, - "requires": {} - }, - "tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - }, - "type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1" - } - }, - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true - }, - "type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true - }, - "typescript": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", - "dev": true - }, - "undici-types": { - "version": "5.25.3", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz", - "integrity": "sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==", - "dev": true - }, - "uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "dev": true - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "workerpool": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", - "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", - "dev": true - }, - "wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "wrap-ansi-cjs": { - "version": "npm:wrap-ansi@7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - }, - "y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, - "yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "requires": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - } - }, - "yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", - "dev": true - }, - "yargs-unparser": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", - "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", - "dev": true, - "requires": { - "camelcase": "^6.0.0", - "decamelize": "^4.0.0", - "flat": "^5.0.2", - "is-plain-obj": "^2.1.0" - } - }, - "yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true - } } } From f01a34bfa858d2f24e3c6f01ea061f6bcd522cb0 Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Fri, 1 Nov 2024 15:46:48 +0800 Subject: [PATCH 10/24] update --- src/AzureAppConfigurationImpl.ts | 34 +++------- src/AzureAppConfigurationOptions.ts | 18 +++--- src/ConfigurationClientManager.ts | 97 +++++++++++++---------------- src/load.ts | 32 ++-------- src/refresh/RefreshTimer.ts | 59 ------------------ src/requestTracing/constants.ts | 2 +- src/requestTracing/utils.ts | 4 +- 7 files changed, 71 insertions(+), 175 deletions(-) diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index ffac36a5..5a81c82d 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -206,11 +206,12 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { const clientWrappers = await this.#clientManager.getClients(); if (clientWrappers.length === 0) { this.#clientManager.refreshClients(); - throw new Error("No client is available to connect to the target App Configuration store."); + throw new Error("No client is available to connect to the target app configuration store."); } + let successful: boolean; for (const clientWrapper of clientWrappers) { - let successful = false; + successful = false; try { const result = await funcToExecute(clientWrapper.client); this.#isFailoverRequest = false; @@ -235,7 +236,6 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { async #loadSelectedKeyValues(): Promise { // validate selectors const selectors = getValidKeyValueSelectors(this.#options?.selectors); - let loadedSettings: ConfigurationSetting[] = []; const funcToExecute = async (client) => { const loadedSettings: ConfigurationSetting[] = []; @@ -260,8 +260,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { return loadedSettings; }; - loadedSettings = await this.#executeWithFailoverPolicy(funcToExecute); - return loadedSettings; + return await this.#executeWithFailoverPolicy(funcToExecute) as ConfigurationSetting[]; } /** @@ -350,8 +349,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { return featureFlagSettings; }; - let featureFlagSettings: ConfigurationSetting[] = []; - featureFlagSettings = await this.#executeWithFailoverPolicy(funcToExecute); + const featureFlagSettings = await this.#executeWithFailoverPolicy(funcToExecute) as ConfigurationSetting[]; // parse feature flags const featureFlags = await Promise.all( @@ -480,13 +478,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { } if (needRefresh) { - try { - await this.#loadSelectedAndWatchedKeyValues(); - } catch (error) { - // if refresh failed, backoff - this.#refreshTimer.backoff(); - throw error; - } + await this.#loadSelectedAndWatchedKeyValues(); } this.#refreshTimer.reset(); @@ -505,7 +497,6 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { // check if any feature flag is changed const funcToExecute = async (client) => { - const needRefresh = false; for (const selector of this.#featureFlagSelectors) { const listOptions: ListConfigurationSettingsOptions = { keyFilter: `${featureFlagPrefix}${selector.keyFilter}`, @@ -525,18 +516,12 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { } } } - return needRefresh; + return false; }; const needRefresh: boolean = await this.#executeWithFailoverPolicy(funcToExecute); if (needRefresh) { - try { - await this.#loadFeatureFlags(); - } catch (error) { - // if refresh failed, backoff - this.#featureFlagRefreshTimer.backoff(); - throw error; - } + await this.#loadFeatureFlags(); } this.#featureFlagRefreshTimer.reset(); @@ -850,5 +835,6 @@ function getValidFeatureFlagSelectors(selectors?: SettingSelector[]): SettingSel } function isFailoverableError(error: any): boolean { - return isRestError(error) && (error.statusCode === 408 || error.statusCode === 429 || (error.statusCode !== undefined && error.statusCode >= 500)); + return isRestError(error) && (error.statusCode === 401 || error.statusCode === 403 || error.statusCode === 408 || error.statusCode === 429 || + (error.statusCode !== undefined && error.statusCode >= 500)); } diff --git a/src/AzureAppConfigurationOptions.ts b/src/AzureAppConfigurationOptions.ts index 9cef1102..4aa3f99d 100644 --- a/src/AzureAppConfigurationOptions.ts +++ b/src/AzureAppConfigurationOptions.ts @@ -12,15 +12,7 @@ export const MaxRetryDelayInMs = 60000; export interface AzureAppConfigurationOptions { /** - * Specifies whether enable replica discovery or not. - * - * @remarks - * If not specified, the default value is true. - */ - replicaDiscoveryEnabled?: boolean; - - /** - * Specify what key-values to include in the configuration provider. + * Specifies what key-values to include in the configuration provider. * * @remarks * If no selectors are specified then all key-values with no label will be included. @@ -55,4 +47,12 @@ export interface AzureAppConfigurationOptions { * Specifies options used to configure feature flags. */ featureFlagOptions?: FeatureFlagOptions; + + /** + * Specifies whether to enable replica discovery or not. + * + * @remarks + * If not specified, the default value is true. + */ + replicaDiscoveryEnabled?: boolean; } diff --git a/src/ConfigurationClientManager.ts b/src/ConfigurationClientManager.ts index ffb85766..58c4bf3e 100644 --- a/src/ConfigurationClientManager.ts +++ b/src/ConfigurationClientManager.ts @@ -8,24 +8,20 @@ import { AzureAppConfigurationOptions, MaxRetries, MaxRetryDelayInMs } from "./A import { isFailoverableEnv } from "./requestTracing/utils"; import * as RequestTracing from "./requestTracing/constants"; -const TCP_ORIGIN = "_origin._tcp"; -const ALT = "_alt"; -const TCP = "_tcp"; -const Endpoint = "Endpoint"; -const Id = "Id"; -const Secret = "Secret"; +const TCP_ORIGIN_KEY_NAME = "_origin._tcp"; +const ALT_KEY_NAME = "_alt"; +const TCP_KEY_NAME = "_tcp"; +const Endpoint_KEY_NAME = "Endpoint"; +const Id_KEY_NAME = "Id"; +const Secret_KEY_NAME = "Secret"; +const ConnectionStringRegex = /Endpoint=(.*);Id=(.*);Secret=(.*)/; const AzConfigDomainLabel = ".azconfig."; const AppConfigDomainLabel = ".appconfig."; const FallbackClientRefreshExpireInterval = 60 * 60 * 1000; // 1 hour in milliseconds const MinimalClientRefreshInterval = 30 * 1000; // 30 seconds in milliseconds -const SrvQueryTimeout = 5000; // 5 seconds +const SrvQueryTimeout = 5* 1000; // 5 seconds in milliseconds -interface IConfigurationClientManager { - getClients(): Promise; - refreshClients(): Promise; -} - -export class ConfigurationClientManager implements IConfigurationClientManager { +export class ConfigurationClientManager { isFailoverable: boolean; endpoint: string; #secret : string; @@ -44,24 +40,41 @@ export class ConfigurationClientManager implements IConfigurationClientManager { appConfigOptions?: AzureAppConfigurationOptions ) { let staticClient: AppConfigurationClient; - let options: AzureAppConfigurationOptions; + let options: AzureAppConfigurationOptions | undefined; - if (typeof connectionStringOrEndpoint === "string") { + if (typeof connectionStringOrEndpoint === "string" && !instanceOfTokenCredential(credentialOrOptions)) { const connectionString = connectionStringOrEndpoint; options = credentialOrOptions as AzureAppConfigurationOptions; this.#clientOptions = getClientOptions(options); staticClient = new AppConfigurationClient(connectionString, this.#clientOptions); - this.#secret = parseConnectionString(connectionString, Secret); - this.#id = parseConnectionString(connectionString, Id); - // TODO: need to check if it's CDN or not - this.endpoint = parseConnectionString(connectionString, Endpoint); + const regexMatch = connectionString.match(ConnectionStringRegex); + if (regexMatch) { + this.endpoint = regexMatch[1]; + this.#id = regexMatch[2]; + this.#secret = regexMatch[3]; + } else { + throw new Error(`Invalid connection string. Valid connection strings should match the regex '${ConnectionStringRegex.source}'.`); + } + } else if ((connectionStringOrEndpoint instanceof URL || typeof connectionStringOrEndpoint === "string") && instanceOfTokenCredential(credentialOrOptions)) { + let endpoint = connectionStringOrEndpoint; + // ensure string is a valid URL. + if (typeof endpoint === "string") { + try { + endpoint = new URL(endpoint); + } catch (error) { + if (error.code === "ERR_INVALID_URL") { + throw new Error("Invalid endpoint URL.", { cause: error }); + } else { + throw error; + } + } + } - } else if (connectionStringOrEndpoint instanceof URL) { const credential = credentialOrOptions as TokenCredential; options = appConfigOptions as AzureAppConfigurationOptions; this.#clientOptions = getClientOptions(options); staticClient = new AppConfigurationClient(connectionStringOrEndpoint.toString(), credential, this.#clientOptions); - this.endpoint = connectionStringOrEndpoint.toString(); + this.endpoint = endpoint.toString(); this.#credential = credential; } else { throw new Error("A connection string or an endpoint with credential must be specified to create a client."); @@ -84,7 +97,7 @@ export class ConfigurationClientManager implements IConfigurationClientManager { await this.#discoverFallbackClients(host); } - // Filter static clients where BackoffEndTime is less than or equal to now + // Filter static clients whose backoff time has ended let availableClients = this.#staticClients.filter(client => client.backoffEndTime <= currentTime); // If there are dynamic clients, filter and concatenate them if (this.#dynamicClients && this.#dynamicClients.length > 0) { @@ -101,8 +114,8 @@ export class ConfigurationClientManager implements IConfigurationClientManager { if (this.isFailoverable && currentTime > new Date(this.#lastFallbackClientRefreshAttempt + MinimalClientRefreshInterval).getTime()) { this.#lastFallbackClientRefreshAttempt = currentTime; - const url = new URL(this.endpoint); - await this.#discoverFallbackClients(url.hostname); + const host = new URL(this.endpoint).hostname; + await this.#discoverFallbackClients(host); } } @@ -176,7 +189,7 @@ async function querySrvTargetHost(host: string): Promise { try { // Look up SRV records for the origin host - const originRecords = await dns.resolveSrv(`${TCP_ORIGIN}.${host}`); + const originRecords = await dns.resolveSrv(`${TCP_ORIGIN_KEY_NAME}.${host}`); if (originRecords.length === 0) { return results; } @@ -189,9 +202,9 @@ async function querySrvTargetHost(host: string): Promise { let index = 0; let moreAltRecordsExist = true; while (moreAltRecordsExist) { - const currentAlt = `${ALT}${index}`; + const currentAlt = `${ALT_KEY_NAME}${index}`; try { - const altRecords = await dns.resolveSrv(`${currentAlt}.${TCP}.${originHost}`); + const altRecords = await dns.resolveSrv(`${currentAlt}.${TCP_KEY_NAME}.${originHost}`); if (altRecords.length === 0) { moreAltRecordsExist = false; break; // No more alternate records, exit loop @@ -223,30 +236,6 @@ async function querySrvTargetHost(host: string): Promise { return results; } -/** - * Parses the connection string to extract the value associated with a specific token. - */ -function parseConnectionString(connectionString, token: string): string { - if (!connectionString) { - throw new Error("connectionString is empty"); - } - - // Token format is "token=" - const searchToken = `${token}=`; - const startIndex = connectionString.indexOf(searchToken); - if (startIndex === -1) { - throw new Error(`Token ${token} not found in connectionString`); - } - - // Move startIndex to the beginning of the token value - const valueStartIndex = startIndex + searchToken.length; - const endIndex = connectionString.indexOf(";", valueStartIndex); - const valueEndIndex = endIndex === -1 ? connectionString.length : endIndex; - - // Extract and return the token value - return connectionString.substring(valueStartIndex, valueEndIndex); -} - /** * Builds a connection string from the given endpoint, secret, and id. * Returns an empty string if either secret or id is empty. @@ -256,7 +245,7 @@ function buildConnectionString(endpoint, secret, id: string): string { return ""; } - return `${Endpoint}=${endpoint};${Id}=${id};${Secret}=${secret}`; + return `${Endpoint_KEY_NAME}=${endpoint};${Id_KEY_NAME}=${id};${Secret_KEY_NAME}=${secret}`; } /** @@ -315,3 +304,7 @@ export function getClientOptions(options?: AzureAppConfigurationOptions): AppCon }); } +export function instanceOfTokenCredential(obj: unknown) { + return obj && typeof obj === "object" && "getToken" in obj && typeof obj.getToken === "function"; +} + diff --git a/src/load.ts b/src/load.ts index cfe95042..5a882ac4 100644 --- a/src/load.ts +++ b/src/load.ts @@ -5,7 +5,7 @@ import { TokenCredential } from "@azure/identity"; import { AzureAppConfiguration } from "./AzureAppConfiguration.js"; import { AzureAppConfigurationImpl } from "./AzureAppConfigurationImpl.js"; import { AzureAppConfigurationOptions } from "./AzureAppConfigurationOptions.js"; -import { ConfigurationClientManager } from "./ConfigurationClientManager.js"; +import { ConfigurationClientManager, instanceOfTokenCredential } from "./ConfigurationClientManager.js"; const MIN_DELAY_FOR_UNHANDLED_ERROR: number = 5000; // 5 seconds @@ -31,32 +31,12 @@ export async function load( ): Promise { const startTimestamp = Date.now(); let options: AzureAppConfigurationOptions | undefined; - let clientManager: ConfigurationClientManager; + const clientManager = new ConfigurationClientManager(connectionStringOrEndpoint, credentialOrOptions, appConfigOptions); - // input validation - if (typeof connectionStringOrEndpoint === "string" && !instanceOfTokenCredential(credentialOrOptions)) { - const connectionString = connectionStringOrEndpoint; + if (!instanceOfTokenCredential(credentialOrOptions)) { options = credentialOrOptions as AzureAppConfigurationOptions; - clientManager = new ConfigurationClientManager(connectionString, options); - } else if ((connectionStringOrEndpoint instanceof URL || typeof connectionStringOrEndpoint === "string") && instanceOfTokenCredential(credentialOrOptions)) { - let endpoint = connectionStringOrEndpoint; - // ensure string is a valid URL. - if (typeof endpoint === "string") { - try { - endpoint = new URL(endpoint); - } catch (error) { - if (error.code === "ERR_INVALID_URL") { - throw new Error("Invalid endpoint URL.", { cause: error }); - } else { - throw error; - } - } - } - const credential = credentialOrOptions as TokenCredential; - options = appConfigOptions; - clientManager = new ConfigurationClientManager(endpoint, credential, options); } else { - throw new Error("A connection string or an endpoint with credential must be specified to create a client."); + options = appConfigOptions; } try { @@ -74,7 +54,3 @@ export async function load( throw error; } } - -function instanceOfTokenCredential(obj: unknown) { - return obj && typeof obj === "object" && "getToken" in obj && typeof obj.getToken === "function"; -} diff --git a/src/refresh/RefreshTimer.ts b/src/refresh/RefreshTimer.ts index ce485947..45fdf0b3 100644 --- a/src/refresh/RefreshTimer.ts +++ b/src/refresh/RefreshTimer.ts @@ -1,30 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -/** - * The backoff time is between the minimum and maximum backoff time, based on the number of attempts. - * An exponential backoff strategy is used, with a jitter factor to prevent clients from retrying at the same time. - * - * The backoff time is calculated as follows: - * - `basic backoff time` = `MinimumBackoffInMs` * 2 ^ `attempts`, and it is no larger than the `MaximumBackoffInMs`. - * - based on jitter ratio, the jittered time is between [-1, 1) * `JitterRatio` * basic backoff time. - * - the final backoff time is the basic backoff time plus the jittered time. - * - * Note: the backoff time usually is no larger than the refresh interval, which is specified by the user. - * - If the interval is less than the minimum backoff, the interval is used. - * - If the interval is between the minimum and maximum backoff, the interval is used as the maximum backoff. - * - Because of the jitter, the maximum backoff time is actually `MaximumBackoffInMs` * (1 + `JitterRatio`). - */ - -const MIN_BACKOFF_IN_MS = 30 * 1000; // 30s -const MAX_BACKOFF_IN_MS = 10 * 60 * 1000; // 10min -const MAX_SAFE_EXPONENTIAL = 30; // Used to avoid overflow. bitwise operations in JavaScript are limited to 32 bits. It overflows at 2^31 - 1. -const JITTER_RATIO = 0.25; - export class RefreshTimer { - #minBackoff: number = MIN_BACKOFF_IN_MS; - #maxBackoff: number = MAX_BACKOFF_IN_MS; - #failedAttempts: number = 0; #backoffEnd: number; // Timestamp #interval: number; @@ -43,43 +20,7 @@ export class RefreshTimer { return Date.now() >= this.#backoffEnd; } - backoff(): void { - this.#failedAttempts += 1; - this.#backoffEnd = Date.now() + this.#calculateBackoffTime(); - } - reset(): void { - this.#failedAttempts = 0; this.#backoffEnd = Date.now() + this.#interval; } - - #calculateBackoffTime(): number { - let minBackoffMs: number; - let maxBackoffMs: number; - if (this.#interval <= this.#minBackoff) { - return this.#interval; - } - - // _minBackoff <= _interval - if (this.#interval <= this.#maxBackoff) { - minBackoffMs = this.#minBackoff; - maxBackoffMs = this.#interval; - } else { - minBackoffMs = this.#minBackoff; - maxBackoffMs = this.#maxBackoff; - } - - // exponential: minBackoffMs * 2^(failedAttempts-1) - const exponential = Math.min(this.#failedAttempts - 1, MAX_SAFE_EXPONENTIAL); - let calculatedBackoffMs = minBackoffMs * (1 << exponential); - if (calculatedBackoffMs > maxBackoffMs) { - calculatedBackoffMs = maxBackoffMs; - } - - // jitter: random value between [-1, 1) * jitterRatio * calculatedBackoffMs - const jitter = JITTER_RATIO * (Math.random() * 2 - 1); - - return calculatedBackoffMs * (1 + jitter); - } - } diff --git a/src/requestTracing/constants.ts b/src/requestTracing/constants.ts index 7d43e0a1..4ddbe2d9 100644 --- a/src/requestTracing/constants.ts +++ b/src/requestTracing/constants.ts @@ -43,7 +43,7 @@ export enum RequestType { STARTUP = "Startup", WATCH = "Watch" } -export const FAILOVER_REQUEST_TYPE = "FailoverRequest"; +export const FAILOVER_REQUEST_TAG = "Failover"; // Tag names export const KEY_VAULT_CONFIGURED_TAG = "UsesKeyVault"; diff --git a/src/requestTracing/utils.ts b/src/requestTracing/utils.ts index 35449337..a4860d5f 100644 --- a/src/requestTracing/utils.ts +++ b/src/requestTracing/utils.ts @@ -20,7 +20,7 @@ import { RequestType, SERVICE_FABRIC_ENV_VAR, CORRELATION_CONTEXT_HEADER_NAME, - FAILOVER_REQUEST_TYPE + FAILOVER_REQUEST_TAG } from "./constants"; // Utils @@ -104,7 +104,7 @@ export function createCorrelationContextHeader(options: AzureAppConfigurationOpt } if (isFailoverRequest) { - contextParts.push(FAILOVER_REQUEST_TYPE); + contextParts.push(FAILOVER_REQUEST_TAG); } return contextParts.join(","); From 6357f5932afef32a645f82e97f0a1a53149a6742 Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Tue, 5 Nov 2024 13:20:02 +0800 Subject: [PATCH 11/24] update --- src/AzureAppConfigurationImpl.ts | 4 +-- src/ConfigurationClientManager.ts | 42 +++++++++++++++---------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index 5a81c82d..8ab539ad 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { ConfigurationSetting, ConfigurationSettingId, GetConfigurationSettingOptions, GetConfigurationSettingResponse, ListConfigurationSettingsOptions, featureFlagPrefix, isFeatureFlag } from "@azure/app-configuration"; +import { AppConfigurationClient, ConfigurationSetting, ConfigurationSettingId, GetConfigurationSettingOptions, GetConfigurationSettingResponse, ListConfigurationSettingsOptions, featureFlagPrefix, isFeatureFlag } from "@azure/app-configuration"; import { isRestError } from "@azure/core-rest-pipeline"; import { AzureAppConfiguration, ConfigurationObjectConstructionOptions } from "./AzureAppConfiguration.js"; import { AzureAppConfigurationOptions } from "./AzureAppConfigurationOptions.js"; @@ -202,7 +202,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { }; } - async #executeWithFailoverPolicy(funcToExecute) { + async #executeWithFailoverPolicy(funcToExecute: (client: AppConfigurationClient) => Promise): Promise { const clientWrappers = await this.#clientManager.getClients(); if (clientWrappers.length === 0) { this.#clientManager.refreshClients(); diff --git a/src/ConfigurationClientManager.ts b/src/ConfigurationClientManager.ts index 58c4bf3e..30798522 100644 --- a/src/ConfigurationClientManager.ts +++ b/src/ConfigurationClientManager.ts @@ -2,24 +2,23 @@ // Licensed under the MIT license. import { AppConfigurationClient, AppConfigurationClientOptions } from "@azure/app-configuration"; -import { ConfigurationClientWrapper } from "./ConfigurationClientWrapper"; +import { ConfigurationClientWrapper } from "./ConfigurationClientWrapper.js"; import { TokenCredential } from "@azure/identity"; -import { AzureAppConfigurationOptions, MaxRetries, MaxRetryDelayInMs } from "./AzureAppConfigurationOptions"; -import { isFailoverableEnv } from "./requestTracing/utils"; -import * as RequestTracing from "./requestTracing/constants"; +import { AzureAppConfigurationOptions, MaxRetries, MaxRetryDelayInMs } from "./AzureAppConfigurationOptions.js"; +import { isFailoverableEnv } from "./requestTracing/utils.js"; +import * as RequestTracing from "./requestTracing/constants.js"; const TCP_ORIGIN_KEY_NAME = "_origin._tcp"; const ALT_KEY_NAME = "_alt"; const TCP_KEY_NAME = "_tcp"; -const Endpoint_KEY_NAME = "Endpoint"; -const Id_KEY_NAME = "Id"; -const Secret_KEY_NAME = "Secret"; -const ConnectionStringRegex = /Endpoint=(.*);Id=(.*);Secret=(.*)/; -const AzConfigDomainLabel = ".azconfig."; -const AppConfigDomainLabel = ".appconfig."; -const FallbackClientRefreshExpireInterval = 60 * 60 * 1000; // 1 hour in milliseconds -const MinimalClientRefreshInterval = 30 * 1000; // 30 seconds in milliseconds -const SrvQueryTimeout = 5* 1000; // 5 seconds in milliseconds +const ENDPOINT_KEY_NAME = "Endpoint"; +const ID_KEY_NAME = "Id"; +const SECRET_KEY_NAME = "Secret"; +const AZCONFIG_DOMAIN_LABEL = ".azconfig."; +const APPCONFIG_DOMAIN_LABEL = ".appconfig."; +const FALLBACK_CLIENT_REFRESH_EXPIRE_INTERVAL = 60 * 60 * 1000; // 1 hour in milliseconds +const MINIMAL_CLIENT_REFRESH_INTERVAL = 30 * 1000; // 30 seconds in milliseconds +const SRV_QUERY_TIMEOUT = 5* 1000; // 5 seconds in milliseconds export class ConfigurationClientManager { isFailoverable: boolean; @@ -47,6 +46,7 @@ export class ConfigurationClientManager { options = credentialOrOptions as AzureAppConfigurationOptions; this.#clientOptions = getClientOptions(options); staticClient = new AppConfigurationClient(connectionString, this.#clientOptions); + const ConnectionStringRegex = /Endpoint=(.*);Id=(.*);Secret=(.*)/; const regexMatch = connectionString.match(ConnectionStringRegex); if (regexMatch) { this.endpoint = regexMatch[1]; @@ -112,7 +112,7 @@ export class ConfigurationClientManager { async refreshClients() { const currentTime = Date.now(); if (this.isFailoverable && - currentTime > new Date(this.#lastFallbackClientRefreshAttempt + MinimalClientRefreshInterval).getTime()) { + currentTime > new Date(this.#lastFallbackClientRefreshAttempt + MINIMAL_CLIENT_REFRESH_INTERVAL).getTime()) { this.#lastFallbackClientRefreshAttempt = currentTime; const host = new URL(this.endpoint).hostname; await this.#discoverFallbackClients(host); @@ -121,7 +121,7 @@ export class ConfigurationClientManager { async #discoverFallbackClients(host) { const timeout = setTimeout(() => { - }, SrvQueryTimeout); + }, SRV_QUERY_TIMEOUT); const srvResults = await querySrvTargetHost(host); try { @@ -167,10 +167,10 @@ export class ConfigurationClientManager { } #isFallbackClientDiscoveryDue(dateTime) { - return dateTime >= this.#lastFallbackClientRefreshAttempt + MinimalClientRefreshInterval + return dateTime >= this.#lastFallbackClientRefreshAttempt + MINIMAL_CLIENT_REFRESH_INTERVAL && (!this.#dynamicClients || this.#dynamicClients.every(client => dateTime < client.backoffEndTime) - || dateTime >= this.#lastFallbackClientRefreshTime + FallbackClientRefreshExpireInterval); + || dateTime >= this.#lastFallbackClientRefreshTime + FALLBACK_CLIENT_REFRESH_EXPIRE_INTERVAL); } } @@ -181,8 +181,8 @@ async function querySrvTargetHost(host: string): Promise { const results: string[] = []; let dns; - if (isFailoverableEnv()) { - dns = require("dns/promises"); + if (typeof global !== "undefined" && global.dns) { + dns = global.dns; } else { throw new Error("Failover is not supported in the current environment."); } @@ -245,7 +245,7 @@ function buildConnectionString(endpoint, secret, id: string): string { return ""; } - return `${Endpoint_KEY_NAME}=${endpoint};${Id_KEY_NAME}=${id};${Secret_KEY_NAME}=${secret}`; + return `${ENDPOINT_KEY_NAME}=${endpoint};${ID_KEY_NAME}=${id};${SECRET_KEY_NAME}=${secret}`; } /** @@ -254,7 +254,7 @@ function buildConnectionString(endpoint, secret, id: string): string { export function getValidDomain(endpoint: string): string { try { const url = new URL(endpoint); - const trustedDomainLabels = [AzConfigDomainLabel, AppConfigDomainLabel]; + const trustedDomainLabels = [AZCONFIG_DOMAIN_LABEL, APPCONFIG_DOMAIN_LABEL]; const host = url.hostname.toLowerCase(); for (const label of trustedDomainLabels) { From 7fbf05f7e833da1e1362aef0f0dcb792128cd858 Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Tue, 5 Nov 2024 13:57:16 +0800 Subject: [PATCH 12/24] update failover error --- src/AzureAppConfigurationImpl.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index 8ab539ad..f9d76a77 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -835,6 +835,6 @@ function getValidFeatureFlagSelectors(selectors?: SettingSelector[]): SettingSel } function isFailoverableError(error: any): boolean { - return isRestError(error) && (error.statusCode === 401 || error.statusCode === 403 || error.statusCode === 408 || error.statusCode === 429 || - (error.statusCode !== undefined && error.statusCode >= 500)); + return isRestError(error) && error.statusCode !== undefined && + (error.statusCode === 401 || error.statusCode === 403 || error.statusCode === 408 || error.statusCode === 429 || error.statusCode >= 500); } From cc13f34b64864edf5fd41f287241d42a0f49fa59 Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Thu, 7 Nov 2024 14:29:44 +0800 Subject: [PATCH 13/24] update --- src/ConfigurationClientManager.ts | 170 ++++++++++++++---------------- src/load.ts | 1 + src/requestTracing/utils.ts | 11 +- 3 files changed, 81 insertions(+), 101 deletions(-) diff --git a/src/ConfigurationClientManager.ts b/src/ConfigurationClientManager.ts index 30798522..22f1519e 100644 --- a/src/ConfigurationClientManager.ts +++ b/src/ConfigurationClientManager.ts @@ -5,7 +5,7 @@ import { AppConfigurationClient, AppConfigurationClientOptions } from "@azure/ap import { ConfigurationClientWrapper } from "./ConfigurationClientWrapper.js"; import { TokenCredential } from "@azure/identity"; import { AzureAppConfigurationOptions, MaxRetries, MaxRetryDelayInMs } from "./AzureAppConfigurationOptions.js"; -import { isFailoverableEnv } from "./requestTracing/utils.js"; +import { isBrowser, isWebWorker } from "./requestTracing/utils.js"; import * as RequestTracing from "./requestTracing/constants.js"; const TCP_ORIGIN_KEY_NAME = "_origin._tcp"; @@ -14,19 +14,20 @@ const TCP_KEY_NAME = "_tcp"; const ENDPOINT_KEY_NAME = "Endpoint"; const ID_KEY_NAME = "Id"; const SECRET_KEY_NAME = "Secret"; -const AZCONFIG_DOMAIN_LABEL = ".azconfig."; -const APPCONFIG_DOMAIN_LABEL = ".appconfig."; +const TRUSTED_DOMAIN_LABELS = [".azconfig.", ".appconfig."]; const FALLBACK_CLIENT_REFRESH_EXPIRE_INTERVAL = 60 * 60 * 1000; // 1 hour in milliseconds const MINIMAL_CLIENT_REFRESH_INTERVAL = 30 * 1000; // 30 seconds in milliseconds -const SRV_QUERY_TIMEOUT = 5* 1000; // 5 seconds in milliseconds +const SRV_QUERY_TIMEOUT = 30 * 1000; // 30 seconds in milliseconds export class ConfigurationClientManager { isFailoverable: boolean; + dns: any; endpoint: string; #secret : string; #id : string; #credential: TokenCredential; #clientOptions: AppConfigurationClientOptions | undefined; + #appConfigOptions: AzureAppConfigurationOptions | undefined; #validDomain: string; #staticClients: ConfigurationClientWrapper[]; #dynamicClients: ConfigurationClientWrapper[]; @@ -39,12 +40,11 @@ export class ConfigurationClientManager { appConfigOptions?: AzureAppConfigurationOptions ) { let staticClient: AppConfigurationClient; - let options: AzureAppConfigurationOptions | undefined; if (typeof connectionStringOrEndpoint === "string" && !instanceOfTokenCredential(credentialOrOptions)) { const connectionString = connectionStringOrEndpoint; - options = credentialOrOptions as AzureAppConfigurationOptions; - this.#clientOptions = getClientOptions(options); + this.#appConfigOptions = credentialOrOptions as AzureAppConfigurationOptions; + this.#clientOptions = getClientOptions(this.#appConfigOptions); staticClient = new AppConfigurationClient(connectionString, this.#clientOptions); const ConnectionStringRegex = /Endpoint=(.*);Id=(.*);Secret=(.*)/; const regexMatch = connectionString.match(ConnectionStringRegex); @@ -71,8 +71,8 @@ export class ConfigurationClientManager { } const credential = credentialOrOptions as TokenCredential; - options = appConfigOptions as AzureAppConfigurationOptions; - this.#clientOptions = getClientOptions(options); + this.#appConfigOptions = appConfigOptions as AzureAppConfigurationOptions; + this.#clientOptions = getClientOptions(this.#appConfigOptions); staticClient = new AppConfigurationClient(connectionStringOrEndpoint.toString(), credential, this.#clientOptions); this.endpoint = endpoint.toString(); this.#credential = credential; @@ -82,7 +82,23 @@ export class ConfigurationClientManager { this.#staticClients = [new ConfigurationClientWrapper(this.endpoint, staticClient)]; this.#validDomain = getValidDomain(this.endpoint); - this.isFailoverable = (options?.replicaDiscoveryEnabled ?? true) && isFailoverableEnv(); + } + + async init() { + if (this.#appConfigOptions?.replicaDiscoveryEnabled === false || isBrowser() || isWebWorker()) { + this.isFailoverable = false; + return; + } + + try { + this.dns = await import("dns/promises"); + }catch (error) { + this.isFailoverable = false; + console.warn("Failed to load the dns module:", error.message); + return; + } + + this.isFailoverable = true; } async getClients() : Promise { @@ -120,50 +136,37 @@ export class ConfigurationClientManager { } async #discoverFallbackClients(host) { - const timeout = setTimeout(() => { - }, SRV_QUERY_TIMEOUT); - const srvResults = await querySrvTargetHost(host); - + let result; try { - const result = await Promise.race([srvResults, timeout]); - - if (result === timeout) { - throw new Error("SRV record query timed out."); - } + result = await Promise.race([ + new Promise((_, reject) => setTimeout(() => reject(new Error("SRV record query timed out.")), SRV_QUERY_TIMEOUT)), + this.#querySrvTargetHost(host) + ]); + } catch (error) { + throw new Error(`Fail to build fallback clients, ${error.message}`); + } - const srvTargetHosts = result as string[]; - // Shuffle the list of SRV target hosts - for (let i = srvTargetHosts.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [srvTargetHosts[i], srvTargetHosts[j]] = [srvTargetHosts[j], srvTargetHosts[i]]; - } + const srvTargetHosts = result as string[]; + // Shuffle the list of SRV target hosts + for (let i = srvTargetHosts.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [srvTargetHosts[i], srvTargetHosts[j]] = [srvTargetHosts[j], srvTargetHosts[i]]; + } - const newDynamicClients: ConfigurationClientWrapper[] = []; - for (const host of srvTargetHosts) { - if (isValidEndpoint(host, this.#validDomain)) { - const targetEndpoint = `https://${host}`; - if (targetEndpoint.toLowerCase() === this.endpoint.toLowerCase()) { - continue; - } - const client = this.#newConfigurationClient(targetEndpoint); - newDynamicClients.push(new ConfigurationClientWrapper(targetEndpoint, client)); + const newDynamicClients: ConfigurationClientWrapper[] = []; + for (const host of srvTargetHosts) { + if (isValidEndpoint(host, this.#validDomain)) { + const targetEndpoint = `https://${host}`; + if (targetEndpoint.toLowerCase() === this.endpoint.toLowerCase()) { + continue; } + const client = this.#credential ? new AppConfigurationClient(targetEndpoint, this.#credential, this.#clientOptions) : new AppConfigurationClient(buildConnectionString(targetEndpoint, this.#secret, this.#id), this.#clientOptions); + newDynamicClients.push(new ConfigurationClientWrapper(targetEndpoint, client)); } - - this.#dynamicClients = newDynamicClients; - this.#lastFallbackClientRefreshTime = Date.now(); - } catch (err) { - console.warn(`Fail to build fallback clients, ${err.message}`); - } - } - - #newConfigurationClient(endpoint) { - if (this.#credential) { - return new AppConfigurationClient(endpoint, this.#credential, this.#clientOptions); } - const connectionStr = buildConnectionString(endpoint, this.#secret, this.#id); - return new AppConfigurationClient(connectionStr, this.#clientOptions); + this.#dynamicClients = newDynamicClients; + this.#lastFallbackClientRefreshTime = Date.now(); } #isFallbackClientDiscoveryDue(dateTime) { @@ -172,41 +175,31 @@ export class ConfigurationClientManager { || this.#dynamicClients.every(client => dateTime < client.backoffEndTime) || dateTime >= this.#lastFallbackClientRefreshTime + FALLBACK_CLIENT_REFRESH_EXPIRE_INTERVAL); } -} -/** + /** * Query SRV records and return target hosts. */ -async function querySrvTargetHost(host: string): Promise { - const results: string[] = []; - let dns; - - if (typeof global !== "undefined" && global.dns) { - dns = global.dns; - } else { - throw new Error("Failover is not supported in the current environment."); - } + async #querySrvTargetHost(host: string): Promise { + const results: string[] = []; - try { - // Look up SRV records for the origin host - const originRecords = await dns.resolveSrv(`${TCP_ORIGIN_KEY_NAME}.${host}`); - if (originRecords.length === 0) { - return results; - } + try { + // Look up SRV records for the origin host + const originRecords = await this.dns.resolveSrv(`${TCP_ORIGIN_KEY_NAME}.${host}`); + if (originRecords.length === 0) { + return results; + } + + // Add the first origin record to results + const originHost = originRecords[0].name; + results.push(originHost); - // Add the first origin record to results - const originHost = originRecords[0].name; - results.push(originHost); - - // Look up SRV records for alternate hosts - let index = 0; - let moreAltRecordsExist = true; - while (moreAltRecordsExist) { - const currentAlt = `${ALT_KEY_NAME}${index}`; - try { - const altRecords = await dns.resolveSrv(`${currentAlt}.${TCP_KEY_NAME}.${originHost}`); + // Look up SRV records for alternate hosts + let index = 0; + // eslint-disable-next-line no-constant-condition + while (true) { + const currentAlt = `${ALT_KEY_NAME}${index}`; + const altRecords = await this.dns.resolveSrv(`${currentAlt}.${TCP_KEY_NAME}.${originHost}`); if (altRecords.length === 0) { - moreAltRecordsExist = false; break; // No more alternate records, exit loop } @@ -217,23 +210,17 @@ async function querySrvTargetHost(host: string): Promise { } }); index++; - } catch (err) { - if (err.code === "ENOTFOUND") { - break; // No more alternate records, exit loop - } else { - throw new Error(`Failed to lookup alternate SRV records: ${err.message}`); - } + } + } catch (err) { + if (err.code === "ENOTFOUND") { + return results; // No more SRV records found, return results + } else { + throw new Error(`Failed to lookup SRV records: ${err.message}`); } } - } catch (err) { - if (err.code === "ENOTFOUND") { - return results; // No SRV records found, return empty array - } else { - throw new Error(`Failed to lookup SRV records: ${err.message}`); - } - } - return results; + return results; + } } /** @@ -254,10 +241,9 @@ function buildConnectionString(endpoint, secret, id: string): string { export function getValidDomain(endpoint: string): string { try { const url = new URL(endpoint); - const trustedDomainLabels = [AZCONFIG_DOMAIN_LABEL, APPCONFIG_DOMAIN_LABEL]; const host = url.hostname.toLowerCase(); - for (const label of trustedDomainLabels) { + for (const label of TRUSTED_DOMAIN_LABELS) { const index = host.lastIndexOf(label); if (index !== -1) { return host.substring(index); diff --git a/src/load.ts b/src/load.ts index 5a882ac4..4d24174e 100644 --- a/src/load.ts +++ b/src/load.ts @@ -32,6 +32,7 @@ export async function load( const startTimestamp = Date.now(); let options: AzureAppConfigurationOptions | undefined; const clientManager = new ConfigurationClientManager(connectionStringOrEndpoint, credentialOrOptions, appConfigOptions); + await clientManager.init(); if (!instanceOfTokenCredential(credentialOrOptions)) { options = credentialOrOptions as AzureAppConfigurationOptions; diff --git a/src/requestTracing/utils.ts b/src/requestTracing/utils.ts index a4860d5f..8a2fdbc4 100644 --- a/src/requestTracing/utils.ts +++ b/src/requestTracing/utils.ts @@ -153,7 +153,7 @@ function isDevEnvironment(): boolean { return false; } -function isBrowser() { +export function isBrowser() { // https://developer.mozilla.org/en-US/docs/Web/API/Window const isWindowDefinedAsExpected = typeof window === "object" && typeof Window === "function" && window instanceof Window; // https://developer.mozilla.org/en-US/docs/Web/API/Document @@ -162,7 +162,7 @@ function isBrowser() { return isWindowDefinedAsExpected && isDocumentDefinedAsExpected; } -function isWebWorker() { +export function isWebWorker() { // https://developer.mozilla.org/en-US/docs/Web/API/WorkerGlobalScope const workerGlobalScopeDefined = typeof WorkerGlobalScope !== "undefined"; // https://developer.mozilla.org/en-US/docs/Web/API/WorkerNavigator @@ -173,10 +173,3 @@ function isWebWorker() { return workerGlobalScopeDefined && importScriptsAsGlobalFunction && isNavigatorDefinedAsExpected; } -export function isFailoverableEnv() { - if (isBrowser() || isWebWorker()) { - return false; - } - - return true; -} From 8802f748cf974e7007c42fe0094e5568ab39ee81 Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Fri, 8 Nov 2024 10:46:21 +0800 Subject: [PATCH 14/24] update --- src/ConfigurationClientManager.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ConfigurationClientManager.ts b/src/ConfigurationClientManager.ts index 22f1519e..a19c2827 100644 --- a/src/ConfigurationClientManager.ts +++ b/src/ConfigurationClientManager.ts @@ -177,8 +177,8 @@ export class ConfigurationClientManager { } /** - * Query SRV records and return target hosts. - */ + * Query SRV records and return target hosts. + */ async #querySrvTargetHost(host: string): Promise { const results: string[] = []; From 3b4839c51a46aa247b9f18cf9b9ec2f87a9cedb6 Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Tue, 12 Nov 2024 14:50:01 +0800 Subject: [PATCH 15/24] update failoverable error with 'ENOTFOUND' --- src/AzureAppConfigurationImpl.ts | 4 ++-- src/requestTracing/constants.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index f9d76a77..418067e0 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -835,6 +835,6 @@ function getValidFeatureFlagSelectors(selectors?: SettingSelector[]): SettingSel } function isFailoverableError(error: any): boolean { - return isRestError(error) && error.statusCode !== undefined && - (error.statusCode === 401 || error.statusCode === 403 || error.statusCode === 408 || error.statusCode === 429 || error.statusCode >= 500); + return isRestError(error) && (error.code === 'ENOTFOUND' || + (error.statusCode !== undefined && (error.statusCode === 401 || error.statusCode === 403 || error.statusCode === 408 || error.statusCode === 429 || error.statusCode >= 500))); } diff --git a/src/requestTracing/constants.ts b/src/requestTracing/constants.ts index 4ddbe2d9..60dbb81a 100644 --- a/src/requestTracing/constants.ts +++ b/src/requestTracing/constants.ts @@ -43,7 +43,7 @@ export enum RequestType { STARTUP = "Startup", WATCH = "Watch" } -export const FAILOVER_REQUEST_TAG = "Failover"; // Tag names +export const FAILOVER_REQUEST_TAG = "Failover"; export const KEY_VAULT_CONFIGURED_TAG = "UsesKeyVault"; From a43af88ee6b5660bea7dfd38990eed05c1eda247 Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Tue, 12 Nov 2024 14:50:20 +0800 Subject: [PATCH 16/24] fix lint --- src/AzureAppConfigurationImpl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index 418067e0..535765a5 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -835,6 +835,6 @@ function getValidFeatureFlagSelectors(selectors?: SettingSelector[]): SettingSel } function isFailoverableError(error: any): boolean { - return isRestError(error) && (error.code === 'ENOTFOUND' || + return isRestError(error) && (error.code === "ENOTFOUND" || (error.statusCode !== undefined && (error.statusCode === 401 || error.statusCode === 403 || error.statusCode === 408 || error.statusCode === 429 || error.statusCode >= 500))); } From 9e59790e669b52abfec6b5cd0837a93afed08b04 Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Thu, 14 Nov 2024 14:11:34 +0800 Subject: [PATCH 17/24] update --- src/AzureAppConfigurationImpl.ts | 11 ++- src/ConfigurationClientManager.ts | 117 +++++++++++++++--------------- src/ConfigurationClientWrapper.ts | 20 ++--- test/clientOptions.test.ts | 4 - test/failover.test.ts | 20 ++--- test/featureFlag.test.ts | 4 +- test/json.test.ts | 4 +- test/keyvault.test.ts | 7 -- test/load.test.ts | 4 +- test/refresh.test.ts | 18 ----- test/requestTracing.test.ts | 67 ++++------------- 11 files changed, 100 insertions(+), 176 deletions(-) diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index 535765a5..95bae535 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -36,7 +36,6 @@ import { RefreshTimer } from "./refresh/RefreshTimer.js"; import { getConfigurationSettingWithTrace, listConfigurationSettingsWithTrace, requestTracingEnabled } from "./requestTracing/utils.js"; import { KeyFilter, LabelFilter, SettingSelector } from "./types.js"; import { ConfigurationClientManager } from "./ConfigurationClientManager.js"; -import { updateClientBackoffStatus } from "./ConfigurationClientWrapper.js"; type PagedSettingSelector = SettingSelector & { /** @@ -206,7 +205,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { const clientWrappers = await this.#clientManager.getClients(); if (clientWrappers.length === 0) { this.#clientManager.refreshClients(); - throw new Error("No client is available to connect to the target app configuration store."); + console.warn("Refresh skipped because no endpoint is accessible."); } let successful: boolean; @@ -216,11 +215,11 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { const result = await funcToExecute(clientWrapper.client); this.#isFailoverRequest = false; successful = true; - updateClientBackoffStatus(clientWrapper, successful); + clientWrapper.updateBackoffStatus(successful); return result; } catch (error) { if (isFailoverableError(error)) { - updateClientBackoffStatus(clientWrapper, successful); + clientWrapper.updateBackoffStatus(successful); this.#isFailoverRequest = true; continue; } @@ -230,7 +229,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { } this.#clientManager.refreshClients(); - throw new Error("All app configuration clients failed to get settings."); + throw new Error("Failed to get configuration settings from endpoint."); } async #loadSelectedKeyValues(): Promise { @@ -673,7 +672,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { } #createFeatureFlagReference(setting: ConfigurationSetting): string { - let featureFlagReference = `${this.#clientManager.endpoint}/kv/${setting.key}`; + let featureFlagReference = `${this.#clientManager.endpoint.origin}/kv/${setting.key}`; if (setting.label && setting.label.trim().length !== 0) { featureFlagReference += `?label=${setting.label}`; } diff --git a/src/ConfigurationClientManager.ts b/src/ConfigurationClientManager.ts index a19c2827..884f2e60 100644 --- a/src/ConfigurationClientManager.ts +++ b/src/ConfigurationClientManager.ts @@ -20,9 +20,9 @@ const MINIMAL_CLIENT_REFRESH_INTERVAL = 30 * 1000; // 30 seconds in milliseconds const SRV_QUERY_TIMEOUT = 30 * 1000; // 30 seconds in milliseconds export class ConfigurationClientManager { - isFailoverable: boolean; - dns: any; - endpoint: string; + #isFailoverable: boolean; + #dns: any; + endpoint: URL; #secret : string; #id : string; #credential: TokenCredential; @@ -40,81 +40,79 @@ export class ConfigurationClientManager { appConfigOptions?: AzureAppConfigurationOptions ) { let staticClient: AppConfigurationClient; + const credentialPassed = instanceOfTokenCredential(credentialOrOptions); - if (typeof connectionStringOrEndpoint === "string" && !instanceOfTokenCredential(credentialOrOptions)) { + if (typeof connectionStringOrEndpoint === "string" && !credentialPassed) { const connectionString = connectionStringOrEndpoint; this.#appConfigOptions = credentialOrOptions as AzureAppConfigurationOptions; this.#clientOptions = getClientOptions(this.#appConfigOptions); - staticClient = new AppConfigurationClient(connectionString, this.#clientOptions); const ConnectionStringRegex = /Endpoint=(.*);Id=(.*);Secret=(.*)/; const regexMatch = connectionString.match(ConnectionStringRegex); if (regexMatch) { - this.endpoint = regexMatch[1]; + const endpointFromConnectionStr = regexMatch[1]; + this.endpoint = getValidUrl(endpointFromConnectionStr); this.#id = regexMatch[2]; this.#secret = regexMatch[3]; } else { throw new Error(`Invalid connection string. Valid connection strings should match the regex '${ConnectionStringRegex.source}'.`); } - } else if ((connectionStringOrEndpoint instanceof URL || typeof connectionStringOrEndpoint === "string") && instanceOfTokenCredential(credentialOrOptions)) { + staticClient = new AppConfigurationClient(connectionString, this.#clientOptions); + } else if ((connectionStringOrEndpoint instanceof URL || typeof connectionStringOrEndpoint === "string") && credentialPassed) { let endpoint = connectionStringOrEndpoint; // ensure string is a valid URL. if (typeof endpoint === "string") { - try { - endpoint = new URL(endpoint); - } catch (error) { - if (error.code === "ERR_INVALID_URL") { - throw new Error("Invalid endpoint URL.", { cause: error }); - } else { - throw error; - } - } + endpoint = getValidUrl(endpoint); } const credential = credentialOrOptions as TokenCredential; this.#appConfigOptions = appConfigOptions as AzureAppConfigurationOptions; this.#clientOptions = getClientOptions(this.#appConfigOptions); - staticClient = new AppConfigurationClient(connectionStringOrEndpoint.toString(), credential, this.#clientOptions); - this.endpoint = endpoint.toString(); + this.endpoint = endpoint; this.#credential = credential; + staticClient = new AppConfigurationClient(this.endpoint.origin, this.#credential, this.#clientOptions); } else { throw new Error("A connection string or an endpoint with credential must be specified to create a client."); } - this.#staticClients = [new ConfigurationClientWrapper(this.endpoint, staticClient)]; - this.#validDomain = getValidDomain(this.endpoint); + this.#staticClients = [new ConfigurationClientWrapper(this.endpoint.origin, staticClient)]; + this.#validDomain = getValidDomain(this.endpoint.hostname.toLowerCase()); } async init() { if (this.#appConfigOptions?.replicaDiscoveryEnabled === false || isBrowser() || isWebWorker()) { - this.isFailoverable = false; + this.#isFailoverable = false; return; } try { - this.dns = await import("dns/promises"); + this.#dns = await import("dns/promises"); }catch (error) { - this.isFailoverable = false; + this.#isFailoverable = false; console.warn("Failed to load the dns module:", error.message); return; } - this.isFailoverable = true; + this.#isFailoverable = true; } async getClients() : Promise { - if (!this.isFailoverable) { + if (!this.#isFailoverable) { return this.#staticClients; } const currentTime = Date.now(); - if (this.#isFallbackClientDiscoveryDue(currentTime)) { + // Filter static clients whose backoff time has ended + let availableClients = this.#staticClients.filter(client => client.backoffEndTime <= currentTime); + if (currentTime >= this.#lastFallbackClientRefreshAttempt + MINIMAL_CLIENT_REFRESH_INTERVAL && + (!this.#dynamicClients || + // All dynamic clients are in backoff means no client is available + this.#dynamicClients.every(client => currentTime < client.backoffEndTime) || + currentTime >= this.#lastFallbackClientRefreshTime + FALLBACK_CLIENT_REFRESH_EXPIRE_INTERVAL)) { this.#lastFallbackClientRefreshAttempt = currentTime; - const host = new URL(this.endpoint).hostname; - await this.#discoverFallbackClients(host); + await this.#discoverFallbackClients(this.endpoint.hostname); + return availableClients.concat(this.#dynamicClients); } - // Filter static clients whose backoff time has ended - let availableClients = this.#staticClients.filter(client => client.backoffEndTime <= currentTime); // If there are dynamic clients, filter and concatenate them if (this.#dynamicClients && this.#dynamicClients.length > 0) { availableClients = availableClients.concat( @@ -127,15 +125,14 @@ export class ConfigurationClientManager { async refreshClients() { const currentTime = Date.now(); - if (this.isFailoverable && - currentTime > new Date(this.#lastFallbackClientRefreshAttempt + MINIMAL_CLIENT_REFRESH_INTERVAL).getTime()) { + if (this.#isFailoverable && + currentTime >= new Date(this.#lastFallbackClientRefreshAttempt + MINIMAL_CLIENT_REFRESH_INTERVAL).getTime()) { this.#lastFallbackClientRefreshAttempt = currentTime; - const host = new URL(this.endpoint).hostname; - await this.#discoverFallbackClients(host); + await this.#discoverFallbackClients(this.endpoint.hostname); } } - async #discoverFallbackClients(host) { + async #discoverFallbackClients(host: string) { let result; try { result = await Promise.race([ @@ -143,7 +140,7 @@ export class ConfigurationClientManager { this.#querySrvTargetHost(host) ]); } catch (error) { - throw new Error(`Fail to build fallback clients, ${error.message}`); + throw new Error(`Failed to build fallback clients, ${error.message}`); } const srvTargetHosts = result as string[]; @@ -157,10 +154,12 @@ export class ConfigurationClientManager { for (const host of srvTargetHosts) { if (isValidEndpoint(host, this.#validDomain)) { const targetEndpoint = `https://${host}`; - if (targetEndpoint.toLowerCase() === this.endpoint.toLowerCase()) { + if (host.toLowerCase() === this.endpoint.hostname.toLowerCase()) { continue; } - const client = this.#credential ? new AppConfigurationClient(targetEndpoint, this.#credential, this.#clientOptions) : new AppConfigurationClient(buildConnectionString(targetEndpoint, this.#secret, this.#id), this.#clientOptions); + const client = this.#credential ? + new AppConfigurationClient(targetEndpoint, this.#credential, this.#clientOptions) : + new AppConfigurationClient(buildConnectionString(targetEndpoint, this.#secret, this.#id), this.#clientOptions); newDynamicClients.push(new ConfigurationClientWrapper(targetEndpoint, client)); } } @@ -169,13 +168,6 @@ export class ConfigurationClientManager { this.#lastFallbackClientRefreshTime = Date.now(); } - #isFallbackClientDiscoveryDue(dateTime) { - return dateTime >= this.#lastFallbackClientRefreshAttempt + MINIMAL_CLIENT_REFRESH_INTERVAL - && (!this.#dynamicClients - || this.#dynamicClients.every(client => dateTime < client.backoffEndTime) - || dateTime >= this.#lastFallbackClientRefreshTime + FALLBACK_CLIENT_REFRESH_EXPIRE_INTERVAL); - } - /** * Query SRV records and return target hosts. */ @@ -184,7 +176,7 @@ export class ConfigurationClientManager { try { // Look up SRV records for the origin host - const originRecords = await this.dns.resolveSrv(`${TCP_ORIGIN_KEY_NAME}.${host}`); + const originRecords = await this.#dns.resolveSrv(`${TCP_ORIGIN_KEY_NAME}.${host}`); if (originRecords.length === 0) { return results; } @@ -198,7 +190,7 @@ export class ConfigurationClientManager { // eslint-disable-next-line no-constant-condition while (true) { const currentAlt = `${ALT_KEY_NAME}${index}`; - const altRecords = await this.dns.resolveSrv(`${currentAlt}.${TCP_KEY_NAME}.${originHost}`); + const altRecords = await this.#dns.resolveSrv(`${currentAlt}.${TCP_KEY_NAME}.${originHost}`); if (altRecords.length === 0) { break; // No more alternate records, exit loop } @@ -238,19 +230,12 @@ function buildConnectionString(endpoint, secret, id: string): string { /** * Extracts a valid domain from the given endpoint URL based on trusted domain labels. */ -export function getValidDomain(endpoint: string): string { - try { - const url = new URL(endpoint); - const host = url.hostname.toLowerCase(); - - for (const label of TRUSTED_DOMAIN_LABELS) { - const index = host.lastIndexOf(label); - if (index !== -1) { - return host.substring(index); - } +export function getValidDomain(host: string): string { + for (const label of TRUSTED_DOMAIN_LABELS) { + const index = host.lastIndexOf(label); + if (index !== -1) { + return host.substring(index); } - } catch (error) { - console.error("Error parsing URL:", error.message); } return ""; @@ -267,7 +252,7 @@ export function isValidEndpoint(host: string, validDomain: string): boolean { return host.toLowerCase().endsWith(validDomain.toLowerCase()); } -export function getClientOptions(options?: AzureAppConfigurationOptions): AppConfigurationClientOptions | undefined { +function getClientOptions(options?: AzureAppConfigurationOptions): AppConfigurationClientOptions | undefined { // user-agent let userAgentPrefix = RequestTracing.USER_AGENT_PREFIX; // Default UA for JavaScript Provider const userAgentOptions = options?.clientOptions?.userAgentOptions; @@ -290,6 +275,18 @@ export function getClientOptions(options?: AzureAppConfigurationOptions): AppCon }); } +function getValidUrl(endpoint: string): URL { + try { + return new URL(endpoint); + } catch (error) { + if (error.code === "ERR_INVALID_URL") { + throw new Error("Invalid endpoint URL.", { cause: error }); + } else { + throw error; + } + } +} + export function instanceOfTokenCredential(obj: unknown) { return obj && typeof obj === "object" && "getToken" in obj && typeof obj.getToken === "function"; } diff --git a/src/ConfigurationClientWrapper.ts b/src/ConfigurationClientWrapper.ts index d7ad7830..7dd6f418 100644 --- a/src/ConfigurationClientWrapper.ts +++ b/src/ConfigurationClientWrapper.ts @@ -12,21 +12,21 @@ export class ConfigurationClientWrapper { endpoint: string; client: AppConfigurationClient; backoffEndTime: number = 0; // Timestamp - failedAttempts: number = 0; + #failedAttempts: number = 0; constructor(endpoint: string, client: AppConfigurationClient) { this.endpoint = endpoint; this.client = client; } -} -export function updateClientBackoffStatus(clientWrapper: ConfigurationClientWrapper, successfull: boolean) { - if (successfull) { - clientWrapper.failedAttempts = 0; - clientWrapper.backoffEndTime = Date.now(); - } else { - clientWrapper.failedAttempts += 1; - clientWrapper.backoffEndTime = Date.now() + calculateBackoffDuration(clientWrapper.failedAttempts); + updateBackoffStatus(successfull: boolean) { + if (successfull) { + this.#failedAttempts = 0; + this.backoffEndTime = Date.now(); + } else { + this.#failedAttempts += 1; + this.backoffEndTime = Date.now() + calculateBackoffDuration(this.#failedAttempts); + } } } @@ -35,7 +35,7 @@ export function calculateBackoffDuration(failedAttempts: number) { return MinBackoffDuration; } - // exponential: minBackoff * 2^(failedAttempts-1) + // exponential: minBackoff * 2 ^ (failedAttempts - 1) const exponential = Math.min(failedAttempts - 1, MAX_SAFE_EXPONENTIAL); let calculatedBackoffDuration = MinBackoffDuration * (1 << exponential); if (calculatedBackoffDuration > MaxBackoffDuration) { diff --git a/test/clientOptions.test.ts b/test/clientOptions.test.ts index 4342334f..62e1b21c 100644 --- a/test/clientOptions.test.ts +++ b/test/clientOptions.test.ts @@ -30,7 +30,6 @@ describe("custom client options", function () { this.timeout(15000); const fakeEndpoint = "https://azure.azconfig.io"; - const replicaDiscoveryEnabled = false; beforeEach(() => { // Thus here mock it to reply 500, in which case the retry mechanism works. nock(fakeEndpoint).persist().get(() => true).reply(500); @@ -44,7 +43,6 @@ describe("custom client options", function () { const countPolicy = new HttpRequestCountPolicy(); const loadPromise = () => { return load(createMockedConnectionString(fakeEndpoint), { - replicaDiscoveryEnabled: replicaDiscoveryEnabled, clientOptions: { additionalPolicies: [{ policy: countPolicy, @@ -67,7 +65,6 @@ describe("custom client options", function () { const countPolicy = new HttpRequestCountPolicy(); const loadWithMaxRetries = (maxRetries: number) => { return load(createMockedConnectionString(fakeEndpoint), { - replicaDiscoveryEnabled: replicaDiscoveryEnabled, clientOptions: { additionalPolicies: [{ policy: countPolicy, @@ -106,7 +103,6 @@ describe("custom client options", function () { const countPolicy = new HttpRequestCountPolicy(); const loadPromise = () => { return load(createMockedConnectionString(fakeEndpoint), { - replicaDiscoveryEnabled: replicaDiscoveryEnabled, clientOptions: { additionalPolicies: [{ policy: countPolicy, diff --git a/test/failover.test.ts b/test/failover.test.ts index b5e97fcd..1515f9db 100644 --- a/test/failover.test.ts +++ b/test/failover.test.ts @@ -6,7 +6,7 @@ import * as chaiAsPromised from "chai-as-promised"; chai.use(chaiAsPromised); const expect = chai.expect; import { load } from "./exportedApi"; -import { createMockedConnectionString, createMockedEndpoint, createMockedFeatureFlag, createMockedKeyValue, mockAppConfigurationClientListConfigurationSettingsWithFailure, mockConfigurationManagerGetClients, restoreMocks } from "./utils/testHelper"; +import { createMockedConnectionString, createMockedFeatureFlag, createMockedKeyValue, mockAppConfigurationClientListConfigurationSettingsWithFailure, mockConfigurationManagerGetClients, restoreMocks } from "./utils/testHelper"; import { getValidDomain, isValidEndpoint } from "../src/ConfigurationClientManager"; const mockedKVs = [{ @@ -75,12 +75,12 @@ describe("failover", function () { mockAppConfigurationClientListConfigurationSettingsWithFailure(mockedKVs); const connectionString = createMockedConnectionString(); - return expect(load(connectionString)).eventually.rejectedWith("All app configuration clients failed to get settings."); + return expect(load(connectionString)).eventually.rejectedWith("Failed to get configuration settings from endpoint."); }); it("should validate endpoint", () => { - const fakeEndpoint = createMockedEndpoint("fake"); - const validDomain = getValidDomain(fakeEndpoint); + const fakeHost = "fake.azconfig.io"; + const validDomain = getValidDomain(fakeHost); expect(isValidEndpoint("azure.azconfig.io", validDomain)).to.be.true; expect(isValidEndpoint("azure.privatelink.azconfig.io", validDomain)).to.be.true; @@ -90,8 +90,8 @@ describe("failover", function () { expect(isValidEndpoint("azure.appconfig.azure.com", validDomain)).to.be.false; expect(isValidEndpoint("azure.azconfig.bad.io", validDomain)).to.be.false; - const fakeEndpoint2 = "https://foobar.appconfig.azure.com"; - const validDomain2 = getValidDomain(fakeEndpoint2); + const fakeHost2 = "foobar.appconfig.azure.com"; + const validDomain2 = getValidDomain(fakeHost2); expect(isValidEndpoint("azure.appconfig.azure.com", validDomain2)).to.be.true; expect(isValidEndpoint("azure.z1.appconfig.azure.com", validDomain2)).to.be.true; @@ -102,14 +102,14 @@ describe("failover", function () { expect(isValidEndpoint("azure.badappconfig.azure.com", validDomain2)).to.be.false; expect(isValidEndpoint("azure.appconfigbad.azure.com", validDomain2)).to.be.false; - const fakeEndpoint3 = "https://foobar.azconfig-test.io"; - const validDomain3 = getValidDomain(fakeEndpoint3); + const fakeHost3 = "foobar.azconfig-test.io"; + const validDomain3 = getValidDomain(fakeHost3); expect(isValidEndpoint("azure.azconfig-test.io", validDomain3)).to.be.false; expect(isValidEndpoint("azure.azconfig.io", validDomain3)).to.be.false; - const fakeEndpoint4 = "https://foobar.z1.appconfig-test.azure.com"; - const validDomain4 = getValidDomain(fakeEndpoint4); + const fakeHost4 = "foobar.z1.appconfig-test.azure.com"; + const validDomain4 = getValidDomain(fakeHost4); expect(isValidEndpoint("foobar.z2.appconfig-test.azure.com", validDomain4)).to.be.false; expect(isValidEndpoint("foobar.appconfig-test.azure.com", validDomain4)).to.be.false; diff --git a/test/featureFlag.test.ts b/test/featureFlag.test.ts index faa11f33..1bf92d7f 100644 --- a/test/featureFlag.test.ts +++ b/test/featureFlag.test.ts @@ -4,7 +4,7 @@ import * as chai from "chai"; import * as chaiAsPromised from "chai-as-promised"; import { load } from "./exportedApi.js"; -import { createMockedConnectionString, createMockedEndpoint, createMockedFeatureFlag, createMockedKeyValue, mockAppConfigurationClientListConfigurationSettings, mockConfigurationManagerGetClients, restoreMocks } from "./utils/testHelper.js"; +import { createMockedConnectionString, createMockedEndpoint, createMockedFeatureFlag, createMockedKeyValue, mockAppConfigurationClientListConfigurationSettings, restoreMocks } from "./utils/testHelper.js"; chai.use(chaiAsPromised); const expect = chai.expect; @@ -201,9 +201,7 @@ const mockedKVs = [{ describe("feature flags", function () { this.timeout(10000); - const isFailoverable = false; before(() => { - mockConfigurationManagerGetClients(isFailoverable); mockAppConfigurationClientListConfigurationSettings(mockedKVs); }); diff --git a/test/json.test.ts b/test/json.test.ts index 52b7c71f..c139d53b 100644 --- a/test/json.test.ts +++ b/test/json.test.ts @@ -6,15 +6,13 @@ import * as chaiAsPromised from "chai-as-promised"; chai.use(chaiAsPromised); const expect = chai.expect; import { load } from "./exportedApi.js"; -import { mockAppConfigurationClientListConfigurationSettings, restoreMocks, createMockedConnectionString, createMockedKeyVaultReference, createMockedJsonKeyValue, mockConfigurationManagerGetClients } from "./utils/testHelper.js"; +import { mockAppConfigurationClientListConfigurationSettings, restoreMocks, createMockedConnectionString, createMockedKeyVaultReference, createMockedJsonKeyValue } from "./utils/testHelper.js"; const jsonKeyValue = createMockedJsonKeyValue("json.settings.logging", '{"Test":{"Level":"Debug"},"Prod":{"Level":"Warning"}}'); const keyVaultKeyValue = createMockedKeyVaultReference("TestKey", "https://fake-vault-name.vault.azure.net/secrets/fakeSecretName"); -const isFailoverable = false; describe("json", function () { beforeEach(() => { - mockConfigurationManagerGetClients(isFailoverable); }); afterEach(() => { diff --git a/test/keyvault.test.ts b/test/keyvault.test.ts index 3e8b5ee6..f17b0a6d 100644 --- a/test/keyvault.test.ts +++ b/test/keyvault.test.ts @@ -9,7 +9,6 @@ import { load } from "./exportedApi.js"; import { sinon, createMockedConnectionString, createMockedTokenCredential, mockAppConfigurationClientListConfigurationSettings, mockSecretClientGetSecret, restoreMocks, createMockedKeyVaultReference } from "./utils/testHelper.js"; import { KeyVaultSecret, SecretClient } from "@azure/keyvault-secrets"; -const replicaDiscoveryEnabled = false; const mockedData = [ // key, secretUri, value ["TestKey", "https://fake-vault-name.vault.azure.net/secrets/fakeSecretName", "SecretValue"], @@ -41,13 +40,11 @@ describe("key vault reference", function () { it("require key vault options to resolve reference", async () => { return expect(load(createMockedConnectionString(), { - replicaDiscoveryEnabled: replicaDiscoveryEnabled })).eventually.rejectedWith("Configure keyVaultOptions to resolve Key Vault Reference(s)."); }); it("should resolve key vault reference with credential", async () => { const settings = await load(createMockedConnectionString(), { - replicaDiscoveryEnabled: replicaDiscoveryEnabled, keyVaultOptions: { credential: createMockedTokenCredential() } @@ -59,7 +56,6 @@ describe("key vault reference", function () { it("should resolve key vault reference with secret resolver", async () => { const settings = await load(createMockedConnectionString(), { - replicaDiscoveryEnabled: replicaDiscoveryEnabled, keyVaultOptions: { secretResolver: (kvrUrl) => { return "SecretResolver::" + kvrUrl.toString(); @@ -80,7 +76,6 @@ describe("key vault reference", function () { const client2 = new SecretClient("https://fake-vault-name2.vault.azure.net", createMockedTokenCredential()); sinon.stub(client2, "getSecret").returns(Promise.resolve({value: "SecretValueViaClient2" } as KeyVaultSecret)); const settings = await load(createMockedConnectionString(), { - replicaDiscoveryEnabled: replicaDiscoveryEnabled, keyVaultOptions: { secretClients: [ client1, @@ -95,7 +90,6 @@ describe("key vault reference", function () { it("should throw error when secret clients not provided for all key vault references", async () => { const loadKeyVaultPromise = load(createMockedConnectionString(), { - replicaDiscoveryEnabled: replicaDiscoveryEnabled, keyVaultOptions: { secretClients: [ new SecretClient("https://fake-vault-name.vault.azure.net", createMockedTokenCredential()), @@ -107,7 +101,6 @@ describe("key vault reference", function () { it("should fallback to use default credential when corresponding secret client not provided", async () => { const settings = await load(createMockedConnectionString(), { - replicaDiscoveryEnabled: replicaDiscoveryEnabled, keyVaultOptions: { secretClients: [ new SecretClient("https://fake-vault-name.vault.azure.net", createMockedTokenCredential()), diff --git a/test/load.test.ts b/test/load.test.ts index 0e3e7275..ce22b1a8 100644 --- a/test/load.test.ts +++ b/test/load.test.ts @@ -6,7 +6,7 @@ import * as chaiAsPromised from "chai-as-promised"; chai.use(chaiAsPromised); const expect = chai.expect; import { load } from "./exportedApi.js"; -import { mockAppConfigurationClientListConfigurationSettings, restoreMocks, createMockedConnectionString, createMockedEndpoint, createMockedTokenCredential, createMockedKeyValue, mockConfigurationManagerGetClients } from "./utils/testHelper.js"; +import { mockAppConfigurationClientListConfigurationSettings, restoreMocks, createMockedConnectionString, createMockedEndpoint, createMockedTokenCredential, createMockedKeyValue } from "./utils/testHelper.js"; const mockedKVs = [{ key: "app.settings.fontColor", @@ -79,9 +79,7 @@ const mockedKVs = [{ describe("load", function () { this.timeout(10000); - const isFailoverable = false; before(() => { - mockConfigurationManagerGetClients(isFailoverable); mockAppConfigurationClientListConfigurationSettings(mockedKVs); }); diff --git a/test/refresh.test.ts b/test/refresh.test.ts index ac28bbce..cf91add3 100644 --- a/test/refresh.test.ts +++ b/test/refresh.test.ts @@ -10,7 +10,6 @@ import { mockAppConfigurationClientListConfigurationSettings, mockAppConfigurati import * as uuid from "uuid"; let mockedKVs: any[] = []; -const replicaDiscoveryEnabled = false; function updateSetting(key: string, value: any) { const setting = mockedKVs.find(elem => elem.key === key); @@ -44,7 +43,6 @@ describe("dynamic refresh", function () { it("should throw error when refresh is not enabled but refresh is called", async () => { const connectionString = createMockedConnectionString(); const settings = await load(connectionString, { - replicaDiscoveryEnabled: replicaDiscoveryEnabled }); const refreshCall = settings.refresh(); return expect(refreshCall).eventually.rejectedWith("Refresh is not enabled for key-values or feature flags."); @@ -53,14 +51,12 @@ describe("dynamic refresh", function () { it("should only allow non-empty list of watched settings when refresh is enabled", async () => { const connectionString = createMockedConnectionString(); const loadWithEmptyWatchedSettings = load(connectionString, { - replicaDiscoveryEnabled: replicaDiscoveryEnabled, refreshOptions: { enabled: true, watchedSettings: [] } }); const loadWithUndefinedWatchedSettings = load(connectionString, { - replicaDiscoveryEnabled: replicaDiscoveryEnabled, refreshOptions: { enabled: true } @@ -74,7 +70,6 @@ describe("dynamic refresh", function () { it("should not allow refresh interval less than 1 second", async () => { const connectionString = createMockedConnectionString(); const loadWithInvalidRefreshInterval = load(connectionString, { - replicaDiscoveryEnabled: replicaDiscoveryEnabled, refreshOptions: { enabled: true, watchedSettings: [ @@ -89,7 +84,6 @@ describe("dynamic refresh", function () { it("should not allow '*' in key or label", async () => { const connectionString = createMockedConnectionString(); const loadWithInvalidKey = load(connectionString, { - replicaDiscoveryEnabled: replicaDiscoveryEnabled, refreshOptions: { enabled: true, watchedSettings: [ @@ -98,7 +92,6 @@ describe("dynamic refresh", function () { } }); const loadWithInvalidKey2 = load(connectionString, { - replicaDiscoveryEnabled: replicaDiscoveryEnabled, refreshOptions: { enabled: true, watchedSettings: [ @@ -133,7 +126,6 @@ describe("dynamic refresh", function () { it("should throw error when calling onRefresh when refresh is not enabled", async () => { const connectionString = createMockedConnectionString(); const settings = await load(connectionString, { - replicaDiscoveryEnabled: replicaDiscoveryEnabled, }); expect(() => settings.onRefresh(() => { })).throws("Refresh is not enabled for key-values or feature flags."); }); @@ -141,7 +133,6 @@ describe("dynamic refresh", function () { it("should only update values after refreshInterval", async () => { const connectionString = createMockedConnectionString(); const settings = await load(connectionString, { - replicaDiscoveryEnabled: replicaDiscoveryEnabled, refreshOptions: { enabled: true, refreshIntervalInMs: 2000, @@ -170,7 +161,6 @@ describe("dynamic refresh", function () { it("should update values when watched setting is deleted", async () => { const connectionString = createMockedConnectionString(); const settings = await load(connectionString, { - replicaDiscoveryEnabled: replicaDiscoveryEnabled, refreshOptions: { enabled: true, refreshIntervalInMs: 2000, @@ -197,7 +187,6 @@ describe("dynamic refresh", function () { it("should not update values when unwatched setting changes", async () => { const connectionString = createMockedConnectionString(); const settings = await load(connectionString, { - replicaDiscoveryEnabled: replicaDiscoveryEnabled, refreshOptions: { enabled: true, refreshIntervalInMs: 2000, @@ -219,7 +208,6 @@ describe("dynamic refresh", function () { it("should watch multiple settings if specified", async () => { const connectionString = createMockedConnectionString(); const settings = await load(connectionString, { - replicaDiscoveryEnabled: replicaDiscoveryEnabled, refreshOptions: { enabled: true, refreshIntervalInMs: 2000, @@ -245,7 +233,6 @@ describe("dynamic refresh", function () { it("should execute callbacks on successful refresh", async () => { const connectionString = createMockedConnectionString(); const settings = await load(connectionString, { - replicaDiscoveryEnabled: replicaDiscoveryEnabled, refreshOptions: { enabled: true, refreshIntervalInMs: 2000, @@ -275,7 +262,6 @@ describe("dynamic refresh", function () { it("should not include watched settings into configuration if not specified in selectors", async () => { const connectionString = createMockedConnectionString(); const settings = await load(connectionString, { - replicaDiscoveryEnabled: replicaDiscoveryEnabled, selectors: [ { keyFilter: "app.settings.fontColor" } ], @@ -295,7 +281,6 @@ describe("dynamic refresh", function () { it("should refresh when watched setting is added", async () => { const connectionString = createMockedConnectionString(); const settings = await load(connectionString, { - replicaDiscoveryEnabled: replicaDiscoveryEnabled, refreshOptions: { enabled: true, refreshIntervalInMs: 2000, @@ -318,7 +303,6 @@ describe("dynamic refresh", function () { it("should not refresh when watched setting keeps not existing", async () => { const connectionString = createMockedConnectionString(); const settings = await load(connectionString, { - replicaDiscoveryEnabled: replicaDiscoveryEnabled, refreshOptions: { enabled: true, refreshIntervalInMs: 2000, @@ -360,7 +344,6 @@ describe("dynamic refresh feature flags", function () { const connectionString = createMockedConnectionString(); const settings = await load(connectionString, { - replicaDiscoveryEnabled: replicaDiscoveryEnabled, featureFlagOptions: { enabled: true, selectors: [{ @@ -411,7 +394,6 @@ describe("dynamic refresh feature flags", function () { const connectionString = createMockedConnectionString(); const settings = await load(connectionString, { - replicaDiscoveryEnabled: replicaDiscoveryEnabled, featureFlagOptions: { enabled: true, selectors: [{ diff --git a/test/requestTracing.test.ts b/test/requestTracing.test.ts index 5da1ff80..62d0c5b5 100644 --- a/test/requestTracing.test.ts +++ b/test/requestTracing.test.ts @@ -25,7 +25,6 @@ class HttpRequestHeadersPolicy { describe("request tracing", function () { this.timeout(15000); - const replicaDiscoveryEnabled = false; const fakeEndpoint = "https://127.0.0.1"; // sufficient to test the request it sends out const headerPolicy = new HttpRequestHeadersPolicy(); const position: "perCall" | "perRetry" = "perCall"; @@ -47,9 +46,7 @@ describe("request tracing", function () { it("should have correct user agent prefix", async () => { try { - await load(createMockedConnectionString(fakeEndpoint), { - replicaDiscoveryEnabled: replicaDiscoveryEnabled, - clientOptions }); + await load(createMockedConnectionString(fakeEndpoint), { clientOptions }); } catch (e) { /* empty */ } expect(headerPolicy.headers).not.undefined; expect(headerPolicy.headers.get("User-Agent")).satisfy((ua: string) => ua.startsWith("javascript-appconfiguration-provider")); @@ -57,10 +54,7 @@ describe("request tracing", function () { it("should have request type in correlation-context header", async () => { try { - await load(createMockedConnectionString(fakeEndpoint), { - replicaDiscoveryEnabled: replicaDiscoveryEnabled, - clientOptions - }); + await load(createMockedConnectionString(fakeEndpoint), { clientOptions }); } catch (e) { /* empty */ } expect(headerPolicy.headers).not.undefined; expect(headerPolicy.headers.get("Correlation-Context")).eq("RequestType=Startup"); @@ -69,7 +63,6 @@ describe("request tracing", function () { it("should have key vault tag in correlation-context header", async () => { try { await load(createMockedConnectionString(fakeEndpoint), { - replicaDiscoveryEnabled: replicaDiscoveryEnabled, clientOptions, keyVaultOptions: { credential: createMockedTokenCredential() @@ -85,10 +78,7 @@ describe("request tracing", function () { it("should detect env in correlation-context header", async () => { process.env.NODE_ENV = "development"; try { - await load(createMockedConnectionString(fakeEndpoint), { - replicaDiscoveryEnabled: replicaDiscoveryEnabled, - clientOptions - }); + await load(createMockedConnectionString(fakeEndpoint), { clientOptions }); } catch (e) { /* empty */ } expect(headerPolicy.headers).not.undefined; const correlationContext = headerPolicy.headers.get("Correlation-Context"); @@ -100,10 +90,7 @@ describe("request tracing", function () { it("should detect host type in correlation-context header", async () => { process.env.WEBSITE_SITE_NAME = "website-name"; try { - await load(createMockedConnectionString(fakeEndpoint), { - replicaDiscoveryEnabled: replicaDiscoveryEnabled, - clientOptions - }); + await load(createMockedConnectionString(fakeEndpoint), { clientOptions }); } catch (e) { /* empty */ } expect(headerPolicy.headers).not.undefined; const correlationContext = headerPolicy.headers.get("Correlation-Context"); @@ -116,10 +103,7 @@ describe("request tracing", function () { for (const indicator of ["TRUE", "true"]) { process.env.AZURE_APP_CONFIGURATION_TRACING_DISABLED = indicator; try { - await load(createMockedConnectionString(fakeEndpoint), { - replicaDiscoveryEnabled: replicaDiscoveryEnabled, - clientOptions - }); + await load(createMockedConnectionString(fakeEndpoint), { clientOptions }); } catch (e) { /* empty */ } expect(headerPolicy.headers).not.undefined; const correlationContext = headerPolicy.headers.get("Correlation-Context"); @@ -137,7 +121,6 @@ describe("request tracing", function () { }].map(createMockedKeyValue)); const settings = await load(createMockedConnectionString(fakeEndpoint), { - replicaDiscoveryEnabled: replicaDiscoveryEnabled, clientOptions, refreshOptions: { enabled: true, @@ -196,9 +179,7 @@ describe("request tracing", function () { (global as any).importScripts = function importScripts() { }; try { - await load(createMockedConnectionString(fakeEndpoint), { - replicaDiscoveryEnabled: replicaDiscoveryEnabled, - clientOptions }); + await load(createMockedConnectionString(fakeEndpoint), { clientOptions }); } catch (e) { /* empty */ } expect(headerPolicy.headers).not.undefined; const correlationContext = headerPolicy.headers.get("Correlation-Context"); @@ -216,9 +197,7 @@ describe("request tracing", function () { (global as any).importScripts = function importScripts() { }; try { - await load(createMockedConnectionString(fakeEndpoint), { - replicaDiscoveryEnabled: replicaDiscoveryEnabled, - clientOptions }); + await load(createMockedConnectionString(fakeEndpoint), { clientOptions }); } catch (e) { /* empty */ } expect(headerPolicy.headers).not.undefined; const correlationContext = headerPolicy.headers.get("Correlation-Context"); @@ -236,9 +215,7 @@ describe("request tracing", function () { (global as any).importScripts = function importScripts() { }; try { - await load(createMockedConnectionString(fakeEndpoint), { - replicaDiscoveryEnabled: replicaDiscoveryEnabled, - clientOptions }); + await load(createMockedConnectionString(fakeEndpoint), { clientOptions }); } catch (e) { /* empty */ } expect(headerPolicy.headers).not.undefined; const correlationContext = headerPolicy.headers.get("Correlation-Context"); @@ -256,9 +233,7 @@ describe("request tracing", function () { (global as any).importScripts = function importScripts() { }; try { - await load(createMockedConnectionString(fakeEndpoint), { - replicaDiscoveryEnabled: replicaDiscoveryEnabled, - clientOptions }); + await load(createMockedConnectionString(fakeEndpoint), { clientOptions }); } catch (e) { /* empty */ } expect(headerPolicy.headers).not.undefined; const correlationContext = headerPolicy.headers.get("Correlation-Context"); @@ -276,9 +251,7 @@ describe("request tracing", function () { (global as any).importScripts = undefined; try { - await load(createMockedConnectionString(fakeEndpoint), { - replicaDiscoveryEnabled: replicaDiscoveryEnabled, - clientOptions }); + await load(createMockedConnectionString(fakeEndpoint), { clientOptions }); } catch (e) { /* empty */ } expect(headerPolicy.headers).not.undefined; const correlationContext = headerPolicy.headers.get("Correlation-Context"); @@ -316,9 +289,7 @@ describe("request tracing", function () { (global as any).document = new (global as any).Document(); try { - await load(createMockedConnectionString(fakeEndpoint), { - replicaDiscoveryEnabled: replicaDiscoveryEnabled, - clientOptions }); + await load(createMockedConnectionString(fakeEndpoint), { clientOptions }); } catch (e) { /* empty */ } expect(headerPolicy.headers).not.undefined; const correlationContext = headerPolicy.headers.get("Correlation-Context"); @@ -333,9 +304,7 @@ describe("request tracing", function () { (global as any).document = undefined; // not an instance of Document try { - await load(createMockedConnectionString(fakeEndpoint), { - replicaDiscoveryEnabled: replicaDiscoveryEnabled, - clientOptions }); + await load(createMockedConnectionString(fakeEndpoint), { clientOptions }); } catch (e) { /* empty */ } expect(headerPolicy.headers).not.undefined; const correlationContext = headerPolicy.headers.get("Correlation-Context"); @@ -350,9 +319,7 @@ describe("request tracing", function () { (global as any).document = {}; // Not an instance of Document try { - await load(createMockedConnectionString(fakeEndpoint), { - replicaDiscoveryEnabled: replicaDiscoveryEnabled, - clientOptions }); + await load(createMockedConnectionString(fakeEndpoint), { clientOptions }); } catch (e) { /* empty */ } expect(headerPolicy.headers).not.undefined; const correlationContext = headerPolicy.headers.get("Correlation-Context"); @@ -367,9 +334,7 @@ describe("request tracing", function () { (global as any).document = new (global as any).Document(); try { - await load(createMockedConnectionString(fakeEndpoint), { - replicaDiscoveryEnabled: replicaDiscoveryEnabled, - clientOptions }); + await load(createMockedConnectionString(fakeEndpoint), { clientOptions }); } catch (e) { /* empty */ } expect(headerPolicy.headers).not.undefined; const correlationContext = headerPolicy.headers.get("Correlation-Context"); @@ -384,9 +349,7 @@ describe("request tracing", function () { (global as any).document = new (global as any).Document(); try { - await load(createMockedConnectionString(fakeEndpoint), { - replicaDiscoveryEnabled: replicaDiscoveryEnabled, - clientOptions }); + await load(createMockedConnectionString(fakeEndpoint), { clientOptions }); } catch (e) { /* empty */ } expect(headerPolicy.headers).not.undefined; const correlationContext = headerPolicy.headers.get("Correlation-Context"); From c52599a860c282996f4e223b338974d3c230c6c8 Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Thu, 14 Nov 2024 14:29:22 +0800 Subject: [PATCH 18/24] added ENOENT error --- src/AzureAppConfigurationImpl.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index 95bae535..960dece5 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -834,6 +834,7 @@ function getValidFeatureFlagSelectors(selectors?: SettingSelector[]): SettingSel } function isFailoverableError(error: any): boolean { - return isRestError(error) && (error.code === "ENOTFOUND" || + // ENOTFOUND: DNS lookup failed, ENOENT: no such file or directory + return isRestError(error) && (error.code === "ENOTFOUND" || error.code === "ENOENT" || (error.statusCode !== undefined && (error.statusCode === 401 || error.statusCode === 403 || error.statusCode === 408 || error.statusCode === 429 || error.statusCode >= 500))); } From a7b7bfb53ecd85343ae1bc2a5472b0502ef0bd8a Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Fri, 15 Nov 2024 14:44:12 +0800 Subject: [PATCH 19/24] update --- src/AzureAppConfigurationImpl.ts | 8 ++---- test/failover.test.ts | 12 ++++++--- test/keyvault.test.ts | 3 +-- test/refresh.test.ts | 6 ++--- test/utils/testHelper.ts | 46 +------------------------------- 5 files changed, 14 insertions(+), 61 deletions(-) diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index 960dece5..08f3bb3e 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -203,10 +203,6 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { async #executeWithFailoverPolicy(funcToExecute: (client: AppConfigurationClient) => Promise): Promise { const clientWrappers = await this.#clientManager.getClients(); - if (clientWrappers.length === 0) { - this.#clientManager.refreshClients(); - console.warn("Refresh skipped because no endpoint is accessible."); - } let successful: boolean; for (const clientWrapper of clientWrappers) { @@ -229,7 +225,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { } this.#clientManager.refreshClients(); - throw new Error("Failed to get configuration settings from endpoint."); + throw new Error("All clients failed to get configuration settings."); } async #loadSelectedKeyValues(): Promise { @@ -436,7 +432,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { // check if any refresh task failed for (const result of results) { if (result.status === "rejected") { - throw result.reason; + console.warn("Refresh failed:", result.reason); } } diff --git a/test/failover.test.ts b/test/failover.test.ts index 1515f9db..14f29f79 100644 --- a/test/failover.test.ts +++ b/test/failover.test.ts @@ -6,7 +6,7 @@ import * as chaiAsPromised from "chai-as-promised"; chai.use(chaiAsPromised); const expect = chai.expect; import { load } from "./exportedApi"; -import { createMockedConnectionString, createMockedFeatureFlag, createMockedKeyValue, mockAppConfigurationClientListConfigurationSettingsWithFailure, mockConfigurationManagerGetClients, restoreMocks } from "./utils/testHelper"; +import { createMockedConnectionString, createMockedFeatureFlag, createMockedKeyValue, mockAppConfigurationClientListConfigurationSettings, mockAppConfigurationClientListConfigurationSettingsWithFailure, mockConfigurationManagerGetClients, restoreMocks } from "./utils/testHelper"; import { getValidDomain, isValidEndpoint } from "../src/ConfigurationClientManager"; const mockedKVs = [{ @@ -37,7 +37,9 @@ describe("failover", function () { const replicaDiscoveryEnabled = true; const isFailoverable = true; mockConfigurationManagerGetClients(isFailoverable); - mockAppConfigurationClientListConfigurationSettingsWithFailure(mockedKVs); + mockAppConfigurationClientListConfigurationSettingsWithFailure(); + restoreMocks(); + mockAppConfigurationClientListConfigurationSettings(mockedKVs); const connectionString = createMockedConnectionString(); const settings = await load(connectionString, { @@ -52,7 +54,9 @@ describe("failover", function () { const replicaDiscoveryEnabled = true; const isFailoverable = true; mockConfigurationManagerGetClients(isFailoverable); - mockAppConfigurationClientListConfigurationSettingsWithFailure(mockedFeatureFlags); + mockAppConfigurationClientListConfigurationSettingsWithFailure(); + restoreMocks(); + mockAppConfigurationClientListConfigurationSettings(mockedFeatureFlags); const connectionString = createMockedConnectionString(); const settings = await load(connectionString, { @@ -72,7 +76,7 @@ describe("failover", function () { it("should throw error when all clients failed", async () => { const isFailoverable = false; mockConfigurationManagerGetClients(isFailoverable); - mockAppConfigurationClientListConfigurationSettingsWithFailure(mockedKVs); + mockAppConfigurationClientListConfigurationSettingsWithFailure(); const connectionString = createMockedConnectionString(); return expect(load(connectionString)).eventually.rejectedWith("Failed to get configuration settings from endpoint."); diff --git a/test/keyvault.test.ts b/test/keyvault.test.ts index f17b0a6d..cf01235c 100644 --- a/test/keyvault.test.ts +++ b/test/keyvault.test.ts @@ -39,8 +39,7 @@ describe("key vault reference", function () { }); it("require key vault options to resolve reference", async () => { - return expect(load(createMockedConnectionString(), { - })).eventually.rejectedWith("Configure keyVaultOptions to resolve Key Vault Reference(s)."); + return expect(load(createMockedConnectionString())).eventually.rejectedWith("Configure keyVaultOptions to resolve Key Vault Reference(s)."); }); it("should resolve key vault reference with credential", async () => { diff --git a/test/refresh.test.ts b/test/refresh.test.ts index cf91add3..fcffaee5 100644 --- a/test/refresh.test.ts +++ b/test/refresh.test.ts @@ -42,8 +42,7 @@ describe("dynamic refresh", function () { it("should throw error when refresh is not enabled but refresh is called", async () => { const connectionString = createMockedConnectionString(); - const settings = await load(connectionString, { - }); + const settings = await load(connectionString); const refreshCall = settings.refresh(); return expect(refreshCall).eventually.rejectedWith("Refresh is not enabled for key-values or feature flags."); }); @@ -125,8 +124,7 @@ describe("dynamic refresh", function () { it("should throw error when calling onRefresh when refresh is not enabled", async () => { const connectionString = createMockedConnectionString(); - const settings = await load(connectionString, { - }); + const settings = await load(connectionString); expect(() => settings.onRefresh(() => { })).throws("Refresh is not enabled for key-values or feature flags."); }); diff --git a/test/utils/testHelper.ts b/test/utils/testHelper.ts index 1695b868..30906e06 100644 --- a/test/utils/testHelper.ts +++ b/test/utils/testHelper.ts @@ -131,55 +131,11 @@ function mockAppConfigurationClientGetConfigurationSetting(kvList) { }); } -function mockAppConfigurationClientListConfigurationSettingsWithFailure(...pages: ConfigurationSetting[][]) { +function mockAppConfigurationClientListConfigurationSettingsWithFailure() { const stub = sinon.stub(AppConfigurationClient.prototype, "listConfigurationSettings"); // Configure the stub to throw an error on the first call and return mockedKVs on the second call stub.onFirstCall().throws(new RestError("Internal Server Error", { statusCode: 500 })); - stub.callsFake((listOptions) => { - let kvs = _filterKVs(pages.flat(), listOptions); - const mockIterator: AsyncIterableIterator & { byPage(): AsyncIterableIterator } = { - [Symbol.asyncIterator](): AsyncIterableIterator { - kvs = _filterKVs(pages.flat(), listOptions); - return this; - }, - next() { - const value = kvs.shift(); - return Promise.resolve({ done: !value, value }); - }, - byPage(): AsyncIterableIterator { - let remainingPages; - const pageEtags = listOptions?.pageEtags ? [...listOptions.pageEtags] : undefined; // a copy of the original list - return { - [Symbol.asyncIterator](): AsyncIterableIterator { - remainingPages = [...pages]; - return this; - }, - next() { - const pageItems = remainingPages.shift(); - const pageEtag = pageEtags?.shift(); - if (pageItems === undefined) { - return Promise.resolve({ done: true, value: undefined }); - } else { - const items = _filterKVs(pageItems ?? [], listOptions); - const etag = _sha256(JSON.stringify(items)); - const statusCode = pageEtag === etag ? 304 : 200; - return Promise.resolve({ - done: false, - value: { - items, - etag, - _response: { status: statusCode } - } - }); - } - } - }; - } - }; - - return mockIterator as any; - }); } // uriValueList: [["", "value"], ...] From 5adb0893391d616163fcc4a16b390c519d6a88ac Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Fri, 15 Nov 2024 16:04:14 +0800 Subject: [PATCH 20/24] update error message in test --- test/failover.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/failover.test.ts b/test/failover.test.ts index 14f29f79..9abbfc82 100644 --- a/test/failover.test.ts +++ b/test/failover.test.ts @@ -79,7 +79,7 @@ describe("failover", function () { mockAppConfigurationClientListConfigurationSettingsWithFailure(); const connectionString = createMockedConnectionString(); - return expect(load(connectionString)).eventually.rejectedWith("Failed to get configuration settings from endpoint."); + return expect(load(connectionString)).eventually.rejectedWith("All clients failed to get configuration settings."); }); it("should validate endpoint", () => { From 0905cd9c3f2c818f3802a84e326a6505f0feb99f Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Fri, 15 Nov 2024 16:51:23 +0800 Subject: [PATCH 21/24] update test --- test/failover.test.ts | 10 ++--- test/utils/testHelper.ts | 94 ++++++++++++++++++++++------------------ 2 files changed, 54 insertions(+), 50 deletions(-) diff --git a/test/failover.test.ts b/test/failover.test.ts index 9abbfc82..3362ca86 100644 --- a/test/failover.test.ts +++ b/test/failover.test.ts @@ -6,7 +6,7 @@ import * as chaiAsPromised from "chai-as-promised"; chai.use(chaiAsPromised); const expect = chai.expect; import { load } from "./exportedApi"; -import { createMockedConnectionString, createMockedFeatureFlag, createMockedKeyValue, mockAppConfigurationClientListConfigurationSettings, mockAppConfigurationClientListConfigurationSettingsWithFailure, mockConfigurationManagerGetClients, restoreMocks } from "./utils/testHelper"; +import { createMockedConnectionString, createMockedFeatureFlag, createMockedKeyValue, mockAppConfigurationClientListConfigurationSettingsWithFailure, mockConfigurationManagerGetClients, restoreMocks } from "./utils/testHelper"; import { getValidDomain, isValidEndpoint } from "../src/ConfigurationClientManager"; const mockedKVs = [{ @@ -37,9 +37,7 @@ describe("failover", function () { const replicaDiscoveryEnabled = true; const isFailoverable = true; mockConfigurationManagerGetClients(isFailoverable); - mockAppConfigurationClientListConfigurationSettingsWithFailure(); - restoreMocks(); - mockAppConfigurationClientListConfigurationSettings(mockedKVs); + mockAppConfigurationClientListConfigurationSettingsWithFailure(mockedKVs); const connectionString = createMockedConnectionString(); const settings = await load(connectionString, { @@ -54,9 +52,7 @@ describe("failover", function () { const replicaDiscoveryEnabled = true; const isFailoverable = true; mockConfigurationManagerGetClients(isFailoverable); - mockAppConfigurationClientListConfigurationSettingsWithFailure(); - restoreMocks(); - mockAppConfigurationClientListConfigurationSettings(mockedFeatureFlags); + mockAppConfigurationClientListConfigurationSettingsWithFailure(mockedFeatureFlags); const connectionString = createMockedConnectionString(); const settings = await load(connectionString, { diff --git a/test/utils/testHelper.ts b/test/utils/testHelper.ts index 30906e06..5c0d070a 100644 --- a/test/utils/testHelper.ts +++ b/test/utils/testHelper.ts @@ -40,6 +40,50 @@ function _filterKVs(unfilteredKvs: ConfigurationSetting[], listOptions: any) { }); } +function getMockedIterator(pages: ConfigurationSetting[][], kvs: ConfigurationSetting[], listOptions: any) { + const mockIterator: AsyncIterableIterator & { byPage(): AsyncIterableIterator } = { + [Symbol.asyncIterator](): AsyncIterableIterator { + kvs = _filterKVs(pages.flat(), listOptions); + return this; + }, + next() { + const value = kvs.shift(); + return Promise.resolve({ done: !value, value }); + }, + byPage(): AsyncIterableIterator { + let remainingPages; + const pageEtags = listOptions?.pageEtags ? [...listOptions.pageEtags] : undefined; // a copy of the original list + return { + [Symbol.asyncIterator](): AsyncIterableIterator { + remainingPages = [...pages]; + return this; + }, + next() { + const pageItems = remainingPages.shift(); + const pageEtag = pageEtags?.shift(); + if (pageItems === undefined) { + return Promise.resolve({ done: true, value: undefined }); + } else { + const items = _filterKVs(pageItems ?? [], listOptions); + const etag = _sha256(JSON.stringify(items)); + const statusCode = pageEtag === etag ? 304 : 200; + return Promise.resolve({ + done: false, + value: { + items, + etag, + _response: { status: statusCode } + } + }); + } + } + }; + } + }; + + return mockIterator as any; +} + /** * Mocks the listConfigurationSettings method of AppConfigurationClient to return the provided pages of ConfigurationSetting. * E.g. @@ -51,48 +95,8 @@ function _filterKVs(unfilteredKvs: ConfigurationSetting[], listOptions: any) { function mockAppConfigurationClientListConfigurationSettings(...pages: ConfigurationSetting[][]) { sinon.stub(AppConfigurationClient.prototype, "listConfigurationSettings").callsFake((listOptions) => { - let kvs = _filterKVs(pages.flat(), listOptions); - const mockIterator: AsyncIterableIterator & { byPage(): AsyncIterableIterator } = { - [Symbol.asyncIterator](): AsyncIterableIterator { - kvs = _filterKVs(pages.flat(), listOptions); - return this; - }, - next() { - const value = kvs.shift(); - return Promise.resolve({ done: !value, value }); - }, - byPage(): AsyncIterableIterator { - let remainingPages; - const pageEtags = listOptions?.pageEtags ? [...listOptions.pageEtags] : undefined; // a copy of the original list - return { - [Symbol.asyncIterator](): AsyncIterableIterator { - remainingPages = [...pages]; - return this; - }, - next() { - const pageItems = remainingPages.shift(); - const pageEtag = pageEtags?.shift(); - if (pageItems === undefined) { - return Promise.resolve({ done: true, value: undefined }); - } else { - const items = _filterKVs(pageItems ?? [], listOptions); - const etag = _sha256(JSON.stringify(items)); - const statusCode = pageEtag === etag ? 304 : 200; - return Promise.resolve({ - done: false, - value: { - items, - etag, - _response: { status: statusCode } - } - }); - } - } - }; - } - }; - - return mockIterator as any; + const kvs = _filterKVs(pages.flat(), listOptions); + return getMockedIterator(pages, kvs, listOptions); }); } @@ -131,11 +135,15 @@ function mockAppConfigurationClientGetConfigurationSetting(kvList) { }); } -function mockAppConfigurationClientListConfigurationSettingsWithFailure() { +function mockAppConfigurationClientListConfigurationSettingsWithFailure(...pages: ConfigurationSetting[][]) { const stub = sinon.stub(AppConfigurationClient.prototype, "listConfigurationSettings"); // Configure the stub to throw an error on the first call and return mockedKVs on the second call stub.onFirstCall().throws(new RestError("Internal Server Error", { statusCode: 500 })); + stub.callsFake((listOptions) => { + const kvs = _filterKVs(pages.flat(), listOptions); + return getMockedIterator(pages, kvs, listOptions); + }); } // uriValueList: [["", "value"], ...] From f42d3b12fe4c09cfdf7d01f0975bb7ae097b59cd Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Fri, 15 Nov 2024 19:39:30 +0800 Subject: [PATCH 22/24] update test --- test/failover.test.ts | 9 +++------ test/utils/testHelper.ts | 28 +++++++++++----------------- 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/test/failover.test.ts b/test/failover.test.ts index 3362ca86..66e41e54 100644 --- a/test/failover.test.ts +++ b/test/failover.test.ts @@ -6,7 +6,7 @@ import * as chaiAsPromised from "chai-as-promised"; chai.use(chaiAsPromised); const expect = chai.expect; import { load } from "./exportedApi"; -import { createMockedConnectionString, createMockedFeatureFlag, createMockedKeyValue, mockAppConfigurationClientListConfigurationSettingsWithFailure, mockConfigurationManagerGetClients, restoreMocks } from "./utils/testHelper"; +import { createMockedConnectionString, createMockedFeatureFlag, createMockedKeyValue, mockConfigurationManagerGetClients, restoreMocks } from "./utils/testHelper"; import { getValidDomain, isValidEndpoint } from "../src/ConfigurationClientManager"; const mockedKVs = [{ @@ -36,8 +36,7 @@ describe("failover", function () { it("should failover to replica and load key values from config store", async () => { const replicaDiscoveryEnabled = true; const isFailoverable = true; - mockConfigurationManagerGetClients(isFailoverable); - mockAppConfigurationClientListConfigurationSettingsWithFailure(mockedKVs); + mockConfigurationManagerGetClients(isFailoverable, mockedKVs); const connectionString = createMockedConnectionString(); const settings = await load(connectionString, { @@ -51,8 +50,7 @@ describe("failover", function () { it("should failover to replica and load feature flags from config store", async () => { const replicaDiscoveryEnabled = true; const isFailoverable = true; - mockConfigurationManagerGetClients(isFailoverable); - mockAppConfigurationClientListConfigurationSettingsWithFailure(mockedFeatureFlags); + mockConfigurationManagerGetClients(isFailoverable, mockedFeatureFlags); const connectionString = createMockedConnectionString(); const settings = await load(connectionString, { @@ -72,7 +70,6 @@ describe("failover", function () { it("should throw error when all clients failed", async () => { const isFailoverable = false; mockConfigurationManagerGetClients(isFailoverable); - mockAppConfigurationClientListConfigurationSettingsWithFailure(); const connectionString = createMockedConnectionString(); return expect(load(connectionString)).eventually.rejectedWith("All clients failed to get configuration settings."); diff --git a/test/utils/testHelper.ts b/test/utils/testHelper.ts index 5c0d070a..261b9b57 100644 --- a/test/utils/testHelper.ts +++ b/test/utils/testHelper.ts @@ -2,7 +2,7 @@ // Licensed under the MIT license. import * as sinon from "sinon"; -import { AppConfigurationClient, AppConfigurationClientOptions, ConfigurationSetting } from "@azure/app-configuration"; +import { AppConfigurationClient, ConfigurationSetting } from "@azure/app-configuration"; import { ClientSecretCredential } from "@azure/identity"; import { KeyVaultSecret, SecretClient } from "@azure/keyvault-secrets"; import * as uuid from "uuid"; @@ -100,12 +100,15 @@ function mockAppConfigurationClientListConfigurationSettings(...pages: Configura }); } -function mockConfigurationManagerGetClients(isFailoverable: boolean, clientOptions?: AppConfigurationClientOptions) { +function mockConfigurationManagerGetClients(isFailoverable: boolean, ...pages: ConfigurationSetting[][]) { // Stub the getClients method on the class prototype sinon.stub(ConfigurationClientManager.prototype, "getClients").callsFake(async () => { const clients: ConfigurationClientWrapper[] = []; const fakeEndpoint = createMockedEndpoint("fake"); - const fakeStaticClientWrapper = new ConfigurationClientWrapper(fakeEndpoint, new AppConfigurationClient(createMockedConnectionString(fakeEndpoint), clientOptions)); + const fakeStaticClientWrapper = new ConfigurationClientWrapper(fakeEndpoint, new AppConfigurationClient(createMockedConnectionString(fakeEndpoint))); + sinon.stub(fakeStaticClientWrapper.client, "listConfigurationSettings").callsFake(() => { + throw new RestError("Internal Server Error", { statusCode: 500 }); + }); clients.push(fakeStaticClientWrapper); if (!isFailoverable) { @@ -113,9 +116,12 @@ function mockConfigurationManagerGetClients(isFailoverable: boolean, clientOptio } const fakeReplicaEndpoint = createMockedEndpoint("fake-replica"); - const fakeDynamicClientWrapper = new ConfigurationClientWrapper(fakeReplicaEndpoint, new AppConfigurationClient(createMockedConnectionString(fakeReplicaEndpoint), clientOptions)); + const fakeDynamicClientWrapper = new ConfigurationClientWrapper(fakeReplicaEndpoint, new AppConfigurationClient(createMockedConnectionString(fakeReplicaEndpoint))); clients.push(fakeDynamicClientWrapper); - + sinon.stub(fakeDynamicClientWrapper.client, "listConfigurationSettings").callsFake((listOptions) => { + const kvs = _filterKVs(pages.flat(), listOptions); + return getMockedIterator(pages, kvs, listOptions); + }); return clients; }); } @@ -135,17 +141,6 @@ function mockAppConfigurationClientGetConfigurationSetting(kvList) { }); } -function mockAppConfigurationClientListConfigurationSettingsWithFailure(...pages: ConfigurationSetting[][]) { - const stub = sinon.stub(AppConfigurationClient.prototype, "listConfigurationSettings"); - - // Configure the stub to throw an error on the first call and return mockedKVs on the second call - stub.onFirstCall().throws(new RestError("Internal Server Error", { statusCode: 500 })); - stub.callsFake((listOptions) => { - const kvs = _filterKVs(pages.flat(), listOptions); - return getMockedIterator(pages, kvs, listOptions); - }); -} - // uriValueList: [["", "value"], ...] function mockSecretClientGetSecret(uriValueList: [string, string][]) { const dict = new Map(); @@ -235,7 +230,6 @@ export { sinon, mockAppConfigurationClientListConfigurationSettings, mockAppConfigurationClientGetConfigurationSetting, - mockAppConfigurationClientListConfigurationSettingsWithFailure, mockConfigurationManagerGetClients, mockSecretClientGetSecret, restoreMocks, From 1c8f00d4752c0f8a31fcb1c24265e76717899f0f Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Mon, 18 Nov 2024 09:28:26 +0800 Subject: [PATCH 23/24] update --- test/failover.test.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/test/failover.test.ts b/test/failover.test.ts index 66e41e54..c97a127a 100644 --- a/test/failover.test.ts +++ b/test/failover.test.ts @@ -34,27 +34,24 @@ describe("failover", function () { }); it("should failover to replica and load key values from config store", async () => { - const replicaDiscoveryEnabled = true; const isFailoverable = true; mockConfigurationManagerGetClients(isFailoverable, mockedKVs); const connectionString = createMockedConnectionString(); - const settings = await load(connectionString, { - replicaDiscoveryEnabled: replicaDiscoveryEnabled - }); + // replicaDiscoveryEnabled is default to true + const settings = await load(connectionString); expect(settings).not.undefined; expect(settings.get("app.settings.fontColor")).eq("red"); expect(settings.get("app.settings.fontSize")).eq("40"); }); it("should failover to replica and load feature flags from config store", async () => { - const replicaDiscoveryEnabled = true; const isFailoverable = true; mockConfigurationManagerGetClients(isFailoverable, mockedFeatureFlags); const connectionString = createMockedConnectionString(); + // replicaDiscoveryEnabled is default to true const settings = await load(connectionString, { - replicaDiscoveryEnabled: replicaDiscoveryEnabled, featureFlagOptions: { enabled: true, selectors: [{ From 1ea1d5141a80076fada5fbb68e975d8a5accf738 Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Mon, 18 Nov 2024 15:44:56 +0800 Subject: [PATCH 24/24] resolve conflicts --- src/ConfigurationClientManager.ts | 9 ++------- src/common/utils.ts | 8 ++++++++ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/ConfigurationClientManager.ts b/src/ConfigurationClientManager.ts index 884f2e60..59e03aa5 100644 --- a/src/ConfigurationClientManager.ts +++ b/src/ConfigurationClientManager.ts @@ -7,6 +7,7 @@ import { TokenCredential } from "@azure/identity"; import { AzureAppConfigurationOptions, MaxRetries, MaxRetryDelayInMs } from "./AzureAppConfigurationOptions.js"; import { isBrowser, isWebWorker } from "./requestTracing/utils.js"; import * as RequestTracing from "./requestTracing/constants.js"; +import { shuffleList } from "./common/utils.js"; const TCP_ORIGIN_KEY_NAME = "_origin._tcp"; const ALT_KEY_NAME = "_alt"; @@ -143,13 +144,7 @@ export class ConfigurationClientManager { throw new Error(`Failed to build fallback clients, ${error.message}`); } - const srvTargetHosts = result as string[]; - // Shuffle the list of SRV target hosts - for (let i = srvTargetHosts.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [srvTargetHosts[i], srvTargetHosts[j]] = [srvTargetHosts[j], srvTargetHosts[i]]; - } - + const srvTargetHosts = shuffleList(result) as string[]; const newDynamicClients: ConfigurationClientWrapper[] = []; for (const host of srvTargetHosts) { if (isValidEndpoint(host, this.#validDomain)) { diff --git a/src/common/utils.ts b/src/common/utils.ts index ad827bbb..8682484b 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -22,3 +22,11 @@ export function jsonSorter(key, value) { } return value; } + +export function shuffleList(array: T[]): T[] { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + return array; +}