From e29c5e0b95a797740c35ffb987ef6afcdbab591a Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Wed, 9 Oct 2024 17:19:01 +0800 Subject: [PATCH 01/35] update ci --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c4b9e1af..997a794a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: AppConfiguration-JavaScriptProvider CI on: push: - branches: [ "main" ] + branches: [ "main", "preview" ] pull_request: - branches: [ "main" ] + branches: [ "main", "preview" ] jobs: build: From af0ccb3177a1f792733f815c5b6fc89f1b6dacb8 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Tue, 15 Oct 2024 12:35:04 +0800 Subject: [PATCH 02/35] Feature Flag Telemetry Support (#101) * WIP * populate feature flag id * fix lint * populate only when telemetry is enabled * add testcase * fix lint * update * use window.btoa * rename method * revert add window. --- rollup.config.mjs | 2 +- src/AzureAppConfigurationImpl.ts | 91 ++++++++++++++++++++++++++++-- src/featureManagement/constants.ts | 8 ++- src/load.ts | 29 ++++++++-- test/featureFlag.test.ts | 44 ++++++++++++++- 5 files changed, 161 insertions(+), 13 deletions(-) diff --git a/rollup.config.mjs b/rollup.config.mjs index b2e87c64..1cd15dfc 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -4,7 +4,7 @@ import dts from "rollup-plugin-dts"; export default [ { - external: ["@azure/app-configuration", "@azure/keyvault-secrets", "@azure/core-rest-pipeline"], + external: ["@azure/app-configuration", "@azure/keyvault-secrets", "@azure/core-rest-pipeline", "crypto"], input: "src/index.ts", output: [ { diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index 30eba149..7f5600fc 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -9,7 +9,7 @@ import { IKeyValueAdapter } from "./IKeyValueAdapter"; import { JsonKeyValueAdapter } from "./JsonKeyValueAdapter"; import { DEFAULT_REFRESH_INTERVAL_IN_MS, MIN_REFRESH_INTERVAL_IN_MS } from "./RefreshOptions"; import { Disposable } from "./common/disposable"; -import { FEATURE_FLAGS_KEY_NAME, FEATURE_MANAGEMENT_KEY_NAME } from "./featureManagement/constants"; +import { FEATURE_FLAGS_KEY_NAME, FEATURE_MANAGEMENT_KEY_NAME, TELEMETRY_KEY_NAME, ENABLED_KEY_NAME, METADATA_KEY_NAME, ETAG_KEY_NAME, FEATURE_FLAG_ID_KEY_NAME, FEATURE_FLAG_REFERENCE_KEY_NAME } from "./featureManagement/constants"; import { AzureKeyVaultKeyValueAdapter } from "./keyvault/AzureKeyVaultKeyValueAdapter"; import { RefreshTimer } from "./refresh/RefreshTimer"; import { getConfigurationSettingWithTrace, listConfigurationSettingsWithTrace, requestTracingEnabled } from "./requestTracing/utils"; @@ -36,6 +36,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { #sortedTrimKeyPrefixes: string[] | undefined; readonly #requestTracingEnabled: boolean; #client: AppConfigurationClient; + #clientEndpoint: string | undefined; #options: AzureAppConfigurationOptions | undefined; #isInitialLoadCompleted: boolean = false; @@ -57,9 +58,11 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { constructor( client: AppConfigurationClient, + clientEndpoint: string | undefined, options: AzureAppConfigurationOptions | undefined ) { this.#client = client; + this.#clientEndpoint = clientEndpoint; this.#options = options; // Enable request tracing if not opt-out @@ -255,8 +258,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { } async #loadFeatureFlags() { - // Temporary map to store feature flags, key is the key of the setting, value is the raw value of the setting - const featureFlagsMap = new Map(); + const featureFlagSettings: ConfigurationSetting[] = []; for (const selector of this.#featureFlagSelectors) { const listOptions: ListConfigurationSettingsOptions = { keyFilter: `${featureFlagPrefix}${selector.keyFilter}`, @@ -273,7 +275,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { pageEtags.push(page.etag ?? ""); for (const setting of page.items) { if (isFeatureFlag(setting)) { - featureFlagsMap.set(setting.key, setting.value); + featureFlagSettings.push(setting); } } } @@ -281,7 +283,9 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { } // parse feature flags - const featureFlags = Array.from(featureFlagsMap.values()).map(rawFlag => JSON.parse(rawFlag)); + const featureFlags = await Promise.all( + featureFlagSettings.map(setting => this.#parseFeatureFlag(setting)) + ); // feature_management is a reserved key, and feature_flags is an array of feature flags this.#configMap.set(FEATURE_MANAGEMENT_KEY_NAME, { [FEATURE_FLAGS_KEY_NAME]: featureFlags }); @@ -532,6 +536,83 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { } return response; } + + async #parseFeatureFlag(setting: ConfigurationSetting): Promise { + const rawFlag = setting.value; + if (rawFlag === undefined) { + throw new Error("The value of configuration setting cannot be undefined."); + } + const featureFlag = JSON.parse(rawFlag); + + if (featureFlag[TELEMETRY_KEY_NAME] && featureFlag[TELEMETRY_KEY_NAME][ENABLED_KEY_NAME] === true) { + const metadata = featureFlag[TELEMETRY_KEY_NAME][METADATA_KEY_NAME]; + featureFlag[TELEMETRY_KEY_NAME][METADATA_KEY_NAME] = { + [ETAG_KEY_NAME]: setting.etag, + [FEATURE_FLAG_ID_KEY_NAME]: await this.#calculateFeatureFlagId(setting), + [FEATURE_FLAG_REFERENCE_KEY_NAME]: this.#createFeatureFlagReference(setting), + ...(metadata || {}) + }; + } + + return featureFlag; + } + + async #calculateFeatureFlagId(setting: ConfigurationSetting): Promise { + let crypto; + + // Check for browser environment + if (typeof window !== "undefined" && window.crypto && window.crypto.subtle) { + crypto = window.crypto; + } + // Check for Node.js environment + else if (typeof global !== "undefined" && global.crypto) { + crypto = global.crypto; + } + // Fallback to native Node.js crypto module + else { + try { + if (typeof module !== "undefined" && module.exports) { + crypto = require("crypto"); + } + else { + crypto = await import("crypto"); + } + } catch (error) { + console.error("Failed to load the crypto module:", error.message); + throw error; + } + } + + let baseString = `${setting.key}\n`; + if (setting.label && setting.label.trim().length !== 0) { + baseString += `${setting.label}`; + } + + // Convert to UTF-8 encoded bytes + const data = new TextEncoder().encode(baseString); + + // In the browser, use crypto.subtle.digest + if (crypto.subtle) { + const hashBuffer = await crypto.subtle.digest("SHA-256", data); + const hashArray = new Uint8Array(hashBuffer); + const base64String = btoa(String.fromCharCode(...hashArray)); + const base64urlString = base64String.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); + return base64urlString; + } + // In Node.js, use the crypto module's hash function + else { + const hash = crypto.createHash("sha256").update(data).digest(); + return hash.toString("base64url"); + } + } + + #createFeatureFlagReference(setting: ConfigurationSetting): string { + let featureFlagReference = `${this.#clientEndpoint}kv/${setting.key}`; + if (setting.label && setting.label.trim().length !== 0) { + featureFlagReference += `?label=${setting.label}`; + } + return featureFlagReference; + } } function getValidSelectors(selectors: SettingSelector[]): SettingSelector[] { diff --git a/src/featureManagement/constants.ts b/src/featureManagement/constants.ts index f0082f48..fe4d8cdf 100644 --- a/src/featureManagement/constants.ts +++ b/src/featureManagement/constants.ts @@ -2,4 +2,10 @@ // Licensed under the MIT license. export const FEATURE_MANAGEMENT_KEY_NAME = "feature_management"; -export const FEATURE_FLAGS_KEY_NAME = "feature_flags"; +export const FEATURE_FLAGS_KEY_NAME = "feature_flags"; +export const TELEMETRY_KEY_NAME = "telemetry"; +export const ENABLED_KEY_NAME = "enabled"; +export const METADATA_KEY_NAME = "metadata"; +export const ETAG_KEY_NAME = "Etag"; +export const FEATURE_FLAG_ID_KEY_NAME = "FeatureFlagId"; +export const FEATURE_FLAG_REFERENCE_KEY_NAME = "FeatureFlagReference"; diff --git a/src/load.ts b/src/load.ts index 10dda11e..fffd09b8 100644 --- a/src/load.ts +++ b/src/load.ts @@ -32,6 +32,7 @@ export async function load( ): Promise { const startTimestamp = Date.now(); let client: AppConfigurationClient; + let clientEndpoint: string | undefined; let options: AzureAppConfigurationOptions | undefined; // input validation @@ -40,12 +41,13 @@ export async function load( options = credentialOrOptions as AzureAppConfigurationOptions; const clientOptions = getClientOptions(options); client = new AppConfigurationClient(connectionString, clientOptions); + clientEndpoint = getEndpoint(connectionStringOrEndpoint); } else if ((connectionStringOrEndpoint instanceof URL || typeof connectionStringOrEndpoint === "string") && instanceOfTokenCredential(credentialOrOptions)) { - let endpoint = connectionStringOrEndpoint; // ensure string is a valid URL. - if (typeof endpoint === "string") { + if (typeof connectionStringOrEndpoint === "string") { try { - endpoint = new URL(endpoint); + const endpointUrl = new URL(connectionStringOrEndpoint); + clientEndpoint = endpointUrl.toString(); } catch (error) { if (error.code === "ERR_INVALID_URL") { throw new Error("Invalid endpoint URL.", { cause: error }); @@ -53,17 +55,19 @@ export async function load( throw error; } } + } else { + clientEndpoint = connectionStringOrEndpoint.toString(); } const credential = credentialOrOptions as TokenCredential; options = appConfigOptions; const clientOptions = getClientOptions(options); - client = new AppConfigurationClient(endpoint.toString(), credential, clientOptions); + client = new AppConfigurationClient(clientEndpoint, credential, clientOptions); } 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, options); + const appConfiguration = new AzureAppConfigurationImpl(client, clientEndpoint, options); await appConfiguration.load(); return appConfiguration; } catch (error) { @@ -104,3 +108,18 @@ function getClientOptions(options?: AzureAppConfigurationOptions): AppConfigurat } }); } + +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/test/featureFlag.test.ts b/test/featureFlag.test.ts index 5022c0fe..05537fcc 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"; -import { createMockedConnectionString, createMockedFeatureFlag, createMockedKeyValue, mockAppConfigurationClientListConfigurationSettings, restoreMocks } from "./utils/testHelper"; +import { createMockedConnectionString, createMockedEndpoint, createMockedFeatureFlag, createMockedKeyValue, mockAppConfigurationClientListConfigurationSettings, restoreMocks } from "./utils/testHelper"; chai.use(chaiAsPromised); const expect = chai.expect; @@ -54,6 +54,8 @@ const mockedKVs = [{ createMockedFeatureFlag("Beta", { enabled: true }), createMockedFeatureFlag("Alpha_1", { enabled: true }), createMockedFeatureFlag("Alpha_2", { enabled: false }), + createMockedFeatureFlag("Telemetry_1", { enabled: true, telemetry: { enabled: true } }, { etag: "Etag"}), + createMockedFeatureFlag("Telemetry_2", { enabled: true, telemetry: { enabled: true } }, { etag: "Etag", label: "Test"}) ]); describe("feature flags", function () { @@ -158,4 +160,44 @@ describe("feature flags", function () { expect(variant.telemetry).not.undefined; }); + it("should populate telemetry metadata", async () => { + const connectionString = createMockedConnectionString(); + const settings = await load(connectionString, { + featureFlagOptions: { + enabled: true, + selectors: [ + { + keyFilter: "Telemetry_1" + }, + { + keyFilter: "Telemetry_2", + labelFilter: "Test" + } + ] + } + }); + expect(settings).not.undefined; + expect(settings.get("feature_management")).not.undefined; + const featureFlags = settings.get("feature_management").feature_flags; + expect(featureFlags).not.undefined; + expect((featureFlags as []).length).equals(2); + + let featureFlag = featureFlags[0]; + expect(featureFlag).not.undefined; + expect(featureFlag.id).equals("Telemetry_1"); + expect(featureFlag.telemetry).not.undefined; + expect(featureFlag.telemetry.enabled).equals(true); + expect(featureFlag.telemetry.metadata.Etag).equals("Etag"); + expect(featureFlag.telemetry.metadata.FeatureFlagId).equals("krkOsu9dVV9huwbQDPR6gkV_2T0buWxOCS-nNsj5-6g"); + expect(featureFlag.telemetry.metadata.FeatureFlagReference).equals(`${createMockedEndpoint()}/kv/.appconfig.featureflag/Telemetry_1`); + + featureFlag = featureFlags[1]; + expect(featureFlag).not.undefined; + expect(featureFlag.id).equals("Telemetry_2"); + expect(featureFlag.telemetry).not.undefined; + expect(featureFlag.telemetry.enabled).equals(true); + expect(featureFlag.telemetry.metadata.Etag).equals("Etag"); + expect(featureFlag.telemetry.metadata.FeatureFlagId).equals("Rc8Am7HIGDT7HC5Ovs3wKN_aGaaK_Uz1mH2e11gaK0o"); + expect(featureFlag.telemetry.metadata.FeatureFlagReference).equals(`${createMockedEndpoint()}/kv/.appconfig.featureflag/Telemetry_2?label=Test`); + }); }); From f7ea66cab66d164f38a1e8ab5f9b75bb1303ee9b Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Thu, 17 Oct 2024 17:33:31 +0800 Subject: [PATCH 03/35] Update to ETag (#110) * update to ETag * revert change --- src/featureManagement/constants.ts | 2 +- test/featureFlag.test.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/featureManagement/constants.ts b/src/featureManagement/constants.ts index fe4d8cdf..55a1dc54 100644 --- a/src/featureManagement/constants.ts +++ b/src/featureManagement/constants.ts @@ -6,6 +6,6 @@ export const FEATURE_FLAGS_KEY_NAME = "feature_flags"; export const TELEMETRY_KEY_NAME = "telemetry"; export const ENABLED_KEY_NAME = "enabled"; export const METADATA_KEY_NAME = "metadata"; -export const ETAG_KEY_NAME = "Etag"; +export const ETAG_KEY_NAME = "ETag"; export const FEATURE_FLAG_ID_KEY_NAME = "FeatureFlagId"; export const FEATURE_FLAG_REFERENCE_KEY_NAME = "FeatureFlagReference"; diff --git a/test/featureFlag.test.ts b/test/featureFlag.test.ts index 05537fcc..dfca1c0e 100644 --- a/test/featureFlag.test.ts +++ b/test/featureFlag.test.ts @@ -54,8 +54,8 @@ const mockedKVs = [{ createMockedFeatureFlag("Beta", { enabled: true }), createMockedFeatureFlag("Alpha_1", { enabled: true }), createMockedFeatureFlag("Alpha_2", { enabled: false }), - createMockedFeatureFlag("Telemetry_1", { enabled: true, telemetry: { enabled: true } }, { etag: "Etag"}), - createMockedFeatureFlag("Telemetry_2", { enabled: true, telemetry: { enabled: true } }, { etag: "Etag", label: "Test"}) + createMockedFeatureFlag("Telemetry_1", { enabled: true, telemetry: { enabled: true } }, { etag: "ETag"}), + createMockedFeatureFlag("Telemetry_2", { enabled: true, telemetry: { enabled: true } }, { etag: "ETag", label: "Test"}) ]); describe("feature flags", function () { @@ -187,7 +187,7 @@ describe("feature flags", function () { expect(featureFlag.id).equals("Telemetry_1"); expect(featureFlag.telemetry).not.undefined; expect(featureFlag.telemetry.enabled).equals(true); - expect(featureFlag.telemetry.metadata.Etag).equals("Etag"); + expect(featureFlag.telemetry.metadata.ETag).equals("ETag"); expect(featureFlag.telemetry.metadata.FeatureFlagId).equals("krkOsu9dVV9huwbQDPR6gkV_2T0buWxOCS-nNsj5-6g"); expect(featureFlag.telemetry.metadata.FeatureFlagReference).equals(`${createMockedEndpoint()}/kv/.appconfig.featureflag/Telemetry_1`); @@ -196,7 +196,7 @@ describe("feature flags", function () { expect(featureFlag.id).equals("Telemetry_2"); expect(featureFlag.telemetry).not.undefined; expect(featureFlag.telemetry.enabled).equals(true); - expect(featureFlag.telemetry.metadata.Etag).equals("Etag"); + expect(featureFlag.telemetry.metadata.ETag).equals("ETag"); expect(featureFlag.telemetry.metadata.FeatureFlagId).equals("Rc8Am7HIGDT7HC5Ovs3wKN_aGaaK_Uz1mH2e11gaK0o"); expect(featureFlag.telemetry.metadata.FeatureFlagReference).equals(`${createMockedEndpoint()}/kv/.appconfig.featureflag/Telemetry_2?label=Test`); }); From 8de2818c8803ebff5c0edd2231dfa7ae07e81037 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Fri, 18 Oct 2024 04:19:37 +0800 Subject: [PATCH 04/35] add AllocationId to telemetry metadata --- src/AzureAppConfigurationImpl.ts | 145 ++++++++++++++++++++++++++++- src/featureManagement/constants.ts | 12 +++ 2 files changed, 156 insertions(+), 1 deletion(-) diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index 7f5600fc..88d27290 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -9,7 +9,27 @@ import { IKeyValueAdapter } from "./IKeyValueAdapter"; import { JsonKeyValueAdapter } from "./JsonKeyValueAdapter"; import { DEFAULT_REFRESH_INTERVAL_IN_MS, MIN_REFRESH_INTERVAL_IN_MS } from "./RefreshOptions"; import { Disposable } from "./common/disposable"; -import { FEATURE_FLAGS_KEY_NAME, FEATURE_MANAGEMENT_KEY_NAME, TELEMETRY_KEY_NAME, ENABLED_KEY_NAME, METADATA_KEY_NAME, ETAG_KEY_NAME, FEATURE_FLAG_ID_KEY_NAME, FEATURE_FLAG_REFERENCE_KEY_NAME } from "./featureManagement/constants"; +import { + FEATURE_FLAGS_KEY_NAME, + FEATURE_MANAGEMENT_KEY_NAME, + NAME_KEY_NAME, + TELEMETRY_KEY_NAME, + ENABLED_KEY_NAME, + METADATA_KEY_NAME, + ETAG_KEY_NAME, + FEATURE_FLAG_ID_KEY_NAME, + FEATURE_FLAG_REFERENCE_KEY_NAME, + ALLOCATION_KEY_NAME, + DEFAULT_WHEN_ENABLED_KEY_NAME, + DEFAULT_WHEN_DISABLED_KEY_NAME, + PERCENTILE_KEY_NAME, + FROM_KEY_NAME, + TO_KEY_NAME, + SEED_KEY_NAME, + VARIANT_KEY_NAME, + VARIANTS_KEY_NAME, + CONFIGURATION_VALUE_KEY_NAME +} from "./featureManagement/constants"; import { AzureKeyVaultKeyValueAdapter } from "./keyvault/AzureKeyVaultKeyValueAdapter"; import { RefreshTimer } from "./refresh/RefreshTimer"; import { getConfigurationSettingWithTrace, listConfigurationSettingsWithTrace, requestTracingEnabled } from "./requestTracing/utils"; @@ -550,6 +570,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { [ETAG_KEY_NAME]: setting.etag, [FEATURE_FLAG_ID_KEY_NAME]: await this.#calculateFeatureFlagId(setting), [FEATURE_FLAG_REFERENCE_KEY_NAME]: this.#createFeatureFlagReference(setting), + [ALLOCATION_KEY_NAME]: await this.#generateAllocationId(featureFlag), ...(metadata || {}) }; } @@ -595,6 +616,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { if (crypto.subtle) { const hashBuffer = await crypto.subtle.digest("SHA-256", data); const hashArray = new Uint8Array(hashBuffer); + // btoa/atob is also available in Node.js 18+ const base64String = btoa(String.fromCharCode(...hashArray)); const base64urlString = base64String.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); return base64urlString; @@ -613,6 +635,127 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { } return featureFlagReference; } + + async #generateAllocationId(featureFlag: any): Promise { + let rawAllocationId = ""; + // Only default variant when enabled and variants allocated by percentile involve in the experimentation + // The allocation id is genearted from default variant when enabled and percentile allocation + const variantsForExperiementation: string[] = []; + + if (featureFlag[ALLOCATION_KEY_NAME]) { + rawAllocationId += `seed=${featureFlag[ALLOCATION_KEY_NAME][SEED_KEY_NAME] ?? ""}\ndefault_when_enabled=`; + + if (featureFlag[ALLOCATION_KEY_NAME][DEFAULT_WHEN_ENABLED_KEY_NAME]) { + variantsForExperiementation.push(featureFlag[ALLOCATION_KEY_NAME][DEFAULT_WHEN_ENABLED_KEY_NAME]); + rawAllocationId += `${featureFlag[ALLOCATION_KEY_NAME][DEFAULT_WHEN_ENABLED_KEY_NAME]}`; + } + + rawAllocationId += `\npercentiles=`; + + const percentileList = featureFlag[ALLOCATION_KEY_NAME][PERCENTILE_KEY_NAME]; + if (percentileList) { + const sortedPercentileList = percentileList + .filter(p => + (p[FROM_KEY_NAME] !== undefined) && + (p[TO_KEY_NAME] !== undefined) && + (p[VARIANT_KEY_NAME] !== undefined) && + (p[FROM_KEY_NAME] !== p[TO_KEY_NAME])) + .sort((a, b) => a[FROM_KEY_NAME] - b[FROM_KEY_NAME]); + + const percentileAllocation: string[] = []; + for (const percentile of sortedPercentileList) { + variantsForExperiementation.push(percentile[VARIANT_KEY_NAME]); + percentileAllocation.push(`${percentile[FROM_KEY_NAME]},${base64Helper(percentile[VARIANT_KEY_NAME])},${percentile[TO_KEY_NAME]}`); + } + rawAllocationId += percentileAllocation.join(";"); + } + } + + if (variantsForExperiementation.length === 0 && featureFlag[ALLOCATION_KEY_NAME][SEED_KEY_NAME] === undefined) { + // All fields required for generating allocation id are missing, short-circuit and return empty string + return ""; + } + + rawAllocationId += "\nvariants="; + + if (variantsForExperiementation.length !== 0) { + const variantsList = featureFlag[VARIANTS_KEY_NAME]; + if (variantsList) { + const sortedVariantsList = variantsList + .filter(v => + (v[NAME_KEY_NAME] !== undefined) && + variantsForExperiementation.includes(v[NAME_KEY_NAME])) + .sort((a, b) => (a.name > b.name ? 1 : -1)); + + const variantConfiguration: string[] = []; + for (const variant of sortedVariantsList) { + const configurationValue = JSON.stringify(variant[CONFIGURATION_VALUE_KEY_NAME], null, 0) ?? ""; + variantConfiguration.push(`${base64Helper(variant[NAME_KEY_NAME])},${configurationValue}`) + } + rawAllocationId += variantConfiguration.join(";"); + } + } + + let crypto; + + // Check for browser environment + if (typeof window !== "undefined" && window.crypto && window.crypto.subtle) { + crypto = window.crypto; + } + // Check for Node.js environment + else if (typeof global !== "undefined" && global.crypto) { + crypto = global.crypto; + } + // Fallback to native Node.js crypto module + else { + try { + if (typeof module !== "undefined" && module.exports) { + crypto = require("crypto"); + } + else { + crypto = await import("crypto"); + } + } catch (error) { + console.error("Failed to load the crypto module:", error.message); + throw error; + } + } + + // Convert to UTF-8 encoded bytes + const data = new TextEncoder().encode(rawAllocationId); + + // In the browser, use crypto.subtle.digest + if (crypto.subtle) { + const hashBuffer = await crypto.subtle.digest("SHA-256", data); + const hashArray = new Uint8Array(hashBuffer); + + // Only use the first 15 bytes + const first15Bytes = hashArray.slice(0, 15); + + // btoa/atob is also available in Node.js 18+ + const base64String = btoa(String.fromCharCode(...first15Bytes)); + const base64urlString = base64String.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); + return base64urlString; + } + // In Node.js, use the crypto module's hash function + else { + const hash = crypto.createHash("sha256").update(data).digest(); + + // Only use the first 15 bytes + const first15Bytes = hash.slice(0, 15); + + return first15Bytes.toString("base64url"); + } + } +} + +function base64Helper(str: string): string { + const bytes = new TextEncoder().encode(str); // UTF-8 encoding + let chars = ""; + for (let i = 0; i < bytes.length; i++) { + chars += String.fromCharCode(bytes[i]); + } + return btoa(chars); } function getValidSelectors(selectors: SettingSelector[]): SettingSelector[] { diff --git a/src/featureManagement/constants.ts b/src/featureManagement/constants.ts index 55a1dc54..47a04d50 100644 --- a/src/featureManagement/constants.ts +++ b/src/featureManagement/constants.ts @@ -3,9 +3,21 @@ export const FEATURE_MANAGEMENT_KEY_NAME = "feature_management"; export const FEATURE_FLAGS_KEY_NAME = "feature_flags"; +export const NAME_KEY_NAME = "name"; export const TELEMETRY_KEY_NAME = "telemetry"; export const ENABLED_KEY_NAME = "enabled"; export const METADATA_KEY_NAME = "metadata"; export const ETAG_KEY_NAME = "ETag"; export const FEATURE_FLAG_ID_KEY_NAME = "FeatureFlagId"; export const FEATURE_FLAG_REFERENCE_KEY_NAME = "FeatureFlagReference"; +export const ALLOCATION_KEY_NAME = "allocation"; +export const DEFAULT_WHEN_ENABLED_KEY_NAME = "default_when_enabled"; +export const DEFAULT_WHEN_DISABLED_KEY_NAME = "default_when_disabled"; +export const PERCENTILE_KEY_NAME = "percentile"; +export const FROM_KEY_NAME = "from"; +export const TO_KEY_NAME = "to"; +export const SEED_KEY_NAME = "seed"; +export const VARIANT_KEY_NAME = "variant"; +export const VARIANTS_KEY_NAME = "variants"; +export const CONFIGURATION_VALUE_KEY_NAME = "configuration_value"; +export const ALLOCATION_ID_KEY_NAME = "AllocationId"; From 4df864b18ef83a7bfcc86e67103dda6fb3b0cc1b Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Fri, 18 Oct 2024 04:22:58 +0800 Subject: [PATCH 05/35] fix lint --- src/AzureAppConfigurationImpl.ts | 25 ++++++++++++------------- src/featureManagement/constants.ts | 1 - 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index 88d27290..21550a7b 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -9,7 +9,7 @@ import { IKeyValueAdapter } from "./IKeyValueAdapter"; import { JsonKeyValueAdapter } from "./JsonKeyValueAdapter"; import { DEFAULT_REFRESH_INTERVAL_IN_MS, MIN_REFRESH_INTERVAL_IN_MS } from "./RefreshOptions"; import { Disposable } from "./common/disposable"; -import { +import { FEATURE_FLAGS_KEY_NAME, FEATURE_MANAGEMENT_KEY_NAME, NAME_KEY_NAME, @@ -21,7 +21,6 @@ import { FEATURE_FLAG_REFERENCE_KEY_NAME, ALLOCATION_KEY_NAME, DEFAULT_WHEN_ENABLED_KEY_NAME, - DEFAULT_WHEN_DISABLED_KEY_NAME, PERCENTILE_KEY_NAME, FROM_KEY_NAME, TO_KEY_NAME, @@ -650,15 +649,15 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { rawAllocationId += `${featureFlag[ALLOCATION_KEY_NAME][DEFAULT_WHEN_ENABLED_KEY_NAME]}`; } - rawAllocationId += `\npercentiles=`; + rawAllocationId += "\npercentiles="; const percentileList = featureFlag[ALLOCATION_KEY_NAME][PERCENTILE_KEY_NAME]; if (percentileList) { const sortedPercentileList = percentileList - .filter(p => - (p[FROM_KEY_NAME] !== undefined) && - (p[TO_KEY_NAME] !== undefined) && - (p[VARIANT_KEY_NAME] !== undefined) && + .filter(p => + (p[FROM_KEY_NAME] !== undefined) && + (p[TO_KEY_NAME] !== undefined) && + (p[VARIANT_KEY_NAME] !== undefined) && (p[FROM_KEY_NAME] !== p[TO_KEY_NAME])) .sort((a, b) => a[FROM_KEY_NAME] - b[FROM_KEY_NAME]); @@ -670,9 +669,9 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { rawAllocationId += percentileAllocation.join(";"); } } - + if (variantsForExperiementation.length === 0 && featureFlag[ALLOCATION_KEY_NAME][SEED_KEY_NAME] === undefined) { - // All fields required for generating allocation id are missing, short-circuit and return empty string + // All fields required for generating allocation id are missing, short-circuit and return empty string return ""; } @@ -682,15 +681,15 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { const variantsList = featureFlag[VARIANTS_KEY_NAME]; if (variantsList) { const sortedVariantsList = variantsList - .filter(v => - (v[NAME_KEY_NAME] !== undefined) && + .filter(v => + (v[NAME_KEY_NAME] !== undefined) && variantsForExperiementation.includes(v[NAME_KEY_NAME])) .sort((a, b) => (a.name > b.name ? 1 : -1)); - + const variantConfiguration: string[] = []; for (const variant of sortedVariantsList) { const configurationValue = JSON.stringify(variant[CONFIGURATION_VALUE_KEY_NAME], null, 0) ?? ""; - variantConfiguration.push(`${base64Helper(variant[NAME_KEY_NAME])},${configurationValue}`) + variantConfiguration.push(`${base64Helper(variant[NAME_KEY_NAME])},${configurationValue}`); } rawAllocationId += variantConfiguration.join(";"); } diff --git a/src/featureManagement/constants.ts b/src/featureManagement/constants.ts index 47a04d50..7b6bda8e 100644 --- a/src/featureManagement/constants.ts +++ b/src/featureManagement/constants.ts @@ -12,7 +12,6 @@ export const FEATURE_FLAG_ID_KEY_NAME = "FeatureFlagId"; export const FEATURE_FLAG_REFERENCE_KEY_NAME = "FeatureFlagReference"; export const ALLOCATION_KEY_NAME = "allocation"; export const DEFAULT_WHEN_ENABLED_KEY_NAME = "default_when_enabled"; -export const DEFAULT_WHEN_DISABLED_KEY_NAME = "default_when_disabled"; export const PERCENTILE_KEY_NAME = "percentile"; export const FROM_KEY_NAME = "from"; export const TO_KEY_NAME = "to"; From 638338a3b66dfa6d7cbae31d1b9121061550902d Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Fri, 18 Oct 2024 04:36:37 +0800 Subject: [PATCH 06/35] update --- src/AzureAppConfigurationImpl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index 21550a7b..023ee796 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -670,7 +670,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { } } - if (variantsForExperiementation.length === 0 && featureFlag[ALLOCATION_KEY_NAME][SEED_KEY_NAME] === undefined) { + if (featureFlag[ALLOCATION_KEY_NAME] === undefined || (variantsForExperiementation.length === 0 && featureFlag[ALLOCATION_KEY_NAME][SEED_KEY_NAME] === undefined)) { // All fields required for generating allocation id are missing, short-circuit and return empty string return ""; } From 6df197007c8a320f01e8e4b6c7bbed0f7250fc1b Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Fri, 18 Oct 2024 13:43:13 +0800 Subject: [PATCH 07/35] add testcases --- src/AzureAppConfigurationImpl.ts | 4 +- test/featureFlag.test.ts | 109 ++++++++++++++++++++++++++++++- 2 files changed, 111 insertions(+), 2 deletions(-) diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index 023ee796..6f8c1b42 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -19,6 +19,7 @@ import { ETAG_KEY_NAME, FEATURE_FLAG_ID_KEY_NAME, FEATURE_FLAG_REFERENCE_KEY_NAME, + ALLOCATION_ID_KEY_NAME, ALLOCATION_KEY_NAME, DEFAULT_WHEN_ENABLED_KEY_NAME, PERCENTILE_KEY_NAME, @@ -565,11 +566,12 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { if (featureFlag[TELEMETRY_KEY_NAME] && featureFlag[TELEMETRY_KEY_NAME][ENABLED_KEY_NAME] === true) { const metadata = featureFlag[TELEMETRY_KEY_NAME][METADATA_KEY_NAME]; + const allocationId = await this.#generateAllocationId(featureFlag); featureFlag[TELEMETRY_KEY_NAME][METADATA_KEY_NAME] = { [ETAG_KEY_NAME]: setting.etag, [FEATURE_FLAG_ID_KEY_NAME]: await this.#calculateFeatureFlagId(setting), [FEATURE_FLAG_REFERENCE_KEY_NAME]: this.#createFeatureFlagReference(setting), - [ALLOCATION_KEY_NAME]: await this.#generateAllocationId(featureFlag), + ...(allocationId !== "" && { [ALLOCATION_ID_KEY_NAME]: allocationId }), ...(metadata || {}) }; } diff --git a/test/featureFlag.test.ts b/test/featureFlag.test.ts index dfca1c0e..71f05995 100644 --- a/test/featureFlag.test.ts +++ b/test/featureFlag.test.ts @@ -55,7 +55,62 @@ const mockedKVs = [{ createMockedFeatureFlag("Alpha_1", { enabled: true }), createMockedFeatureFlag("Alpha_2", { enabled: false }), createMockedFeatureFlag("Telemetry_1", { enabled: true, telemetry: { enabled: true } }, { etag: "ETag"}), - createMockedFeatureFlag("Telemetry_2", { enabled: true, telemetry: { enabled: true } }, { etag: "ETag", label: "Test"}) + createMockedFeatureFlag("Telemetry_2", { enabled: true, telemetry: { enabled: true } }, { etag: "ETag", label: "Test"}), + createMockedFeatureFlag("NoPercentileAndSeed", { + enabled: true, + telemetry: { enabled: true }, + variants: [ { name: "Control" }, { name: "Test" } ], + allocation: { + default_when_disabled: "Control", + user: [ {users: ["Jeff"], variant: "Test"} ] + } + }), + createMockedFeatureFlag("SeedOnly", { + enabled: true, + telemetry: { enabled: true }, + variants: [ { name: "Control" }, { name: "Test" } ], + allocation: { + default_when_disabled: "Control", + user: [ {users: ["Jeff"], variant: "Test"} ], + seed: "123" + } + }), + createMockedFeatureFlag("DefaultWhenEnabledOnly", { + enabled: true, + telemetry: { enabled: true }, + variants: [ { name: "Control" }, { name: "Test" } ], + allocation: { + default_when_enabled: "Control" + } + }), + createMockedFeatureFlag("PercentileOnly", { + enabled: true, + telemetry: { enabled: true }, + variants: [ ], + allocation: { + percentile: [ { from: 0, to: 50, variant: "Control" }, { from: 50, to: 100, variant: "Test" } ] + } + }), + createMockedFeatureFlag("SimpleConfigurationValue", { + enabled: true, + telemetry: { enabled: true }, + variants: [ { name: "Control", configuration_value: "standard" }, { name: "Test", configuration_value: "special" } ], + allocation: { + default_when_enabled: "Control", + percentile: [ { from: 0, to: 50, variant: "Control" }, { from: 50, to: 100, variant: "Test" } ], + seed: "123" + } + }), + createMockedFeatureFlag("ComplexConfigurationValue", { + enabled: true, + telemetry: { enabled: true }, + variants: [ { name: "Control", configuration_value: { title: { size: 100, color: "red" }, options: [ 1, 2, 3 ]} }, { name: "Test", configuration_value: { title: { size: 200, color: "blue" }, options: [ "1", "2", "3" ]} } ], + allocation: { + default_when_enabled: "Control", + percentile: [ { from: 0, to: 50, variant: "Control" }, { from: 50, to: 100, variant: "Test" } ], + seed: "123" + } + }), ]); describe("feature flags", function () { @@ -200,4 +255,56 @@ describe("feature flags", function () { expect(featureFlag.telemetry.metadata.FeatureFlagId).equals("Rc8Am7HIGDT7HC5Ovs3wKN_aGaaK_Uz1mH2e11gaK0o"); expect(featureFlag.telemetry.metadata.FeatureFlagReference).equals(`${createMockedEndpoint()}/kv/.appconfig.featureflag/Telemetry_2?label=Test`); }); + + it("should not populate allocation id", async () => { + const connectionString = createMockedConnectionString(); + const settings = await load(connectionString, { + featureFlagOptions: { + enabled: true, + selectors: [ { keyFilter: "*" } ] + } + }); + expect(settings).not.undefined; + expect(settings.get("feature_management")).not.undefined; + const featureFlags = settings.get("feature_management").feature_flags; + expect(featureFlags).not.undefined; + + const NoPercentileAndSeed = (featureFlags as any[]).find(item => item.id === "NoPercentileAndSeed"); + expect(NoPercentileAndSeed).not.undefined; + expect(NoPercentileAndSeed?.telemetry.metadata.AllocationId).to.be.undefined; + }); + + it("should populate allocation id", async () => { + const connectionString = createMockedConnectionString(); + const settings = await load(connectionString, { + featureFlagOptions: { + enabled: true, + selectors: [ { keyFilter: "*" } ] + } + }); + expect(settings).not.undefined; + expect(settings.get("feature_management")).not.undefined; + const featureFlags = settings.get("feature_management").feature_flags; + expect(featureFlags).not.undefined; + + const SeedOnly = (featureFlags as any[]).find(item => item.id === "SeedOnly"); + expect(SeedOnly).not.undefined; + expect(SeedOnly?.telemetry.metadata.AllocationId).equals("qZApcKdfXscxpgn_8CMf"); + + const DefaultWhenEnabledOnly = (featureFlags as any[]).find(item => item.id === "DefaultWhenEnabledOnly"); + expect(DefaultWhenEnabledOnly).not.undefined; + expect(DefaultWhenEnabledOnly?.telemetry.metadata.AllocationId).equals("k486zJjud_HkKaL1C4qB"); + + const PercentileOnly = (featureFlags as any[]).find(item => item.id === "PercentileOnly"); + expect(PercentileOnly).not.undefined; + expect(PercentileOnly?.telemetry.metadata.AllocationId).equals("5YUbmP0P5s47zagO_LvI"); + + const SimpleConfigurationValue = (featureFlags as any[]).find(item => item.id === "SimpleConfigurationValue"); + expect(SimpleConfigurationValue).not.undefined; + expect(SimpleConfigurationValue?.telemetry.metadata.AllocationId).equals("QIOEOTQJr2AXo4dkFFqy"); + + const ComplexConfigurationValue = (featureFlags as any[]).find(item => item.id === "ComplexConfigurationValue"); + expect(ComplexConfigurationValue).not.undefined; + expect(ComplexConfigurationValue?.telemetry.metadata.AllocationId).equals("72A0b4sZ5HSAtaQxFe73"); + }); }); From 233553fe3409864a1fcc6fb7ba491834c190702d Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Thu, 24 Oct 2024 12:30:39 +0800 Subject: [PATCH 08/35] sort json key --- src/AzureAppConfigurationImpl.ts | 12 ++--------- src/common/utils.ts | 24 ++++++++++++++++++++++ test/featureFlag.test.ts | 34 +++++++++++++++++++++++++++++++- 3 files changed, 59 insertions(+), 11 deletions(-) create mode 100644 src/common/utils.ts diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index 6f8c1b42..e7c6e6af 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -9,6 +9,7 @@ import { IKeyValueAdapter } from "./IKeyValueAdapter"; import { JsonKeyValueAdapter } from "./JsonKeyValueAdapter"; import { DEFAULT_REFRESH_INTERVAL_IN_MS, MIN_REFRESH_INTERVAL_IN_MS } from "./RefreshOptions"; import { Disposable } from "./common/disposable"; +import { base64Helper, jsonSorter } from "./common/utils"; import { FEATURE_FLAGS_KEY_NAME, FEATURE_MANAGEMENT_KEY_NAME, @@ -690,7 +691,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { const variantConfiguration: string[] = []; for (const variant of sortedVariantsList) { - const configurationValue = JSON.stringify(variant[CONFIGURATION_VALUE_KEY_NAME], null, 0) ?? ""; + const configurationValue = JSON.stringify(variant[CONFIGURATION_VALUE_KEY_NAME], jsonSorter) ?? ""; variantConfiguration.push(`${base64Helper(variant[NAME_KEY_NAME])},${configurationValue}`); } rawAllocationId += variantConfiguration.join(";"); @@ -750,15 +751,6 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { } } -function base64Helper(str: string): string { - const bytes = new TextEncoder().encode(str); // UTF-8 encoding - let chars = ""; - for (let i = 0; i < bytes.length; i++) { - chars += String.fromCharCode(bytes[i]); - } - return btoa(chars); -} - function getValidSelectors(selectors: SettingSelector[]): SettingSelector[] { // below code deduplicates selectors by keyFilter and labelFilter, the latter selector wins const uniqueSelectors: SettingSelector[] = []; diff --git a/src/common/utils.ts b/src/common/utils.ts new file mode 100644 index 00000000..a6d84812 --- /dev/null +++ b/src/common/utils.ts @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +export function base64Helper(str: string): string { + const bytes = new TextEncoder().encode(str); // UTF-8 encoding + let chars = ""; + for (let i = 0; i < bytes.length; i++) { + chars += String.fromCharCode(bytes[i]); + } + return btoa(chars); +} + +export function jsonSorter(key, value) { + if (value === null) { + return null; + } + if (Array.isArray(value)) { + return value; + } + if (typeof value === "object") { + return Object.fromEntries(Object.entries(value).sort()); + } + return value; +} \ No newline at end of file diff --git a/test/featureFlag.test.ts b/test/featureFlag.test.ts index 71f05995..708c301e 100644 --- a/test/featureFlag.test.ts +++ b/test/featureFlag.test.ts @@ -111,6 +111,34 @@ const mockedKVs = [{ seed: "123" } }), + createMockedFeatureFlag("TelemetryVariantPercentile", { + enabled: true, + telemetry: { enabled: true }, + variants: [ + { + name: "True_Override", + configuration_value: { + someOtherKey: { + someSubKey: "someSubValue" + }, + someKey4: [3, 1, 4, true], + someKey: "someValue", + someKey3: 3.14, + someKey2: 3 + } + } + ], + allocation: { + default_when_enabled: "True_Override", + percentile: [ + { + variant: "True_Override", + from: 0, + to: 100 + } + ] + } + }) ]); describe("feature flags", function () { @@ -305,6 +333,10 @@ describe("feature flags", function () { const ComplexConfigurationValue = (featureFlags as any[]).find(item => item.id === "ComplexConfigurationValue"); expect(ComplexConfigurationValue).not.undefined; - expect(ComplexConfigurationValue?.telemetry.metadata.AllocationId).equals("72A0b4sZ5HSAtaQxFe73"); + expect(ComplexConfigurationValue?.telemetry.metadata.AllocationId).equals("4Bes0AlwuO8kYX-YkBWs"); + + const TelemetryVariantPercentile = (featureFlags as any[]).find(item => item.id === "TelemetryVariantPercentile"); + expect(TelemetryVariantPercentile).not.undefined; + expect(TelemetryVariantPercentile?.telemetry.metadata.AllocationId).equals("YsdJ4pQpmhYa8KEhRLUn"); }); }); From 6e7724c5d5f0f35e71380cb679d780e41de2ddd9 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Thu, 24 Oct 2024 13:35:58 +0800 Subject: [PATCH 09/35] fix lint --- src/common/utils.ts | 2 +- test/featureFlag.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/common/utils.ts b/src/common/utils.ts index a6d84812..ad827bbb 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -21,4 +21,4 @@ export function jsonSorter(key, value) { return Object.fromEntries(Object.entries(value).sort()); } return value; -} \ No newline at end of file +} diff --git a/test/featureFlag.test.ts b/test/featureFlag.test.ts index 708c301e..03234e58 100644 --- a/test/featureFlag.test.ts +++ b/test/featureFlag.test.ts @@ -121,10 +121,10 @@ const mockedKVs = [{ someOtherKey: { someSubKey: "someSubValue" }, - someKey4: [3, 1, 4, true], + someKey4: [3, 1, 4, true], someKey: "someValue", someKey3: 3.14, - someKey2: 3 + someKey2: 3 } } ], From 0fe93b44a56c1f2b0668634f33132e3707b1572d Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Mon, 28 Oct 2024 16:45:46 +0800 Subject: [PATCH 10/35] update --- test/featureFlag.test.ts | 61 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/test/featureFlag.test.ts b/test/featureFlag.test.ts index 03234e58..ff77789b 100644 --- a/test/featureFlag.test.ts +++ b/test/featureFlag.test.ts @@ -138,6 +138,63 @@ const mockedKVs = [{ } ] } + }), + createMockedFeatureFlag("Complete", { + enabled: true, + telemetry: { enabled: true }, + variants: [ + { + name: "Large", + configuration_value: 100 + }, + { + name: "Medium", + configuration_value: 50 + }, + { + name: "Small", + configuration_value: 10 + } + ], + allocation: { + percentile: [ + { + variant: "Large", + from: 0, + to: 25 + }, + { + variant: "Medium", + from: 25, + to: 55 + }, + { + variant: "Small", + from: 55, + to: 95 + }, + { + variant: "Large", + from: 95, + to: 100 + } + ], + group: [ + { + variant: "Large", + groups: ["beta"] + } + ], + user: [ + { + variant: "Small", + users: ["Richel"] + } + ], + seed: "test-seed", + default_when_enabled: "Medium", + default_when_disabled: "Medium" + } }) ]); @@ -338,5 +395,9 @@ describe("feature flags", function () { const TelemetryVariantPercentile = (featureFlags as any[]).find(item => item.id === "TelemetryVariantPercentile"); expect(TelemetryVariantPercentile).not.undefined; expect(TelemetryVariantPercentile?.telemetry.metadata.AllocationId).equals("YsdJ4pQpmhYa8KEhRLUn"); + + const Complete = (featureFlags as any[]).find(item => item.id === "Complete"); + expect(Complete).not.undefined; + // expect(Complete?.telemetry.metadata.AllocationId).equals("CgBF0x9j_Ip5ccCrdiNO"); }); }); From 6dad98d8b18b0a6b82b6bb4a5c5b18f5a787dd54 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Tue, 29 Oct 2024 17:07:27 +0800 Subject: [PATCH 11/35] update testcase --- test/featureFlag.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/featureFlag.test.ts b/test/featureFlag.test.ts index ff77789b..4ef8ae7e 100644 --- a/test/featureFlag.test.ts +++ b/test/featureFlag.test.ts @@ -398,6 +398,6 @@ describe("feature flags", function () { const Complete = (featureFlags as any[]).find(item => item.id === "Complete"); expect(Complete).not.undefined; - // expect(Complete?.telemetry.metadata.AllocationId).equals("CgBF0x9j_Ip5ccCrdiNO"); + expect(Complete?.telemetry.metadata.AllocationId).equals("DER2rF-ZYog95c4CBZoi"); }); }); From 6dae81a14afb11bd84b2c718753bb9cbe7375c52 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Thu, 31 Oct 2024 13:26:21 +0800 Subject: [PATCH 12/35] New API to load from CDN endpoint (#106) * Adds additional undefined check (#104) * add loadCdn * revert change * fix lint * update to loadFromCdn & reuse load method * update * update * add js file extension to imports (#109) * update typescript version (#112) * add requestTracingOptions (#114) * Version bump 1.1.1 (#115) * bump up version 1.1.1 * update --------- Co-authored-by: Ross Grambo Co-authored-by: linglingye001 <143174321+linglingye001@users.noreply.github.com> --- package-lock.json | 2639 +----------------- package.json | 8 +- src/AzureAppConfiguration.ts | 2 +- src/AzureAppConfigurationImpl.ts | 32 +- src/AzureAppConfigurationOptions.ts | 16 +- src/JsonKeyValueAdapter.ts | 2 +- src/RefreshOptions.ts | 2 +- src/featureManagement/FeatureFlagOptions.ts | 6 +- src/index.ts | 9 +- src/keyvault/AzureKeyVaultKeyValueAdapter.ts | 6 +- src/load.ts | 25 +- src/requestTracing/RequestTracingOptions.ts | 12 + src/requestTracing/constants.ts | 2 +- src/requestTracing/utils.ts | 4 +- src/version.ts | 2 +- test/clientOptions.test.ts | 4 +- test/featureFlag.test.ts | 4 +- test/json.test.ts | 4 +- test/keyvault.test.ts | 6 +- test/load.test.ts | 4 +- test/refresh.test.ts | 6 +- test/requestTracing.test.ts | 18 +- 22 files changed, 137 insertions(+), 2676 deletions(-) create mode 100644 src/requestTracing/RequestTracingOptions.ts diff --git a/package-lock.json b/package-lock.json index b6171131..b587455c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,7 +1,7 @@ { "name": "@azure/app-configuration-provider", - "version": "1.1.0", - "lockfileVersion": 2, + "version": "1.1.1", + "lockfileVersion": 3, "requires": true, "packages": { "": { @@ -16,7 +16,7 @@ "devDependencies": { "@rollup/plugin-typescript": "^11.1.2", "@types/mocha": "^10.0.4", - "@types/node": "^20.5.7", + "@types/node": "^22.7.7", "@types/sinon": "^17.0.1", "@types/uuid": "^9.0.7", "@typescript-eslint/eslint-plugin": "^6.6.0", @@ -28,11 +28,11 @@ "mocha": "^10.2.0", "nock": "^13.3.3", "rimraf": "^5.0.1", - "rollup": "^3.26.3", + "rollup": "^3.29.5", "rollup-plugin-dts": "^5.3.0", "sinon": "^15.2.0", "tslib": "^2.6.0", - "typescript": "^5.1.6", + "typescript": "^5.6.3", "uuid": "^9.0.1" } }, @@ -865,12 +865,12 @@ "dev": true }, "node_modules/@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==", + "version": "22.7.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.7.tgz", + "integrity": "sha512-SRxCrrg9CL/y54aiMCG3edPKdprgMVGDXjA3gB8UmmBW5TcXzRUYAh8EWzTnSJFAd1rgImPELza+A3bJ+qxz8Q==", "dev": true, "dependencies": { - "undici-types": "~5.25.1" + "undici-types": "~6.19.2" } }, "node_modules/@types/semver": { @@ -2520,12 +2520,12 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -2865,9 +2865,9 @@ } }, "node_modules/path-to-regexp": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "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, "dependencies": { "isarray": "0.0.1" @@ -3081,9 +3081,9 @@ } }, "node_modules/rollup": { - "version": "3.29.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", - "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", + "version": "3.29.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", + "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", "dev": true, "bin": { "rollup": "dist/bin/rollup" @@ -3421,9 +3421,9 @@ } }, "node_modules/typescript": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -3434,9 +3434,9 @@ } }, "node_modules/undici-types": { - "version": "5.25.3", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz", - "integrity": "sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==", + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "dev": true }, "node_modules/uri-js": { @@ -3591,2594 +3591,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.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "requires": { - "braces": "^3.0.2", - "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.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", - "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.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", - "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", - "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/package.json b/package.json index 74aca6ec..25302993 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@azure/app-configuration-provider", - "version": "1.1.0", + "version": "1.1.1", "description": "The JavaScript configuration provider for Azure App Configuration", "main": "dist/index.js", "module": "./dist-esm/index.js", @@ -35,7 +35,7 @@ "devDependencies": { "@rollup/plugin-typescript": "^11.1.2", "@types/mocha": "^10.0.4", - "@types/node": "^20.5.7", + "@types/node": "^22.7.7", "@types/sinon": "^17.0.1", "@types/uuid": "^9.0.7", "@typescript-eslint/eslint-plugin": "^6.6.0", @@ -47,11 +47,11 @@ "mocha": "^10.2.0", "nock": "^13.3.3", "rimraf": "^5.0.1", - "rollup": "^3.26.3", + "rollup": "^3.29.5", "rollup-plugin-dts": "^5.3.0", "sinon": "^15.2.0", "tslib": "^2.6.0", - "typescript": "^5.1.6", + "typescript": "^5.6.3", "uuid": "^9.0.1" }, "dependencies": { diff --git a/src/AzureAppConfiguration.ts b/src/AzureAppConfiguration.ts index 7d8120d3..3f2918be 100644 --- a/src/AzureAppConfiguration.ts +++ b/src/AzureAppConfiguration.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { Disposable } from "./common/disposable"; +import { Disposable } from "./common/disposable.js"; export type AzureAppConfiguration = { /** diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index 7f5600fc..7ca88770 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -3,17 +3,17 @@ 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"; -import { AzureAppConfigurationOptions } from "./AzureAppConfigurationOptions"; -import { IKeyValueAdapter } from "./IKeyValueAdapter"; -import { JsonKeyValueAdapter } from "./JsonKeyValueAdapter"; -import { DEFAULT_REFRESH_INTERVAL_IN_MS, MIN_REFRESH_INTERVAL_IN_MS } from "./RefreshOptions"; -import { Disposable } from "./common/disposable"; -import { FEATURE_FLAGS_KEY_NAME, FEATURE_MANAGEMENT_KEY_NAME, TELEMETRY_KEY_NAME, ENABLED_KEY_NAME, METADATA_KEY_NAME, ETAG_KEY_NAME, FEATURE_FLAG_ID_KEY_NAME, FEATURE_FLAG_REFERENCE_KEY_NAME } from "./featureManagement/constants"; -import { AzureKeyVaultKeyValueAdapter } from "./keyvault/AzureKeyVaultKeyValueAdapter"; -import { RefreshTimer } from "./refresh/RefreshTimer"; -import { getConfigurationSettingWithTrace, listConfigurationSettingsWithTrace, requestTracingEnabled } from "./requestTracing/utils"; -import { KeyFilter, LabelFilter, SettingSelector } from "./types"; +import { AzureAppConfiguration, ConfigurationObjectConstructionOptions } from "./AzureAppConfiguration.js"; +import { AzureAppConfigurationOptions } from "./AzureAppConfigurationOptions.js"; +import { IKeyValueAdapter } from "./IKeyValueAdapter.js"; +import { JsonKeyValueAdapter } from "./JsonKeyValueAdapter.js"; +import { DEFAULT_REFRESH_INTERVAL_IN_MS, MIN_REFRESH_INTERVAL_IN_MS } from "./RefreshOptions.js"; +import { Disposable } from "./common/disposable.js"; +import { FEATURE_FLAGS_KEY_NAME, FEATURE_MANAGEMENT_KEY_NAME, TELEMETRY_KEY_NAME, ENABLED_KEY_NAME, METADATA_KEY_NAME, ETAG_KEY_NAME, FEATURE_FLAG_ID_KEY_NAME, FEATURE_FLAG_REFERENCE_KEY_NAME } from "./featureManagement/constants.js"; +import { AzureKeyVaultKeyValueAdapter } from "./keyvault/AzureKeyVaultKeyValueAdapter.js"; +import { RefreshTimer } from "./refresh/RefreshTimer.js"; +import { getConfigurationSettingWithTrace, listConfigurationSettingsWithTrace, requestTracingEnabled } from "./requestTracing/utils.js"; +import { KeyFilter, LabelFilter, SettingSelector } from "./types.js"; type PagedSettingSelector = SettingSelector & { /** @@ -66,7 +66,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { this.#options = options; // Enable request tracing if not opt-out - this.#requestTracingEnabled = requestTracingEnabled(); + this.#requestTracingEnabled = options?.requestTracingOptions?.enabled ?? requestTracingEnabled(); if (options?.trimKeyPrefixes) { this.#sortedTrimKeyPrefixes = [...options.trimKeyPrefixes].sort((a, b) => b.localeCompare(a)); @@ -143,19 +143,19 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { return this.#configMap.size; } - entries(): IterableIterator<[string, any]> { + entries(): MapIterator<[string, any]> { return this.#configMap.entries(); } - keys(): IterableIterator { + keys(): MapIterator { return this.#configMap.keys(); } - values(): IterableIterator { + values(): MapIterator { return this.#configMap.values(); } - [Symbol.iterator](): IterableIterator<[string, any]> { + [Symbol.iterator](): MapIterator<[string, any]> { return this.#configMap[Symbol.iterator](); } // #endregion diff --git a/src/AzureAppConfigurationOptions.ts b/src/AzureAppConfigurationOptions.ts index c98de463..a9df321d 100644 --- a/src/AzureAppConfigurationOptions.ts +++ b/src/AzureAppConfigurationOptions.ts @@ -2,10 +2,11 @@ // Licensed under the MIT license. import { AppConfigurationClientOptions } from "@azure/app-configuration"; -import { KeyVaultOptions } from "./keyvault/KeyVaultOptions"; -import { RefreshOptions } from "./RefreshOptions"; -import { SettingSelector } from "./types"; -import { FeatureFlagOptions } from "./featureManagement/FeatureFlagOptions"; +import { KeyVaultOptions } from "./keyvault/KeyVaultOptions.js"; +import { RefreshOptions } from "./RefreshOptions.js"; +import { SettingSelector } from "./types.js"; +import { FeatureFlagOptions } from "./featureManagement/FeatureFlagOptions.js"; +import { RequestTracingOptions } from "./requestTracing/RequestTracingOptions.js"; export const MaxRetries = 2; export const MaxRetryDelayInMs = 60000; @@ -47,4 +48,9 @@ export interface AzureAppConfigurationOptions { * Specifies options used to configure feature flags. */ featureFlagOptions?: FeatureFlagOptions; -} + + /** + * Specifies options used to configure request tracing. + */ + requestTracingOptions?: RequestTracingOptions; +} diff --git a/src/JsonKeyValueAdapter.ts b/src/JsonKeyValueAdapter.ts index 6b77da5d..d9157a45 100644 --- a/src/JsonKeyValueAdapter.ts +++ b/src/JsonKeyValueAdapter.ts @@ -2,7 +2,7 @@ // Licensed under the MIT license. import { ConfigurationSetting, featureFlagContentType, secretReferenceContentType } from "@azure/app-configuration"; -import { IKeyValueAdapter } from "./IKeyValueAdapter"; +import { IKeyValueAdapter } from "./IKeyValueAdapter.js"; export class JsonKeyValueAdapter implements IKeyValueAdapter { static readonly #ExcludedJsonContentTypes: string[] = [ diff --git a/src/RefreshOptions.ts b/src/RefreshOptions.ts index 29f35be2..37425112 100644 --- a/src/RefreshOptions.ts +++ b/src/RefreshOptions.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { WatchedSetting } from "./WatchedSetting"; +import { WatchedSetting } from "./WatchedSetting.js"; export const DEFAULT_REFRESH_INTERVAL_IN_MS = 30 * 1000; export const MIN_REFRESH_INTERVAL_IN_MS = 1 * 1000; diff --git a/src/featureManagement/FeatureFlagOptions.ts b/src/featureManagement/FeatureFlagOptions.ts index 6facb592..eedf9ec7 100644 --- a/src/featureManagement/FeatureFlagOptions.ts +++ b/src/featureManagement/FeatureFlagOptions.ts @@ -1,8 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { FeatureFlagRefreshOptions } from "../RefreshOptions"; -import { SettingSelector } from "../types"; +import { FeatureFlagRefreshOptions } from "../RefreshOptions.js"; +import { SettingSelector } from "../types.js"; /** * Options used to configure feature flags. @@ -27,4 +27,4 @@ export interface FeatureFlagOptions { * Specifies how feature flag refresh is configured. All selected feature flags will be watched for changes. */ refresh?: FeatureFlagRefreshOptions; -} +} diff --git a/src/index.ts b/src/index.ts index dd246046..1a3bb318 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -export { AzureAppConfiguration } from "./AzureAppConfiguration"; -export { Disposable } from "./common/disposable"; -export { load } from "./load"; -export { KeyFilter, LabelFilter } from "./types"; +export { AzureAppConfiguration } from "./AzureAppConfiguration.js"; +export { Disposable } from "./common/disposable.js"; +export { load } from "./load.js"; +export { KeyFilter, LabelFilter } from "./types.js"; +export { VERSION } from "./version.js"; diff --git a/src/keyvault/AzureKeyVaultKeyValueAdapter.ts b/src/keyvault/AzureKeyVaultKeyValueAdapter.ts index 92f6e261..1b6fdcc4 100644 --- a/src/keyvault/AzureKeyVaultKeyValueAdapter.ts +++ b/src/keyvault/AzureKeyVaultKeyValueAdapter.ts @@ -2,8 +2,8 @@ // Licensed under the MIT license. import { ConfigurationSetting, isSecretReference, parseSecretReference } from "@azure/app-configuration"; -import { IKeyValueAdapter } from "../IKeyValueAdapter"; -import { KeyVaultOptions } from "./KeyVaultOptions"; +import { IKeyValueAdapter } from "../IKeyValueAdapter.js"; +import { KeyVaultOptions } from "./KeyVaultOptions.js"; import { SecretClient, parseKeyVaultSecretIdentifier } from "@azure/keyvault-secrets"; export class AzureKeyVaultKeyValueAdapter implements IKeyValueAdapter { @@ -72,4 +72,4 @@ export class AzureKeyVaultKeyValueAdapter implements IKeyValueAdapter { function getHost(url: string) { return new URL(url).host; -} +} diff --git a/src/load.ts b/src/load.ts index fffd09b8..f5e2075d 100644 --- a/src/load.ts +++ b/src/load.ts @@ -3,10 +3,10 @@ import { AppConfigurationClient, AppConfigurationClientOptions } from "@azure/app-configuration"; import { TokenCredential } from "@azure/identity"; -import { AzureAppConfiguration } from "./AzureAppConfiguration"; -import { AzureAppConfigurationImpl } from "./AzureAppConfigurationImpl"; -import { AzureAppConfigurationOptions, MaxRetries, MaxRetryDelayInMs } from "./AzureAppConfigurationOptions"; -import * as RequestTracing from "./requestTracing/constants"; +import { AzureAppConfiguration } from "./AzureAppConfiguration.js"; +import { AzureAppConfigurationImpl } from "./AzureAppConfigurationImpl.js"; +import { AzureAppConfigurationOptions, MaxRetries, MaxRetryDelayInMs } from "./AzureAppConfigurationOptions.js"; +import * as RequestTracing from "./requestTracing/constants.js"; const MIN_DELAY_FOR_UNHANDLED_ERROR: number = 5000; // 5 seconds @@ -82,6 +82,23 @@ export async function load( } } +/** + * Loads the data from a CDN and returns an instance of AzureAppConfiguration. + * @param cdnEndpoint The URL to the CDN. + * @param appConfigOptions Optional parameters. + */ +export async function loadFromCdn(cdnEndpoint: URL | string, options?: AzureAppConfigurationOptions): Promise; + +export async function loadFromCdn( + cdnEndpoint: string | URL, + appConfigOptions?: AzureAppConfigurationOptions +): Promise { + const emptyTokenCredential: TokenCredential = { + getToken: async () => ({ token: "", expiresOnTimestamp: 0 }) + }; + return await load(cdnEndpoint, emptyTokenCredential, appConfigOptions); +} + function instanceOfTokenCredential(obj: unknown) { return obj && typeof obj === "object" && "getToken" in obj && typeof obj.getToken === "function"; } diff --git a/src/requestTracing/RequestTracingOptions.ts b/src/requestTracing/RequestTracingOptions.ts new file mode 100644 index 00000000..016da0cd --- /dev/null +++ b/src/requestTracing/RequestTracingOptions.ts @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +/** + * Options used to configure request tracing. + */ +export interface RequestTracingOptions { + /** + * Specifies whether request tracing is enabled. + */ + enabled: boolean; +} diff --git a/src/requestTracing/constants.ts b/src/requestTracing/constants.ts index be33aa5d..d46cdfda 100644 --- a/src/requestTracing/constants.ts +++ b/src/requestTracing/constants.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { VERSION } from "../version"; +import { VERSION } from "../version.js"; export const ENV_AZURE_APP_CONFIGURATION_TRACING_DISABLED = "AZURE_APP_CONFIGURATION_TRACING_DISABLED"; diff --git a/src/requestTracing/utils.ts b/src/requestTracing/utils.ts index f979bf47..de335737 100644 --- a/src/requestTracing/utils.ts +++ b/src/requestTracing/utils.ts @@ -2,7 +2,7 @@ // Licensed under the MIT license. import { AppConfigurationClient, ConfigurationSettingId, GetConfigurationSettingOptions, ListConfigurationSettingsOptions } from "@azure/app-configuration"; -import { AzureAppConfigurationOptions } from "../AzureAppConfigurationOptions"; +import { AzureAppConfigurationOptions } from "../AzureAppConfigurationOptions.js"; import { AZURE_FUNCTION_ENV_VAR, AZURE_WEB_APP_ENV_VAR, @@ -111,7 +111,7 @@ export function requestTracingEnabled(): boolean { function getEnvironmentVariable(name: string) { // Make it compatible with non-Node.js runtime - if (typeof process?.env === "object") { + if (typeof process !== "undefined" && typeof process?.env === "object") { return process.env[name]; } else { return undefined; diff --git a/src/version.ts b/src/version.ts index 877c8101..afee11aa 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,4 +1,4 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -export const VERSION = "1.1.0"; +export const VERSION = "1.1.1"; diff --git a/test/clientOptions.test.ts b/test/clientOptions.test.ts index 58bbd353..62e1b21c 100644 --- a/test/clientOptions.test.ts +++ b/test/clientOptions.test.ts @@ -5,8 +5,8 @@ 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 } from "./utils/testHelper"; +import { load } from "./exportedApi.js"; +import { createMockedConnectionString } from "./utils/testHelper.js"; import * as nock from "nock"; class HttpRequestCountPolicy { diff --git a/test/featureFlag.test.ts b/test/featureFlag.test.ts index dfca1c0e..780cd4a4 100644 --- a/test/featureFlag.test.ts +++ b/test/featureFlag.test.ts @@ -3,8 +3,8 @@ import * as chai from "chai"; import * as chaiAsPromised from "chai-as-promised"; -import { load } from "./exportedApi"; -import { createMockedConnectionString, createMockedEndpoint, createMockedFeatureFlag, createMockedKeyValue, mockAppConfigurationClientListConfigurationSettings, restoreMocks } from "./utils/testHelper"; +import { load } from "./exportedApi.js"; +import { createMockedConnectionString, createMockedEndpoint, createMockedFeatureFlag, createMockedKeyValue, mockAppConfigurationClientListConfigurationSettings, restoreMocks } from "./utils/testHelper.js"; chai.use(chaiAsPromised); const expect = chai.expect; diff --git a/test/json.test.ts b/test/json.test.ts index b25c3392..c139d53b 100644 --- a/test/json.test.ts +++ b/test/json.test.ts @@ -5,8 +5,8 @@ import * as chai from "chai"; import * as chaiAsPromised from "chai-as-promised"; chai.use(chaiAsPromised); const expect = chai.expect; -import { load } from "./exportedApi"; -import { mockAppConfigurationClientListConfigurationSettings, restoreMocks, createMockedConnectionString, createMockedKeyVaultReference, createMockedJsonKeyValue } from "./utils/testHelper"; +import { load } from "./exportedApi.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"); diff --git a/test/keyvault.test.ts b/test/keyvault.test.ts index eaed0476..cf01235c 100644 --- a/test/keyvault.test.ts +++ b/test/keyvault.test.ts @@ -5,8 +5,8 @@ import * as chai from "chai"; import * as chaiAsPromised from "chai-as-promised"; chai.use(chaiAsPromised); const expect = chai.expect; -import { load } from "./exportedApi"; -import { sinon, createMockedConnectionString, createMockedTokenCredential, mockAppConfigurationClientListConfigurationSettings, mockSecretClientGetSecret, restoreMocks, createMockedKeyVaultReference } from "./utils/testHelper"; +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 mockedData = [ @@ -111,4 +111,4 @@ describe("key vault reference", function () { expect(settings.get("TestKey")).eq("SecretValue"); expect(settings.get("TestKey2")).eq("SecretValue2"); }); -}); +}); diff --git a/test/load.test.ts b/test/load.test.ts index 2bd0e58d..ce22b1a8 100644 --- a/test/load.test.ts +++ b/test/load.test.ts @@ -5,8 +5,8 @@ import * as chai from "chai"; import * as chaiAsPromised from "chai-as-promised"; chai.use(chaiAsPromised); const expect = chai.expect; -import { load } from "./exportedApi"; -import { mockAppConfigurationClientListConfigurationSettings, restoreMocks, createMockedConnectionString, createMockedEndpoint, createMockedTokenCredential, createMockedKeyValue } from "./utils/testHelper"; +import { load } from "./exportedApi.js"; +import { mockAppConfigurationClientListConfigurationSettings, restoreMocks, createMockedConnectionString, createMockedEndpoint, createMockedTokenCredential, createMockedKeyValue } from "./utils/testHelper.js"; const mockedKVs = [{ key: "app.settings.fontColor", diff --git a/test/refresh.test.ts b/test/refresh.test.ts index 2af8842e..fcffaee5 100644 --- a/test/refresh.test.ts +++ b/test/refresh.test.ts @@ -5,8 +5,8 @@ import * as chai from "chai"; import * as chaiAsPromised from "chai-as-promised"; chai.use(chaiAsPromised); const expect = chai.expect; -import { load } from "./exportedApi"; -import { mockAppConfigurationClientListConfigurationSettings, mockAppConfigurationClientGetConfigurationSetting, restoreMocks, createMockedConnectionString, createMockedKeyValue, sleepInMs, createMockedFeatureFlag } from "./utils/testHelper"; +import { load } from "./exportedApi.js"; +import { mockAppConfigurationClientListConfigurationSettings, mockAppConfigurationClientGetConfigurationSetting, restoreMocks, createMockedConnectionString, createMockedKeyValue, sleepInMs, createMockedFeatureFlag } from "./utils/testHelper.js"; import * as uuid from "uuid"; let mockedKVs: any[] = []; @@ -423,4 +423,4 @@ describe("dynamic refresh feature flags", function () { await settings.refresh(); expect(refreshSuccessfulCount).eq(1); // change in feature flags, because page etags are different. }); -}); +}); diff --git a/test/requestTracing.test.ts b/test/requestTracing.test.ts index 1496b097..a08ffa8b 100644 --- a/test/requestTracing.test.ts +++ b/test/requestTracing.test.ts @@ -5,8 +5,8 @@ import * as chai from "chai"; import * as chaiAsPromised from "chai-as-promised"; chai.use(chaiAsPromised); const expect = chai.expect; -import { createMockedConnectionString, createMockedKeyValue, createMockedTokenCredential, mockAppConfigurationClientListConfigurationSettings, restoreMocks, sleepInMs } from "./utils/testHelper"; -import { load } from "./exportedApi"; +import { createMockedConnectionString, createMockedKeyValue, createMockedTokenCredential, mockAppConfigurationClientListConfigurationSettings, restoreMocks, sleepInMs } from "./utils/testHelper.js"; +import { load } from "./exportedApi.js"; class HttpRequestHeadersPolicy { headers: any; @@ -122,6 +122,20 @@ describe("request tracing", function () { delete process.env.AZURE_APP_CONFIGURATION_TRACING_DISABLED; }); + it("should disable request tracing by RequestTracingOptions", async () => { + try { + await load(createMockedConnectionString(fakeEndpoint), { + clientOptions, + requestTracingOptions: { + enabled: false + } + }); + } catch (e) { /* empty */ } + expect(headerPolicy.headers).not.undefined; + const correlationContext = headerPolicy.headers.get("Correlation-Context"); + expect(correlationContext).undefined; + }); + it("should have request type in correlation-context header when refresh is enabled", async () => { mockAppConfigurationClientListConfigurationSettings([{ key: "app.settings.fontColor", From 75036884aa0cd5878d97a08e4f6a026f5eb82458 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Thu, 31 Oct 2024 14:23:51 +0800 Subject: [PATCH 13/35] export loadFromCdn (#118) --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 1a3bb318..59bf25c3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,6 @@ export { AzureAppConfiguration } from "./AzureAppConfiguration.js"; export { Disposable } from "./common/disposable.js"; -export { load } from "./load.js"; +export { load, loadFromCdn } from "./load.js"; export { KeyFilter, LabelFilter } from "./types.js"; export { VERSION } from "./version.js"; From b3b6b079988a85468884f6a7741d6645006a3be8 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Fri, 1 Nov 2024 13:06:10 +0800 Subject: [PATCH 14/35] update --- package-lock.json | 2 +- src/AzureAppConfigurationImpl.ts | 60 +++++++++++++++++--------------- 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/package-lock.json b/package-lock.json index b587455c..1dbeb83f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "@azure/app-configuration-provider", - "version": "1.1.0", + "version": "1.1.1", "license": "MIT", "dependencies": { "@azure/app-configuration": "^1.6.1", diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index 8000269e..2257fdb4 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -567,7 +567,10 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { if (featureFlag[TELEMETRY_KEY_NAME] && featureFlag[TELEMETRY_KEY_NAME][ENABLED_KEY_NAME] === true) { const metadata = featureFlag[TELEMETRY_KEY_NAME][METADATA_KEY_NAME]; - const allocationId = await this.#generateAllocationId(featureFlag); + let allocationId = ""; + if (featureFlag[ALLOCATION_KEY_NAME] !== undefined) { + allocationId = await this.#generateAllocationId(featureFlag); + } featureFlag[TELEMETRY_KEY_NAME][METADATA_KEY_NAME] = { [ETAG_KEY_NAME]: setting.etag, [FEATURE_FLAG_ID_KEY_NAME]: await this.#calculateFeatureFlagId(setting), @@ -642,51 +645,50 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { let rawAllocationId = ""; // Only default variant when enabled and variants allocated by percentile involve in the experimentation // The allocation id is genearted from default variant when enabled and percentile allocation - const variantsForExperiementation: string[] = []; + const variantsForExperimentation: string[] = []; - if (featureFlag[ALLOCATION_KEY_NAME]) { - rawAllocationId += `seed=${featureFlag[ALLOCATION_KEY_NAME][SEED_KEY_NAME] ?? ""}\ndefault_when_enabled=`; + rawAllocationId += `seed=${featureFlag[ALLOCATION_KEY_NAME][SEED_KEY_NAME] ?? ""}\ndefault_when_enabled=`; - if (featureFlag[ALLOCATION_KEY_NAME][DEFAULT_WHEN_ENABLED_KEY_NAME]) { - variantsForExperiementation.push(featureFlag[ALLOCATION_KEY_NAME][DEFAULT_WHEN_ENABLED_KEY_NAME]); - rawAllocationId += `${featureFlag[ALLOCATION_KEY_NAME][DEFAULT_WHEN_ENABLED_KEY_NAME]}`; - } + if (featureFlag[ALLOCATION_KEY_NAME][DEFAULT_WHEN_ENABLED_KEY_NAME]) { + variantsForExperimentation.push(featureFlag[ALLOCATION_KEY_NAME][DEFAULT_WHEN_ENABLED_KEY_NAME]); + rawAllocationId += `${featureFlag[ALLOCATION_KEY_NAME][DEFAULT_WHEN_ENABLED_KEY_NAME]}`; + } - rawAllocationId += "\npercentiles="; - - const percentileList = featureFlag[ALLOCATION_KEY_NAME][PERCENTILE_KEY_NAME]; - if (percentileList) { - const sortedPercentileList = percentileList - .filter(p => - (p[FROM_KEY_NAME] !== undefined) && - (p[TO_KEY_NAME] !== undefined) && - (p[VARIANT_KEY_NAME] !== undefined) && - (p[FROM_KEY_NAME] !== p[TO_KEY_NAME])) - .sort((a, b) => a[FROM_KEY_NAME] - b[FROM_KEY_NAME]); - - const percentileAllocation: string[] = []; - for (const percentile of sortedPercentileList) { - variantsForExperiementation.push(percentile[VARIANT_KEY_NAME]); - percentileAllocation.push(`${percentile[FROM_KEY_NAME]},${base64Helper(percentile[VARIANT_KEY_NAME])},${percentile[TO_KEY_NAME]}`); - } - rawAllocationId += percentileAllocation.join(";"); + rawAllocationId += "\npercentiles="; + + const percentileList = featureFlag[ALLOCATION_KEY_NAME][PERCENTILE_KEY_NAME]; + if (percentileList) { + const sortedPercentileList = percentileList + .filter(p => + (p[FROM_KEY_NAME] !== undefined) && + (p[TO_KEY_NAME] !== undefined) && + (p[VARIANT_KEY_NAME] !== undefined) && + (p[FROM_KEY_NAME] !== p[TO_KEY_NAME])) + .sort((a, b) => a[FROM_KEY_NAME] - b[FROM_KEY_NAME]); + + const percentileAllocation: string[] = []; + for (const percentile of sortedPercentileList) { + variantsForExperimentation.push(percentile[VARIANT_KEY_NAME]); + percentileAllocation.push(`${percentile[FROM_KEY_NAME]},${base64Helper(percentile[VARIANT_KEY_NAME])},${percentile[TO_KEY_NAME]}`); } + rawAllocationId += percentileAllocation.join(";"); } + - if (featureFlag[ALLOCATION_KEY_NAME] === undefined || (variantsForExperiementation.length === 0 && featureFlag[ALLOCATION_KEY_NAME][SEED_KEY_NAME] === undefined)) { + if (variantsForExperimentation.length === 0 && featureFlag[ALLOCATION_KEY_NAME][SEED_KEY_NAME] === undefined) { // All fields required for generating allocation id are missing, short-circuit and return empty string return ""; } rawAllocationId += "\nvariants="; - if (variantsForExperiementation.length !== 0) { + if (variantsForExperimentation.length !== 0) { const variantsList = featureFlag[VARIANTS_KEY_NAME]; if (variantsList) { const sortedVariantsList = variantsList .filter(v => (v[NAME_KEY_NAME] !== undefined) && - variantsForExperiementation.includes(v[NAME_KEY_NAME])) + variantsForExperimentation.includes(v[NAME_KEY_NAME])) .sort((a, b) => (a.name > b.name ? 1 : -1)); const variantConfiguration: string[] = []; From 16bf0c02cc9b8b917e7c4cd13a3498dd37ca58f6 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Fri, 1 Nov 2024 13:26:17 +0800 Subject: [PATCH 15/35] fix lint --- src/AzureAppConfigurationImpl.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index 2257fdb4..7be1d223 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -673,7 +673,6 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { } rawAllocationId += percentileAllocation.join(";"); } - if (variantsForExperimentation.length === 0 && featureFlag[ALLOCATION_KEY_NAME][SEED_KEY_NAME] === undefined) { // All fields required for generating allocation id are missing, short-circuit and return empty string From 68fba74d772d133b9783f28b660f559f5b0e6a01 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:37:57 +0800 Subject: [PATCH 16/35] remove requestracingoptions (#127) --- src/requestTracing/RequestTracingOptions.ts | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 src/requestTracing/RequestTracingOptions.ts diff --git a/src/requestTracing/RequestTracingOptions.ts b/src/requestTracing/RequestTracingOptions.ts deleted file mode 100644 index 016da0cd..00000000 --- a/src/requestTracing/RequestTracingOptions.ts +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -/** - * Options used to configure request tracing. - */ -export interface RequestTracingOptions { - /** - * Specifies whether request tracing is enabled. - */ - enabled: boolean; -} From d573c31d64fc63e94c51dfead7d0767cd1c85dca Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Tue, 5 Nov 2024 17:15:20 +0800 Subject: [PATCH 17/35] Revert "New API to load from CDN endpoint (#106)" This reverts commit 6dae81a14afb11bd84b2c718753bb9cbe7375c52. --- src/load.ts | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/load.ts b/src/load.ts index f5e2075d..ce3d39c2 100644 --- a/src/load.ts +++ b/src/load.ts @@ -82,23 +82,6 @@ export async function load( } } -/** - * Loads the data from a CDN and returns an instance of AzureAppConfiguration. - * @param cdnEndpoint The URL to the CDN. - * @param appConfigOptions Optional parameters. - */ -export async function loadFromCdn(cdnEndpoint: URL | string, options?: AzureAppConfigurationOptions): Promise; - -export async function loadFromCdn( - cdnEndpoint: string | URL, - appConfigOptions?: AzureAppConfigurationOptions -): Promise { - const emptyTokenCredential: TokenCredential = { - getToken: async () => ({ token: "", expiresOnTimestamp: 0 }) - }; - return await load(cdnEndpoint, emptyTokenCredential, appConfigOptions); -} - function instanceOfTokenCredential(obj: unknown) { return obj && typeof obj === "object" && "getToken" in obj && typeof obj.getToken === "function"; } From ae78584c6feba63f8c18dc3c5211379c9962e34e Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Tue, 5 Nov 2024 17:16:08 +0800 Subject: [PATCH 18/35] Revert "export loadFromCdn (#118)" This reverts commit 75036884aa0cd5878d97a08e4f6a026f5eb82458. --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 59bf25c3..1a3bb318 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,6 @@ export { AzureAppConfiguration } from "./AzureAppConfiguration.js"; export { Disposable } from "./common/disposable.js"; -export { load, loadFromCdn } from "./load.js"; +export { load } from "./load.js"; export { KeyFilter, LabelFilter } from "./types.js"; export { VERSION } from "./version.js"; From 214a1d9ee0c301e8efede1d10e588da8f68248f4 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Wed, 6 Nov 2024 00:56:33 +0800 Subject: [PATCH 19/35] revert change --- server.cert | 18 ------------------ server.key | 27 --------------------------- 2 files changed, 45 deletions(-) delete mode 100644 server.cert delete mode 100644 server.key diff --git a/server.cert b/server.cert deleted file mode 100644 index befe90d0..00000000 --- a/server.cert +++ /dev/null @@ -1,18 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIC8DCCAdigAwIBAgIJWz41hwnlFODkMA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNV -BAMTCWxvY2FsaG9zdDAeFw0yNDExMDUxNjI3NDVaFw0yNTExMDUxNjI3NDVaMBQx -EjAQBgNVBAMTCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC -ggEBAOBEIu1gsfcnkDKkHFbdkmqodiXltftn8rTmq4qWjh1xnB7d0ZZpyvCNGo+K -QuZ0L/yTbDOL+ijM1KjXHQmXP75fPn0VrCKy4jgc5dz1NgU+XzFQe1WyqYlsUAm9 -YaDiscYIVpC03xkXJdWkAHXsN/P46pLxGX1fMHVQsAi2PNTtuO7lBj6T/NHL5QbI -610I76drBY6KL2B5Djmi6YsPZ6pmppylGr8KlHsaKzWGytHkKIPLPhg6hDvvFSko -SsXhBjF2rZcauG7y91azfkkGTsB1PO4QZD62MNgeF+7Ly7KkYOVzOop14vOjZxFz -mhETRNVpUsLqYwh7VzKGbheflKUCAwEAAaNFMEMwDAYDVR0TBAUwAwEB/zALBgNV -HQ8EBAMCAvQwJgYDVR0RBB8wHYYbaHR0cDovL2V4YW1wbGUub3JnL3dlYmlkI21l -MA0GCSqGSIb3DQEBBQUAA4IBAQAv5NwaNUAjKlg9u7WNARDD7eio+NpHEOFX0ayf -3WyJR3AgMjP8M95cgjq+BZF/vpbCxDJHZjad0ZkPQYe848hfBu9d/jcuXOB1Z3Zl -/8QOgaHk8+NqeUyAVUgaLFaChMLAeUE4e+hsfDUiXmEmpFMDAEM3fxPZ73yxK/Qs -okXOxFDiImjO3NZjcyLvV+5T0UvH+PAPx1e8j1c3n8J+FpQivsK16GJU0F+SLn6t -s+FPpb4rD3pG5soPHtM1G9Jr9V9ArgSvDHdEmnwZAwgBUu6pZC3b7S1HNjJYyTay -gTn7XOciel8YdDGPOh0vNABrSOHuJwkeQnbFIqIflbiZjbnI ------END CERTIFICATE----- diff --git a/server.key b/server.key deleted file mode 100644 index bfdba5f5..00000000 --- a/server.key +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEoAIBAAKCAQEA4EQi7WCx9yeQMqQcVt2Saqh2JeW1+2fytOaripaOHXGcHt3R -lmnK8I0aj4pC5nQv/JNsM4v6KMzUqNcdCZc/vl8+fRWsIrLiOBzl3PU2BT5fMVB7 -VbKpiWxQCb1hoOKxxghWkLTfGRcl1aQAdew38/jqkvEZfV8wdVCwCLY81O247uUG -PpP80cvlBsjrXQjvp2sFjoovYHkOOaLpiw9nqmamnKUavwqUexorNYbK0eQog8s+ -GDqEO+8VKShKxeEGMXatlxq4bvL3VrN+SQZOwHU87hBkPrYw2B4X7svLsqRg5XM6 -inXi86NnEXOaERNE1WlSwupjCHtXMoZuF5+UpQIDAQABAoIBABJ4pN8j+N8lno5V -OaemmJcj/esxfX4nTx8QOq5vp7qMGh2UC92qzqtma9Aeb/6Bhlb0INQpYiF3Jsgr -Q48qf8u4a4dWQCHG1bQPyUJG3hFWKaAAWXrdpsdp/jj3JLHhdLPzXtyMpoLEsc85 -XU0DzONRgVHjUsP35vVaqne6js3DXookFjfIerA5tK0Olai4cikh19ux5bboGSsl -rxnhbKkpASMrgNO7vLPYGj5ofAqH7AVgiPEvmch6pMRXMyn6EW/FatUtzl2tkVHu -qAt1SHNQtzvv/M3owmGcWzqRYrd8yk1YtU6XObshoNB1IUgqGswPvoCa+r21euH7 -8a8cd0ECgYEA83EovMsITETiXzORY/Z6p1wla/VDALBA5GNnUEbUo7ZXlNIqjCbb -ym32ffDW5vNgzjQM8mhZ3tTymimy+KWYuS+GU4n6Y9eoMCtuuZf/2HS5+OuKuNa6 -qebAD6QHuqjkf9cQ9eT2bbUT+6YYfu4peT7Ukms20cGibgLUB397Z6ECgYEA69W+ -vG9c34pTFzzk9Qc1Npmq5z/0POLVD/Q+s4WbDhGnLPlMOru50KPGZIwZt3oxLxuO -SPMUKRwQWS7/OIoAMWm7DCQQ8kLMEgogZrfvF9Vg/LJqkdcflMYYYONi+qKWuxnE -BcbGHbOzylTWwE0Ab3BMTe72BOrMRHPJJkoR/oUCgYBT56Ce0Wueve832NI3nfBQ -VpYfS304c4cqMIifRoprkqFFDdrUS2BduODIL4YiO6gV6iK8OfMftk2XjN++i3TD -/vlRObEvUFkLoSyckyL/O9wNS0GYy4mynXF88pAnaaa32SxjU/MLhOnKRkkTFor8 -2wbxIDrYuOB/AkKKAwpJgQKBgFZwiLGei8bvd1ccHbtlwe5aUpbiDQ5IAdoQJu87 -+EIm3ESSt4sLhabgcHYoF8D1S5O9oOBCIFCVFbTqkedSRZHW9BUWHxP8wbmW9ptP -rZXpwtu8NW1xEQVBumvAzGWbNG4tmniXi8QVOr/dar0fPBqcaYtjG89flDE+0ypy -mbfVAn9UszXDlfJmWEu3AEhHPTiBRLOK01Zl6pJgABeRZOh4q1TCelA7Gksi/5Yz -/pHxUETVwD98Lz61wbgtbwteGhUm4umFRc+mK2gliFs/GnuAuPUCtHmVp3lLP9uf -r4kulWFhEWbDj7UzkUWwq7B6oGh1TQZPgqGWfX06IxFiXqkx ------END RSA PRIVATE KEY----- From a251f82454e31f060beab1486bfd9b872770ea8c Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Fri, 8 Nov 2024 12:51:24 +0800 Subject: [PATCH 20/35] version bump 2.0.0-preview.1 (#132) --- package-lock.json | 2 +- package.json | 2 +- src/version.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index ac1204cd..498f59e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@azure/app-configuration-provider", - "version": "1.1.2", + "version": "2.0.0-preview.1", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/package.json b/package.json index 0b493ea3..0842f457 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@azure/app-configuration-provider", - "version": "1.1.2", + "version": "2.0.0-preview.1", "description": "The JavaScript configuration provider for Azure App Configuration", "main": "dist/index.js", "module": "./dist-esm/index.js", diff --git a/src/version.ts b/src/version.ts index 1c39d36f..beea578c 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,4 +1,4 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -export const VERSION = "1.1.2"; +export const VERSION = "2.0.0-preview.1"; From 7db665fde28d218211ae909684bf158852af6f7a Mon Sep 17 00:00:00 2001 From: linglingye001 <143174321+linglingye001@users.noreply.github.com> Date: Mon, 18 Nov 2024 16:07:14 +0800 Subject: [PATCH 21/35] Failover support (#98) * resolve conflicts * resolve conflicts * resolve conflicts * add tests * resolve conflicts and update * fix lint * resolve conflicts * resolve comments * update package-lock * update * update * update failover error * update * update * update failoverable error with 'ENOTFOUND' * fix lint * update * added ENOENT error * update * update error message in test * update test * update test * update * resolve conflicts --- package-lock.json | 3 + src/AzureAppConfigurationImpl.ts | 216 ++++++++++++--------- src/AzureAppConfigurationOptions.ts | 10 +- src/ConfigurationClientManager.ts | 288 ++++++++++++++++++++++++++++ src/ConfigurationClientWrapper.ts | 49 +++++ src/common/utils.ts | 8 + src/load.ts | 82 +------- src/refresh/RefreshTimer.ts | 59 ------ src/requestTracing/constants.ts | 1 + src/requestTracing/utils.ts | 24 ++- test/failover.test.ts | 112 +++++++++++ test/requestTracing.test.ts | 16 +- test/utils/testHelper.ts | 117 +++++++---- 13 files changed, 700 insertions(+), 285 deletions(-) create mode 100644 src/ConfigurationClientManager.ts create mode 100644 src/ConfigurationClientWrapper.ts create mode 100644 test/failover.test.ts diff --git a/package-lock.json b/package-lock.json index 498f59e1..7cc0a774 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" }, diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index 977872a8..08f3bb3e 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,10 +57,10 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { */ #sortedTrimKeyPrefixes: string[] | undefined; readonly #requestTracingEnabled: boolean; - #client: AppConfigurationClient; - #clientEndpoint: string | undefined; + #clientManager: ConfigurationClientManager; #options: AzureAppConfigurationOptions | undefined; #isInitialLoadCompleted: boolean = false; + #isFailoverRequest: boolean = false; // Refresh #refreshInterval: number = DEFAULT_REFRESH_INTERVAL_IN_MS; @@ -78,13 +79,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(); @@ -197,35 +196,66 @@ 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: (client: AppConfigurationClient) => Promise): Promise { + const clientWrappers = await this.#clientManager.getClients(); - // validate selectors - const selectors = getValidKeyValueSelectors(this.#options?.selectors); + let successful: boolean; + for (const clientWrapper of clientWrappers) { + successful = false; + try { + const result = await funcToExecute(clientWrapper.client); + this.#isFailoverRequest = false; + successful = true; + clientWrapper.updateBackoffStatus(successful); + return result; + } catch (error) { + if (isFailoverableError(error)) { + clientWrapper.updateBackoffStatus(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 clients failed to get configuration 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); + + 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; + return loadedSettings; + }; + + return await this.#executeWithFailoverPolicy(funcToExecute) as ConfigurationSetting[]; } /** @@ -279,29 +309,42 @@ 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[] = []; + // deep copy selectors to avoid modification if current client fails + 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; + }; + + const featureFlagSettings = await this.#executeWithFailoverPolicy(funcToExecute) as ConfigurationSetting[]; // parse feature flags const featureFlags = await Promise.all( @@ -389,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); } } @@ -430,13 +473,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(); @@ -454,39 +491,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) => { + 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; + } } } + return false; + }; - if (needRefresh) { - break; // short-circuit if result from any of the selectors is changed - } - } - + 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(); @@ -540,14 +570,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; @@ -634,7 +668,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { } #createFeatureFlagReference(setting: ConfigurationSetting): string { - let featureFlagReference = `${this.#clientEndpoint}kv/${setting.key}`; + let featureFlagReference = `${this.#clientManager.endpoint.origin}/kv/${setting.key}`; if (setting.label && setting.label.trim().length !== 0) { featureFlagReference += `?label=${setting.label}`; } @@ -794,3 +828,9 @@ function getValidFeatureFlagSelectors(selectors?: SettingSelector[]): SettingSel return getValidSelectors(selectors); } } + +function isFailoverableError(error: any): boolean { + // 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))); +} diff --git a/src/AzureAppConfigurationOptions.ts b/src/AzureAppConfigurationOptions.ts index f88ad67c..4aa3f99d 100644 --- a/src/AzureAppConfigurationOptions.ts +++ b/src/AzureAppConfigurationOptions.ts @@ -12,7 +12,7 @@ export const MaxRetryDelayInMs = 60000; export interface AzureAppConfigurationOptions { /** - * 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. @@ -47,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 new file mode 100644 index 00000000..59e03aa5 --- /dev/null +++ b/src/ConfigurationClientManager.ts @@ -0,0 +1,288 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { AppConfigurationClient, AppConfigurationClientOptions } from "@azure/app-configuration"; +import { ConfigurationClientWrapper } from "./ConfigurationClientWrapper.js"; +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"; +const TCP_KEY_NAME = "_tcp"; +const ENDPOINT_KEY_NAME = "Endpoint"; +const ID_KEY_NAME = "Id"; +const SECRET_KEY_NAME = "Secret"; +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 = 30 * 1000; // 30 seconds in milliseconds + +export class ConfigurationClientManager { + #isFailoverable: boolean; + #dns: any; + endpoint: URL; + #secret : string; + #id : string; + #credential: TokenCredential; + #clientOptions: AppConfigurationClientOptions | undefined; + #appConfigOptions: AzureAppConfigurationOptions | undefined; + #validDomain: string; + #staticClients: ConfigurationClientWrapper[]; + #dynamicClients: ConfigurationClientWrapper[]; + #lastFallbackClientRefreshTime: number = 0; + #lastFallbackClientRefreshAttempt: number = 0; + + constructor ( + connectionStringOrEndpoint?: string | URL, + credentialOrOptions?: TokenCredential | AzureAppConfigurationOptions, + appConfigOptions?: AzureAppConfigurationOptions + ) { + let staticClient: AppConfigurationClient; + const credentialPassed = instanceOfTokenCredential(credentialOrOptions); + + if (typeof connectionStringOrEndpoint === "string" && !credentialPassed) { + const connectionString = connectionStringOrEndpoint; + this.#appConfigOptions = credentialOrOptions as AzureAppConfigurationOptions; + this.#clientOptions = getClientOptions(this.#appConfigOptions); + const ConnectionStringRegex = /Endpoint=(.*);Id=(.*);Secret=(.*)/; + const regexMatch = connectionString.match(ConnectionStringRegex); + if (regexMatch) { + 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}'.`); + } + 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") { + endpoint = getValidUrl(endpoint); + } + + const credential = credentialOrOptions as TokenCredential; + this.#appConfigOptions = appConfigOptions as AzureAppConfigurationOptions; + this.#clientOptions = getClientOptions(this.#appConfigOptions); + 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.origin, staticClient)]; + this.#validDomain = getValidDomain(this.endpoint.hostname.toLowerCase()); + } + + 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 { + if (!this.#isFailoverable) { + return this.#staticClients; + } + + const currentTime = Date.now(); + // 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; + await this.#discoverFallbackClients(this.endpoint.hostname); + return availableClients.concat(this.#dynamicClients); + } + + // 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 + MINIMAL_CLIENT_REFRESH_INTERVAL).getTime()) { + this.#lastFallbackClientRefreshAttempt = currentTime; + await this.#discoverFallbackClients(this.endpoint.hostname); + } + } + + async #discoverFallbackClients(host: string) { + let result; + try { + 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(`Failed to build fallback clients, ${error.message}`); + } + + const srvTargetHosts = shuffleList(result) as string[]; + const newDynamicClients: ConfigurationClientWrapper[] = []; + for (const host of srvTargetHosts) { + if (isValidEndpoint(host, this.#validDomain)) { + const targetEndpoint = `https://${host}`; + 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); + newDynamicClients.push(new ConfigurationClientWrapper(targetEndpoint, client)); + } + } + + this.#dynamicClients = newDynamicClients; + this.#lastFallbackClientRefreshTime = Date.now(); + } + + /** + * Query SRV records and return target hosts. + */ + async #querySrvTargetHost(host: string): Promise { + const results: string[] = []; + + 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); + + // 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) { + 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") { + return results; // No more SRV records found, return results + } else { + throw new Error(`Failed to lookup SRV records: ${err.message}`); + } + } + + return results; + } +} + +/** + * Builds a connection string from the given endpoint, secret, and id. + * Returns an empty string if either secret or id is empty. + */ +function buildConnectionString(endpoint, secret, id: string): string { + if (!secret || !id) { + return ""; + } + + return `${ENDPOINT_KEY_NAME}=${endpoint};${ID_KEY_NAME}=${id};${SECRET_KEY_NAME}=${secret}`; +} + +/** + * Extracts a valid domain from the given endpoint URL based on trusted domain labels. + */ +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); + } + } + + return ""; +} + +/** + * Checks if the given host ends with the valid domain. + */ +export function isValidEndpoint(host: string, validDomain: string): boolean { + if (!validDomain) { + return false; + } + + return host.toLowerCase().endsWith(validDomain.toLowerCase()); +} + +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 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 new file mode 100644 index 00000000..7dd6f418 --- /dev/null +++ b/src/ConfigurationClientWrapper.ts @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +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 = 0; // Timestamp + #failedAttempts: number = 0; + + constructor(endpoint: string, client: AppConfigurationClient) { + this.endpoint = endpoint; + this.client = client; + } + + updateBackoffStatus(successfull: boolean) { + if (successfull) { + this.#failedAttempts = 0; + this.backoffEndTime = Date.now(); + } else { + this.#failedAttempts += 1; + this.backoffEndTime = Date.now() + calculateBackoffDuration(this.#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); +} 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; +} diff --git a/src/load.ts b/src/load.ts index ce3d39c2..4d24174e 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, instanceOfTokenCredential } from "./ConfigurationClientManager.js"; const MIN_DELAY_FOR_UNHANDLED_ERROR: number = 5000; // 5 seconds @@ -31,43 +30,18 @@ export async function load( appConfigOptions?: AzureAppConfigurationOptions ): Promise { const startTimestamp = Date.now(); - let client: AppConfigurationClient; - let clientEndpoint: string | undefined; let options: AzureAppConfigurationOptions | undefined; + const clientManager = new ConfigurationClientManager(connectionStringOrEndpoint, credentialOrOptions, appConfigOptions); + await clientManager.init(); - // input validation - if (typeof connectionStringOrEndpoint === "string" && !instanceOfTokenCredential(credentialOrOptions)) { - const connectionString = connectionStringOrEndpoint; + if (!instanceOfTokenCredential(credentialOrOptions)) { options = credentialOrOptions as AzureAppConfigurationOptions; - const clientOptions = getClientOptions(options); - client = new AppConfigurationClient(connectionString, clientOptions); - clientEndpoint = getEndpoint(connectionStringOrEndpoint); - } else if ((connectionStringOrEndpoint instanceof URL || typeof connectionStringOrEndpoint === "string") && instanceOfTokenCredential(credentialOrOptions)) { - // ensure string is a valid URL. - if (typeof connectionStringOrEndpoint === "string") { - try { - const endpointUrl = new URL(connectionStringOrEndpoint); - clientEndpoint = endpointUrl.toString(); - } catch (error) { - if (error.code === "ERR_INVALID_URL") { - throw new Error("Invalid endpoint URL.", { cause: error }); - } else { - throw error; - } - } - } else { - clientEndpoint = connectionStringOrEndpoint.toString(); - } - const credential = credentialOrOptions as TokenCredential; - options = appConfigOptions; - const clientOptions = getClientOptions(options); - client = new AppConfigurationClient(clientEndpoint, credential, clientOptions); } else { - throw new Error("A connection string or an endpoint with credential must be specified to create a client."); + options = appConfigOptions; } try { - const appConfiguration = new AzureAppConfigurationImpl(client, clientEndpoint, options); + const appConfiguration = new AzureAppConfigurationImpl(clientManager, options); await appConfiguration.load(); return appConfiguration; } catch (error) { @@ -81,45 +55,3 @@ export async function load( throw error; } } - -function instanceOfTokenCredential(obj: unknown) { - return obj && typeof obj === "object" && "getToken" in obj && typeof obj.getToken === "function"; -} - -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/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 d46cdfda..60dbb81a 100644 --- a/src/requestTracing/constants.ts +++ b/src/requestTracing/constants.ts @@ -45,4 +45,5 @@ export enum RequestType { } // Tag names +export const FAILOVER_REQUEST_TAG = "Failover"; export const KEY_VAULT_CONFIGURED_TAG = "UsesKeyVault"; diff --git a/src/requestTracing/utils.ts b/src/requestTracing/utils.ts index de335737..8a2fdbc4 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_TAG } 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_TAG); + } + return contextParts.join(","); } @@ -146,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 @@ -155,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 @@ -165,3 +172,4 @@ function isWebWorker() { return workerGlobalScopeDefined && importScriptsAsGlobalFunction && isNavigatorDefinedAsExpected; } + diff --git a/test/failover.test.ts b/test/failover.test.ts new file mode 100644 index 00000000..c97a127a --- /dev/null +++ b/test/failover.test.ts @@ -0,0 +1,112 @@ +// 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, createMockedFeatureFlag, createMockedKeyValue, 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 isFailoverable = true; + mockConfigurationManagerGetClients(isFailoverable, mockedKVs); + + const connectionString = createMockedConnectionString(); + // 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 isFailoverable = true; + mockConfigurationManagerGetClients(isFailoverable, mockedFeatureFlags); + + const connectionString = createMockedConnectionString(); + // replicaDiscoveryEnabled is default to true + const settings = await load(connectionString, { + 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); + + const connectionString = createMockedConnectionString(); + return expect(load(connectionString)).eventually.rejectedWith("All clients failed to get configuration settings."); + }); + + it("should validate endpoint", () => { + 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; + 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 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; + 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 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 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; + expect(isValidEndpoint("foobar.appconfig.azure.com", validDomain4)).to.be.false; + }); +}); diff --git a/test/requestTracing.test.ts b/test/requestTracing.test.ts index d4e7edcf..62d0c5b5 100644 --- a/test/requestTracing.test.ts +++ b/test/requestTracing.test.ts @@ -54,9 +54,7 @@ describe("request tracing", function () { it("should have request type in correlation-context header", async () => { try { - await load(createMockedConnectionString(fakeEndpoint), { - clientOptions - }); + await load(createMockedConnectionString(fakeEndpoint), { clientOptions }); } catch (e) { /* empty */ } expect(headerPolicy.headers).not.undefined; expect(headerPolicy.headers.get("Correlation-Context")).eq("RequestType=Startup"); @@ -80,9 +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), { - clientOptions - }); + await load(createMockedConnectionString(fakeEndpoint), { clientOptions }); } catch (e) { /* empty */ } expect(headerPolicy.headers).not.undefined; const correlationContext = headerPolicy.headers.get("Correlation-Context"); @@ -94,9 +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), { - clientOptions - }); + await load(createMockedConnectionString(fakeEndpoint), { clientOptions }); } catch (e) { /* empty */ } expect(headerPolicy.headers).not.undefined; const correlationContext = headerPolicy.headers.get("Correlation-Context"); @@ -109,9 +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), { - clientOptions - }); + await load(createMockedConnectionString(fakeEndpoint), { 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..261b9b57 100644 --- a/test/utils/testHelper.ts +++ b/test/utils/testHelper.ts @@ -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"; @@ -38,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. @@ -49,48 +95,34 @@ 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 } - } - }); - } - } - }; - } - }; + const kvs = _filterKVs(pages.flat(), listOptions); + return getMockedIterator(pages, kvs, listOptions); + }); +} - return mockIterator as any; +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))); + sinon.stub(fakeStaticClientWrapper.client, "listConfigurationSettings").callsFake(() => { + throw new RestError("Internal Server Error", { statusCode: 500 }); + }); + clients.push(fakeStaticClientWrapper); + + if (!isFailoverable) { + return clients; + } + + const fakeReplicaEndpoint = createMockedEndpoint("fake-replica"); + 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; }); } @@ -198,6 +230,7 @@ export { sinon, mockAppConfigurationClientListConfigurationSettings, mockAppConfigurationClientGetConfigurationSetting, + mockConfigurationManagerGetClients, mockSecretClientGetSecret, restoreMocks, @@ -210,4 +243,4 @@ export { createMockedFeatureFlag, sleepInMs -}; +}; From 477f18de35107c6ec7ac6d6b9f4df0a32925082d Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Tue, 19 Nov 2024 01:36:23 +0800 Subject: [PATCH 22/35] add dns module to rollup whitelist (#134) --- rollup.config.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rollup.config.mjs b/rollup.config.mjs index 1cd15dfc..8ad51640 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -4,7 +4,7 @@ import dts from "rollup-plugin-dts"; export default [ { - external: ["@azure/app-configuration", "@azure/keyvault-secrets", "@azure/core-rest-pipeline", "crypto"], + external: ["@azure/app-configuration", "@azure/keyvault-secrets", "@azure/core-rest-pipeline", "crypto", "dns/promises"], input: "src/index.ts", output: [ { From 2315e4c341b6a6629fc10f6286f38a2cf19c1fbb Mon Sep 17 00:00:00 2001 From: linglingye001 <143174321+linglingye001@users.noreply.github.com> Date: Wed, 27 Nov 2024 14:58:45 +0800 Subject: [PATCH 23/35] Load balance support (#135) * load balance support * improve test --- src/AzureAppConfigurationImpl.ts | 22 ++++++- src/AzureAppConfigurationOptions.ts | 8 +++ src/requestTracing/constants.ts | 3 + src/requestTracing/utils.ts | 7 ++- test/failover.test.ts | 6 +- test/loadBalance.test.ts | 96 +++++++++++++++++++++++++++++ test/utils/testHelper.ts | 15 ++++- 7 files changed, 150 insertions(+), 7 deletions(-) create mode 100644 test/loadBalance.test.ts diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index 08f3bb3e..bde471e6 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -75,9 +75,12 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { #featureFlagRefreshInterval: number = DEFAULT_REFRESH_INTERVAL_IN_MS; #featureFlagRefreshTimer: RefreshTimer; - // selectors + // Selectors #featureFlagSelectors: PagedSettingSelector[] = []; + // Load balancing + #lastSuccessfulEndpoint: string = ""; + constructor( clientManager: ConfigurationClientManager, options: AzureAppConfigurationOptions | undefined, @@ -202,7 +205,21 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { } async #executeWithFailoverPolicy(funcToExecute: (client: AppConfigurationClient) => Promise): Promise { - const clientWrappers = await this.#clientManager.getClients(); + let clientWrappers = await this.#clientManager.getClients(); + if (this.#options?.loadBalancingEnabled && this.#lastSuccessfulEndpoint !== "" && clientWrappers.length > 1) { + let nextClientIndex = 0; + // Iterate through clients to find the index of the client with the last successful endpoint + for (const clientWrapper of clientWrappers) { + nextClientIndex++; + if (clientWrapper.endpoint === this.#lastSuccessfulEndpoint) { + break; + } + } + // If we found the last successful client, rotate the list so that the next client is at the beginning + if (nextClientIndex < clientWrappers.length) { + clientWrappers = [...clientWrappers.slice(nextClientIndex), ...clientWrappers.slice(0, nextClientIndex)]; + } + } let successful: boolean; for (const clientWrapper of clientWrappers) { @@ -210,6 +227,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { try { const result = await funcToExecute(clientWrapper.client); this.#isFailoverRequest = false; + this.#lastSuccessfulEndpoint = clientWrapper.endpoint; successful = true; clientWrapper.updateBackoffStatus(successful); return result; diff --git a/src/AzureAppConfigurationOptions.ts b/src/AzureAppConfigurationOptions.ts index 4aa3f99d..56b47b50 100644 --- a/src/AzureAppConfigurationOptions.ts +++ b/src/AzureAppConfigurationOptions.ts @@ -55,4 +55,12 @@ export interface AzureAppConfigurationOptions { * If not specified, the default value is true. */ replicaDiscoveryEnabled?: boolean; + + /** + * Specifies whether to enable load balance or not. + * + * @remarks + * If not specified, the default value is false. + */ + loadBalancingEnabled?: boolean; } diff --git a/src/requestTracing/constants.ts b/src/requestTracing/constants.ts index 60dbb81a..8b39b636 100644 --- a/src/requestTracing/constants.ts +++ b/src/requestTracing/constants.ts @@ -44,6 +44,9 @@ export enum RequestType { WATCH = "Watch" } +export const FEATURES_KEY = "Features"; + // Tag names export const FAILOVER_REQUEST_TAG = "Failover"; export const KEY_VAULT_CONFIGURED_TAG = "UsesKeyVault"; +export const LOAD_BALANCE_CONFIGURED_TAG = "LB"; diff --git a/src/requestTracing/utils.ts b/src/requestTracing/utils.ts index 8a2fdbc4..d48bd5ec 100644 --- a/src/requestTracing/utils.ts +++ b/src/requestTracing/utils.ts @@ -20,7 +20,9 @@ import { RequestType, SERVICE_FABRIC_ENV_VAR, CORRELATION_CONTEXT_HEADER_NAME, - FAILOVER_REQUEST_TAG + FAILOVER_REQUEST_TAG, + FEATURES_KEY, + LOAD_BALANCE_CONFIGURED_TAG } from "./constants"; // Utils @@ -84,6 +86,9 @@ export function createCorrelationContextHeader(options: AzureAppConfigurationOpt keyValues.set(REQUEST_TYPE_KEY, isInitialLoadCompleted ? RequestType.WATCH : RequestType.STARTUP); keyValues.set(HOST_TYPE_KEY, getHostType()); keyValues.set(ENV_KEY, isDevEnvironment() ? DEV_ENV_VAL : undefined); + if (options?.loadBalancingEnabled) { + keyValues.set(FEATURES_KEY, LOAD_BALANCE_CONFIGURED_TAG); + } const tags: string[] = []; if (options?.keyVaultOptions) { diff --git a/test/failover.test.ts b/test/failover.test.ts index c97a127a..2671a1f2 100644 --- a/test/failover.test.ts +++ b/test/failover.test.ts @@ -35,7 +35,7 @@ describe("failover", function () { it("should failover to replica and load key values from config store", async () => { const isFailoverable = true; - mockConfigurationManagerGetClients(isFailoverable, mockedKVs); + mockConfigurationManagerGetClients([], isFailoverable, mockedKVs); const connectionString = createMockedConnectionString(); // replicaDiscoveryEnabled is default to true @@ -47,7 +47,7 @@ describe("failover", function () { it("should failover to replica and load feature flags from config store", async () => { const isFailoverable = true; - mockConfigurationManagerGetClients(isFailoverable, mockedFeatureFlags); + mockConfigurationManagerGetClients([], isFailoverable, mockedFeatureFlags); const connectionString = createMockedConnectionString(); // replicaDiscoveryEnabled is default to true @@ -66,7 +66,7 @@ describe("failover", function () { it("should throw error when all clients failed", async () => { const isFailoverable = false; - mockConfigurationManagerGetClients(isFailoverable); + mockConfigurationManagerGetClients([], isFailoverable); const connectionString = createMockedConnectionString(); return expect(load(connectionString)).eventually.rejectedWith("All clients failed to get configuration settings."); diff --git a/test/loadBalance.test.ts b/test/loadBalance.test.ts new file mode 100644 index 00000000..f0e04b01 --- /dev/null +++ b/test/loadBalance.test.ts @@ -0,0 +1,96 @@ +// 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.js"; +import { restoreMocks, createMockedConnectionString, sleepInMs, createMockedEndpoint, mockConfigurationManagerGetClients, mockAppConfigurationClientLoadBalanceMode } from "./utils/testHelper.js"; +import { AppConfigurationClient } from "@azure/app-configuration"; +import { ConfigurationClientWrapper } from "../src/ConfigurationClientWrapper.js"; + +const fakeEndpoint_1 = createMockedEndpoint("fake_1"); +const fakeEndpoint_2 = createMockedEndpoint("fake_2"); +const fakeClientWrapper_1 = new ConfigurationClientWrapper(fakeEndpoint_1, new AppConfigurationClient(createMockedConnectionString(fakeEndpoint_1))); +const fakeClientWrapper_2 = new ConfigurationClientWrapper(fakeEndpoint_2, new AppConfigurationClient(createMockedConnectionString(fakeEndpoint_2))); +const clientRequestCounter_1 = {count: 0}; +const clientRequestCounter_2 = {count: 0}; + +describe("load balance", function () { + this.timeout(10000); + + beforeEach(() => { + }); + + afterEach(() => { + restoreMocks(); + }); + + it("should load balance the request when loadBalancingEnabled", async () => { + mockConfigurationManagerGetClients([fakeClientWrapper_1, fakeClientWrapper_2], false); + mockAppConfigurationClientLoadBalanceMode(fakeClientWrapper_1, clientRequestCounter_1); + mockAppConfigurationClientLoadBalanceMode(fakeClientWrapper_2, clientRequestCounter_2); + + const connectionString = createMockedConnectionString(); + const settings = await load(connectionString, { + loadBalancingEnabled: true, + featureFlagOptions: { + enabled: true, + selectors: [{ + keyFilter: "*" + }], + refresh: { + enabled: true, + refreshIntervalInMs: 2000 // 2 seconds for quick test. + } + } + }); + // one request for key values, one request for feature flags + expect(clientRequestCounter_1.count).eq(1); + expect(clientRequestCounter_2.count).eq(1); + + await sleepInMs(2 * 1000 + 1); + await settings.refresh(); + // refresh request for feature flags + expect(clientRequestCounter_1.count).eq(2); + expect(clientRequestCounter_2.count).eq(1); + + await sleepInMs(2 * 1000 + 1); + await settings.refresh(); + expect(clientRequestCounter_1.count).eq(2); + expect(clientRequestCounter_2.count).eq(2); + }); + + it("should not load balance the request when loadBalance disabled", async () => { + clientRequestCounter_1.count = 0; + clientRequestCounter_2.count = 0; + mockConfigurationManagerGetClients([fakeClientWrapper_1, fakeClientWrapper_2], false); + mockAppConfigurationClientLoadBalanceMode(fakeClientWrapper_1, clientRequestCounter_1); + mockAppConfigurationClientLoadBalanceMode(fakeClientWrapper_2, clientRequestCounter_2); + + const connectionString = createMockedConnectionString(); + // loadBalancingEnabled is default to false + const settings = await load(connectionString, { + featureFlagOptions: { + enabled: true, + selectors: [{ + keyFilter: "*" + }], + refresh: { + enabled: true, + refreshIntervalInMs: 2000 // 2 seconds for quick test. + } + } + }); + // one request for key values, one request for feature flags + expect(clientRequestCounter_1.count).eq(2); + expect(clientRequestCounter_2.count).eq(0); + + await sleepInMs(2 * 1000 + 1); + await settings.refresh(); + // refresh request for feature flags + expect(clientRequestCounter_1.count).eq(3); + expect(clientRequestCounter_2.count).eq(0); + }); +}); diff --git a/test/utils/testHelper.ts b/test/utils/testHelper.ts index 261b9b57..9284f871 100644 --- a/test/utils/testHelper.ts +++ b/test/utils/testHelper.ts @@ -100,9 +100,21 @@ function mockAppConfigurationClientListConfigurationSettings(...pages: Configura }); } -function mockConfigurationManagerGetClients(isFailoverable: boolean, ...pages: ConfigurationSetting[][]) { +function mockAppConfigurationClientLoadBalanceMode(clientWrapper: ConfigurationClientWrapper, countObject: { count: number }) { + const emptyPages: ConfigurationSetting[][] = []; + sinon.stub(clientWrapper.client, "listConfigurationSettings").callsFake((listOptions) => { + countObject.count += 1; + const kvs = _filterKVs(emptyPages.flat(), listOptions); + return getMockedIterator(emptyPages, kvs, listOptions); + }); +} + +function mockConfigurationManagerGetClients(fakeClientWrappers: ConfigurationClientWrapper[], isFailoverable: boolean, ...pages: ConfigurationSetting[][]) { // Stub the getClients method on the class prototype sinon.stub(ConfigurationClientManager.prototype, "getClients").callsFake(async () => { + if (fakeClientWrappers?.length > 0) { + return fakeClientWrappers; + } const clients: ConfigurationClientWrapper[] = []; const fakeEndpoint = createMockedEndpoint("fake"); const fakeStaticClientWrapper = new ConfigurationClientWrapper(fakeEndpoint, new AppConfigurationClient(createMockedConnectionString(fakeEndpoint))); @@ -230,6 +242,7 @@ export { sinon, mockAppConfigurationClientListConfigurationSettings, mockAppConfigurationClientGetConfigurationSetting, + mockAppConfigurationClientLoadBalanceMode, mockConfigurationManagerGetClients, mockSecretClientGetSecret, restoreMocks, From 9300106b37781ded6be9ca81fe52570734a6bd30 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Thu, 12 Dec 2024 15:55:54 +0800 Subject: [PATCH 24/35] add replica count tracing --- package-lock.json | 2 +- src/AzureAppConfigurationImpl.ts | 9 ++-- src/ConfigurationClientManager.ts | 8 +++- src/requestTracing/constants.ts | 10 +++-- src/requestTracing/utils.ts | 70 +++++++++++++++++-------------- 5 files changed, 57 insertions(+), 42 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7cc0a774..e3a0c076 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "@azure/app-configuration-provider", - "version": "1.1.2", + "version": "2.0.0-preview.1", "license": "MIT", "dependencies": { "@azure/app-configuration": "^1.6.1", diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index d709a808..5a66cfb6 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -33,7 +33,7 @@ import { } from "./featureManagement/constants.js"; import { AzureKeyVaultKeyValueAdapter } from "./keyvault/AzureKeyVaultKeyValueAdapter.js"; import { RefreshTimer } from "./refresh/RefreshTimer.js"; -import { getConfigurationSettingWithTrace, listConfigurationSettingsWithTrace, requestTracingEnabled } from "./requestTracing/utils.js"; +import { RequestTracingOptions, getConfigurationSettingWithTrace, listConfigurationSettingsWithTrace, requestTracingEnabled } from "./requestTracing/utils.js"; import { KeyFilter, LabelFilter, SettingSelector } from "./types.js"; import { ConfigurationClientManager } from "./ConfigurationClientManager.js"; @@ -197,11 +197,12 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { return this.#featureFlagEnabled && !!this.#options?.featureFlagOptions?.refresh?.enabled; } - get #requestTraceOptions() { + get #requestTraceOptions(): RequestTracingOptions { return { - requestTracingEnabled: this.#requestTracingEnabled, - initialLoadCompleted: this.#isInitialLoadCompleted, + enabled: this.#requestTracingEnabled, appConfigOptions: this.#options, + initialLoadCompleted: this.#isInitialLoadCompleted, + replicaCount: this.#clientManager.getReplicaCount(), isFailoverRequest: this.#isFailoverRequest }; } diff --git a/src/ConfigurationClientManager.ts b/src/ConfigurationClientManager.ts index 59e03aa5..2421a786 100644 --- a/src/ConfigurationClientManager.ts +++ b/src/ConfigurationClientManager.ts @@ -30,7 +30,7 @@ export class ConfigurationClientManager { #clientOptions: AppConfigurationClientOptions | undefined; #appConfigOptions: AzureAppConfigurationOptions | undefined; #validDomain: string; - #staticClients: ConfigurationClientWrapper[]; + #staticClients: ConfigurationClientWrapper[]; // there should always be only one static client #dynamicClients: ConfigurationClientWrapper[]; #lastFallbackClientRefreshTime: number = 0; #lastFallbackClientRefreshAttempt: number = 0; @@ -96,7 +96,11 @@ export class ConfigurationClientManager { this.#isFailoverable = true; } - async getClients() : Promise { + getReplicaCount(): number { + return this.#dynamicClients.length; + } + + async getClients(): Promise { if (!this.#isFailoverable) { return this.#staticClients; } diff --git a/src/requestTracing/constants.ts b/src/requestTracing/constants.ts index 8b39b636..30a12f43 100644 --- a/src/requestTracing/constants.ts +++ b/src/requestTracing/constants.ts @@ -37,16 +37,20 @@ export const CONTAINER_APP_ENV_VAR = "CONTAINER_APP_NAME"; export const KUBERNETES_ENV_VAR = "KUBERNETES_PORT"; export const SERVICE_FABRIC_ENV_VAR = "Fabric_NodeName"; // See: https://docs.microsoft.com/en-us/azure/service-fabric/service-fabric-environment-variables-reference -// Request Type +// Request type export const REQUEST_TYPE_KEY = "RequestType"; export enum RequestType { STARTUP = "Startup", WATCH = "Watch" } -export const FEATURES_KEY = "Features"; +// Replica count +export const REPLICA_COUNT_KEY = "ReplicaCount"; // Tag names -export const FAILOVER_REQUEST_TAG = "Failover"; export const KEY_VAULT_CONFIGURED_TAG = "UsesKeyVault"; +export const FAILOVER_REQUEST_TAG = "Failover"; + +// Compact feature tags +export const FEATURES_KEY = "Features"; export const LOAD_BALANCE_CONFIGURED_TAG = "LB"; diff --git a/src/requestTracing/utils.ts b/src/requestTracing/utils.ts index d48bd5ec..23ec4602 100644 --- a/src/requestTracing/utils.ts +++ b/src/requestTracing/utils.ts @@ -20,29 +20,31 @@ import { RequestType, SERVICE_FABRIC_ENV_VAR, CORRELATION_CONTEXT_HEADER_NAME, + REPLICA_COUNT_KEY, FAILOVER_REQUEST_TAG, FEATURES_KEY, LOAD_BALANCE_CONFIGURED_TAG } from "./constants"; +export interface RequestTracingOptions { + enabled: boolean; + appConfigOptions: AzureAppConfigurationOptions | undefined; + initialLoadCompleted: boolean; + replicaCount: number; + isFailoverRequest: boolean; +} + // Utils export function listConfigurationSettingsWithTrace( - requestTracingOptions: { - requestTracingEnabled: boolean; - initialLoadCompleted: boolean; - appConfigOptions: AzureAppConfigurationOptions | undefined; - isFailoverRequest: boolean; - }, + requestTracingOptions: RequestTracingOptions, client: AppConfigurationClient, listOptions: ListConfigurationSettingsOptions ) { - const { requestTracingEnabled, initialLoadCompleted, appConfigOptions, isFailoverRequest } = requestTracingOptions; - const actualListOptions = { ...listOptions }; - if (requestTracingEnabled) { + if (requestTracingOptions.enabled) { actualListOptions.requestOptions = { customHeaders: { - [CORRELATION_CONTEXT_HEADER_NAME]: createCorrelationContextHeader(appConfigOptions, initialLoadCompleted, isFailoverRequest) + [CORRELATION_CONTEXT_HEADER_NAME]: createCorrelationContextHeader(requestTracingOptions) } }; } @@ -51,23 +53,17 @@ export function listConfigurationSettingsWithTrace( } export function getConfigurationSettingWithTrace( - requestTracingOptions: { - requestTracingEnabled: boolean; - initialLoadCompleted: boolean; - appConfigOptions: AzureAppConfigurationOptions | undefined; - isFailoverRequest: boolean; - }, + requestTracingOptions: RequestTracingOptions, client: AppConfigurationClient, configurationSettingId: ConfigurationSettingId, getOptions?: GetConfigurationSettingOptions, ) { - const { requestTracingEnabled, initialLoadCompleted, appConfigOptions, isFailoverRequest } = requestTracingOptions; const actualGetOptions = { ...getOptions }; - if (requestTracingEnabled) { + if (requestTracingOptions.enabled) { actualGetOptions.requestOptions = { customHeaders: { - [CORRELATION_CONTEXT_HEADER_NAME]: createCorrelationContextHeader(appConfigOptions, initialLoadCompleted, isFailoverRequest) + [CORRELATION_CONTEXT_HEADER_NAME]: createCorrelationContextHeader(requestTracingOptions) } }; } @@ -75,29 +71,43 @@ export function getConfigurationSettingWithTrace( return client.getConfigurationSetting(configurationSettingId, actualGetOptions); } -export function createCorrelationContextHeader(options: AzureAppConfigurationOptions | undefined, isInitialLoadCompleted: boolean, isFailoverRequest: boolean): string { +export function createCorrelationContextHeader(requestTracingOptions: RequestTracingOptions): string { /* RequestType: 'Startup' during application starting up, 'Watch' after startup completed. Host: identify with defined envs - Env: identify by env `NODE_ENV` which is a popular but not standard.usually the value can be "development", "production". + Env: identify by env `NODE_ENV` which is a popular but not standard. Usually, the value can be "development", "production". + ReplicaCount: identify how many replicas are found + Features: LB UsersKeyVault + Failover */ const keyValues = new Map(); - keyValues.set(REQUEST_TYPE_KEY, isInitialLoadCompleted ? RequestType.WATCH : RequestType.STARTUP); + const tags: string[] = []; + + keyValues.set(REQUEST_TYPE_KEY, requestTracingOptions.initialLoadCompleted ? RequestType.WATCH : RequestType.STARTUP); keyValues.set(HOST_TYPE_KEY, getHostType()); keyValues.set(ENV_KEY, isDevEnvironment() ? DEV_ENV_VAL : undefined); - if (options?.loadBalancingEnabled) { - keyValues.set(FEATURES_KEY, LOAD_BALANCE_CONFIGURED_TAG); - } - const tags: string[] = []; - if (options?.keyVaultOptions) { - const { credential, secretClients, secretResolver } = options.keyVaultOptions; + const appConfigOptions = requestTracingOptions.appConfigOptions; + if (appConfigOptions?.keyVaultOptions) { + const { credential, secretClients, secretResolver } = appConfigOptions.keyVaultOptions; if (credential !== undefined || secretClients?.length || secretResolver !== undefined) { tags.push(KEY_VAULT_CONFIGURED_TAG); } } + if (requestTracingOptions.isFailoverRequest) { + tags.push(FAILOVER_REQUEST_TAG); + } + if (requestTracingOptions.replicaCount > 0) { + keyValues.set(REPLICA_COUNT_KEY, requestTracingOptions.replicaCount.toString()); + } + + // Compact tags: Features=LB+... + if (appConfigOptions?.loadBalancingEnabled) { + keyValues.set(FEATURES_KEY, LOAD_BALANCE_CONFIGURED_TAG); + } + const contextParts: string[] = []; for (const [k, v] of keyValues) { if (v !== undefined) { @@ -108,10 +118,6 @@ export function createCorrelationContextHeader(options: AzureAppConfigurationOpt contextParts.push(tag); } - if (isFailoverRequest) { - contextParts.push(FAILOVER_REQUEST_TAG); - } - return contextParts.join(","); } From 9a38443ff44ffc2c7eba4f829a3f965d0644a25a Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Fri, 13 Dec 2024 13:36:25 +0800 Subject: [PATCH 25/35] fix bug --- src/ConfigurationClientManager.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ConfigurationClientManager.ts b/src/ConfigurationClientManager.ts index 2421a786..b11e9350 100644 --- a/src/ConfigurationClientManager.ts +++ b/src/ConfigurationClientManager.ts @@ -32,6 +32,7 @@ export class ConfigurationClientManager { #validDomain: string; #staticClients: ConfigurationClientWrapper[]; // there should always be only one static client #dynamicClients: ConfigurationClientWrapper[]; + #replicaCount: number = 0; #lastFallbackClientRefreshTime: number = 0; #lastFallbackClientRefreshAttempt: number = 0; @@ -97,7 +98,7 @@ export class ConfigurationClientManager { } getReplicaCount(): number { - return this.#dynamicClients.length; + return this.#replicaCount; } async getClients(): Promise { @@ -165,6 +166,7 @@ export class ConfigurationClientManager { this.#dynamicClients = newDynamicClients; this.#lastFallbackClientRefreshTime = Date.now(); + this.#replicaCount = this.#dynamicClients.length; } /** From ddf3d6017420ec3dcb0b5ebebc3947dd9e412cc7 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Fri, 13 Dec 2024 13:45:03 +0800 Subject: [PATCH 26/35] audit vulnerablitiy --- package-lock.json | 173 +++++++++++++++++++++++----------------------- 1 file changed, 87 insertions(+), 86 deletions(-) diff --git a/package-lock.json b/package-lock.json index e3a0c076..44b53a05 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1138,10 +1138,11 @@ } }, "node_modules/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==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -1425,10 +1426,11 @@ "dev": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -1439,11 +1441,12 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -1501,10 +1504,11 @@ } }, "node_modules/diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } @@ -2574,32 +2578,32 @@ } }, "node_modules/mocha": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", - "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", - "dev": true, - "dependencies": { - "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" + "version": "10.8.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", + "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" }, "bin": { "_mocha": "bin/_mocha", @@ -2607,10 +2611,6 @@ }, "engines": { "node": ">= 14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mochajs" } }, "node_modules/mocha/node_modules/brace-expansion": { @@ -2618,15 +2618,38 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, + "node_modules/mocha/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/mocha/node_modules/minimatch": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", - "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -2634,12 +2657,6 @@ "node": ">=10" } }, - "node_modules/mocha/node_modules/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 - }, "node_modules/mocha/node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -2656,21 +2673,10 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/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, - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" }, "node_modules/natural-compare": { "version": "1.4.0", @@ -2957,6 +2963,7 @@ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "dev": true, + "license": "MIT", "dependencies": { "safe-buffer": "^5.1.0" } @@ -3178,10 +3185,11 @@ } }, "node_modules/serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "randombytes": "^2.1.0" } @@ -3238,15 +3246,6 @@ "url": "https://opencollective.com/sinon" } }, - "node_modules/sinon/node_modules/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, - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -3480,10 +3479,11 @@ } }, "node_modules/workerpool": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", - "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", - "dev": true + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", + "dev": true, + "license": "Apache-2.0" }, "node_modules/wrap-ansi": { "version": "7.0.0", @@ -3559,10 +3559,11 @@ } }, "node_modules/yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } From eced463a356007ee7ac0553d5a84c1260a8056bc Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Fri, 13 Dec 2024 14:04:45 +0800 Subject: [PATCH 27/35] centralize timeout --- test/clientOptions.test.ts | 4 ++-- test/failover.test.ts | 4 ++-- test/featureFlag.test.ts | 4 ++-- test/json.test.ts | 4 +++- test/keyvault.test.ts | 4 ++-- test/load.test.ts | 4 ++-- test/loadBalance.test.ts | 4 ++-- test/refresh.test.ts | 4 ++-- test/requestTracing.test.ts | 4 ++-- test/utils/testHelper.ts | 6 +++++- 10 files changed, 24 insertions(+), 18 deletions(-) diff --git a/test/clientOptions.test.ts b/test/clientOptions.test.ts index 62e1b21c..2e9417e9 100644 --- a/test/clientOptions.test.ts +++ b/test/clientOptions.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 { createMockedConnectionString } from "./utils/testHelper.js"; +import { MAX_TIME_OUT, createMockedConnectionString } from "./utils/testHelper.js"; import * as nock from "nock"; class HttpRequestCountPolicy { @@ -27,7 +27,7 @@ class HttpRequestCountPolicy { } describe("custom client options", function () { - this.timeout(15000); + this.timeout(MAX_TIME_OUT); const fakeEndpoint = "https://azure.azconfig.io"; beforeEach(() => { diff --git a/test/failover.test.ts b/test/failover.test.ts index 2671a1f2..e1f2f043 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, mockConfigurationManagerGetClients, restoreMocks } from "./utils/testHelper"; +import { MAX_TIME_OUT, createMockedConnectionString, createMockedFeatureFlag, createMockedKeyValue, mockConfigurationManagerGetClients, restoreMocks } from "./utils/testHelper"; import { getValidDomain, isValidEndpoint } from "../src/ConfigurationClientManager"; const mockedKVs = [{ @@ -27,7 +27,7 @@ const mockedFeatureFlags = [{ ]); describe("failover", function () { - this.timeout(15000); + this.timeout(MAX_TIME_OUT); afterEach(() => { restoreMocks(); diff --git a/test/featureFlag.test.ts b/test/featureFlag.test.ts index 9294a451..2544e617 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 { MAX_TIME_OUT, createMockedConnectionString, createMockedEndpoint, createMockedFeatureFlag, createMockedKeyValue, mockAppConfigurationClientListConfigurationSettings, restoreMocks } from "./utils/testHelper.js"; chai.use(chaiAsPromised); const expect = chai.expect; @@ -199,7 +199,7 @@ const mockedKVs = [{ ]); describe("feature flags", function () { - this.timeout(10000); + this.timeout(MAX_TIME_OUT); before(() => { mockAppConfigurationClientListConfigurationSettings([mockedKVs]); diff --git a/test/json.test.ts b/test/json.test.ts index 47a3f670..cb937bd9 100644 --- a/test/json.test.ts +++ b/test/json.test.ts @@ -6,12 +6,14 @@ 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 { MAX_TIME_OUT, 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"); describe("json", function () { + this.timeout(MAX_TIME_OUT); + beforeEach(() => { }); diff --git a/test/keyvault.test.ts b/test/keyvault.test.ts index 2877243b..e88044ea 100644 --- a/test/keyvault.test.ts +++ b/test/keyvault.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 { sinon, createMockedConnectionString, createMockedTokenCredential, mockAppConfigurationClientListConfigurationSettings, mockSecretClientGetSecret, restoreMocks, createMockedKeyVaultReference } from "./utils/testHelper.js"; +import { MAX_TIME_OUT, sinon, createMockedConnectionString, createMockedTokenCredential, mockAppConfigurationClientListConfigurationSettings, mockSecretClientGetSecret, restoreMocks, createMockedKeyVaultReference } from "./utils/testHelper.js"; import { KeyVaultSecret, SecretClient } from "@azure/keyvault-secrets"; const mockedData = [ @@ -27,7 +27,7 @@ function mockNewlyCreatedKeyVaultSecretClients() { mockSecretClientGetSecret(mockedData.map(([_key, secretUri, value]) => [secretUri, value])); } describe("key vault reference", function () { - this.timeout(10000); + this.timeout(MAX_TIME_OUT); beforeEach(() => { mockAppConfigurationClient(); diff --git a/test/load.test.ts b/test/load.test.ts index 6d2c94b8..d36a3311 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 { MAX_TIME_OUT, mockAppConfigurationClientListConfigurationSettings, restoreMocks, createMockedConnectionString, createMockedEndpoint, createMockedTokenCredential, createMockedKeyValue } from "./utils/testHelper.js"; const mockedKVs = [{ key: "app.settings.fontColor", @@ -77,7 +77,7 @@ const mockedKVs = [{ ].map(createMockedKeyValue); describe("load", function () { - this.timeout(10000); + this.timeout(MAX_TIME_OUT); before(() => { mockAppConfigurationClientListConfigurationSettings([mockedKVs]); diff --git a/test/loadBalance.test.ts b/test/loadBalance.test.ts index f0e04b01..59248b38 100644 --- a/test/loadBalance.test.ts +++ b/test/loadBalance.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 { restoreMocks, createMockedConnectionString, sleepInMs, createMockedEndpoint, mockConfigurationManagerGetClients, mockAppConfigurationClientLoadBalanceMode } from "./utils/testHelper.js"; +import { MAX_TIME_OUT, restoreMocks, createMockedConnectionString, sleepInMs, createMockedEndpoint, mockConfigurationManagerGetClients, mockAppConfigurationClientLoadBalanceMode } from "./utils/testHelper.js"; import { AppConfigurationClient } from "@azure/app-configuration"; import { ConfigurationClientWrapper } from "../src/ConfigurationClientWrapper.js"; @@ -18,7 +18,7 @@ const clientRequestCounter_1 = {count: 0}; const clientRequestCounter_2 = {count: 0}; describe("load balance", function () { - this.timeout(10000); + this.timeout(MAX_TIME_OUT); beforeEach(() => { }); diff --git a/test/refresh.test.ts b/test/refresh.test.ts index 5fbeb973..6c537fb4 100644 --- a/test/refresh.test.ts +++ b/test/refresh.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, mockAppConfigurationClientGetConfigurationSetting, restoreMocks, createMockedConnectionString, createMockedKeyValue, sleepInMs, createMockedFeatureFlag } from "./utils/testHelper.js"; +import { MAX_TIME_OUT, mockAppConfigurationClientListConfigurationSettings, mockAppConfigurationClientGetConfigurationSetting, restoreMocks, createMockedConnectionString, createMockedKeyValue, sleepInMs, createMockedFeatureFlag } from "./utils/testHelper.js"; import * as uuid from "uuid"; let mockedKVs: any[] = []; @@ -33,7 +33,7 @@ const getKvCallback = () => { }; describe("dynamic refresh", function () { - this.timeout(10000); + this.timeout(MAX_TIME_OUT); beforeEach(() => { mockedKVs = [ diff --git a/test/requestTracing.test.ts b/test/requestTracing.test.ts index d146301d..4e5e5fab 100644 --- a/test/requestTracing.test.ts +++ b/test/requestTracing.test.ts @@ -5,7 +5,7 @@ import * as chai from "chai"; import * as chaiAsPromised from "chai-as-promised"; chai.use(chaiAsPromised); const expect = chai.expect; -import { createMockedConnectionString, createMockedKeyValue, createMockedTokenCredential, mockAppConfigurationClientListConfigurationSettings, restoreMocks, sleepInMs } from "./utils/testHelper.js"; +import { MAX_TIME_OUT, createMockedConnectionString, createMockedKeyValue, createMockedTokenCredential, mockAppConfigurationClientListConfigurationSettings, restoreMocks, sleepInMs } from "./utils/testHelper.js"; import { load } from "./exportedApi.js"; class HttpRequestHeadersPolicy { @@ -23,7 +23,7 @@ class HttpRequestHeadersPolicy { } describe("request tracing", function () { - this.timeout(15000); + this.timeout(MAX_TIME_OUT); const fakeEndpoint = "https://127.0.0.1"; // sufficient to test the request it sends out const headerPolicy = new HttpRequestHeadersPolicy(); diff --git a/test/utils/testHelper.ts b/test/utils/testHelper.ts index 3b51596c..4d3b4145 100644 --- a/test/utils/testHelper.ts +++ b/test/utils/testHelper.ts @@ -13,6 +13,8 @@ import * as crypto from "crypto"; import { ConfigurationClientManager } from "../../src/ConfigurationClientManager"; import { ConfigurationClientWrapper } from "../../src/ConfigurationClientWrapper"; +const MAX_TIME_OUT = 20000; + const TEST_CLIENT_ID = "00000000-0000-0000-0000-000000000000"; const TEST_TENANT_ID = "00000000-0000-0000-0000-000000000000"; const TEST_CLIENT_SECRET = "0000000000000000000000000000000000000000"; @@ -262,5 +264,7 @@ export { createMockedKeyValue, createMockedFeatureFlag, - sleepInMs + sleepInMs, + + MAX_TIME_OUT }; From 4d77f89a02962913a2f0c4b7e739c4910bf1987b Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Fri, 13 Dec 2024 14:07:02 +0800 Subject: [PATCH 28/35] fix lint --- test/loadBalance.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/loadBalance.test.ts b/test/loadBalance.test.ts index 59248b38..59bdf0f8 100644 --- a/test/loadBalance.test.ts +++ b/test/loadBalance.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 { MAX_TIME_OUT, restoreMocks, createMockedConnectionString, sleepInMs, createMockedEndpoint, mockConfigurationManagerGetClients, mockAppConfigurationClientLoadBalanceMode } from "./utils/testHelper.js"; +import { MAX_TIME_OUT, restoreMocks, createMockedConnectionString, sleepInMs, createMockedEndpoint, mockConfigurationManagerGetClients, mockAppConfigurationClientLoadBalanceMode } from "./utils/testHelper.js"; import { AppConfigurationClient } from "@azure/app-configuration"; import { ConfigurationClientWrapper } from "../src/ConfigurationClientWrapper.js"; From 04f6d23a790c061a88ea8b81d554e5c1e1217c1d Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Fri, 13 Dec 2024 14:49:53 +0800 Subject: [PATCH 29/35] add testcase --- test/requestTracing.test.ts | 16 +++++++++++++++- test/utils/testHelper.ts | 4 ++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/test/requestTracing.test.ts b/test/requestTracing.test.ts index 4e5e5fab..311f857e 100644 --- a/test/requestTracing.test.ts +++ b/test/requestTracing.test.ts @@ -5,7 +5,8 @@ import * as chai from "chai"; import * as chaiAsPromised from "chai-as-promised"; chai.use(chaiAsPromised); const expect = chai.expect; -import { MAX_TIME_OUT, createMockedConnectionString, createMockedKeyValue, createMockedTokenCredential, mockAppConfigurationClientListConfigurationSettings, restoreMocks, sleepInMs } from "./utils/testHelper.js"; +import { MAX_TIME_OUT, createMockedConnectionString, createMockedKeyValue, createMockedTokenCredential, mockAppConfigurationClientListConfigurationSettings, restoreMocks, sinon, sleepInMs } from "./utils/testHelper.js"; +import { ConfigurationClientManager } from "../src/ConfigurationClientManager.js"; import { load } from "./exportedApi.js"; class HttpRequestHeadersPolicy { @@ -75,6 +76,19 @@ describe("request tracing", function () { expect(correlationContext.includes("UsesKeyVault")).eq(true); }); + it("should have replica count in correlation-context header", async () => { + const replicaCount = 2; + sinon.stub(ConfigurationClientManager.prototype, "getReplicaCount").returns(replicaCount); + try { + await load(createMockedConnectionString(fakeEndpoint), { clientOptions }); + } catch (e) { /* empty */ } + expect(headerPolicy.headers).not.undefined; + const correlationContext = headerPolicy.headers.get("Correlation-Context"); + expect(correlationContext).not.undefined; + expect(correlationContext.includes(`ReplicaCount=${replicaCount}`)).eq(true); + sinon.restore(); + }); + it("should detect env in correlation-context header", async () => { process.env.NODE_ENV = "development"; try { diff --git a/test/utils/testHelper.ts b/test/utils/testHelper.ts index 4d3b4145..d3e9a063 100644 --- a/test/utils/testHelper.ts +++ b/test/utils/testHelper.ts @@ -10,8 +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"; +import { ConfigurationClientManager } from "../../src/ConfigurationClientManager.js"; +import { ConfigurationClientWrapper } from "../../src/ConfigurationClientWrapper.js"; const MAX_TIME_OUT = 20000; From 71aebabca61a3e3fa5df2b7112b1236768932408 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Thu, 19 Dec 2024 13:51:25 +0800 Subject: [PATCH 30/35] Refresh key value collection based on page etag (#133) * refresh based on page etag * remove watchAll & reorganize the code * add testcase * fix lint & update method name * add comment * update variable name * move public method * add more comments * fix lint * resolve merge conflict --- src/AzureAppConfigurationImpl.ts | 569 ++++++++++++++++--------------- src/RefreshOptions.ts | 3 + test/refresh.test.ts | 88 +++-- 3 files changed, 363 insertions(+), 297 deletions(-) diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index 6f2ee9dd..916ece49 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -69,20 +69,27 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { // Refresh #refreshInProgress: boolean = false; - #refreshInterval: number = DEFAULT_REFRESH_INTERVAL_IN_MS; #onRefreshListeners: Array<() => any> = []; /** * Aka watched settings. */ #sentinels: ConfigurationSettingId[] = []; - #refreshTimer: RefreshTimer; + #watchAll: boolean = false; + #kvRefreshInterval: number = DEFAULT_REFRESH_INTERVAL_IN_MS; + #kvRefreshTimer: RefreshTimer; // Feature flags - #featureFlagRefreshInterval: number = DEFAULT_REFRESH_INTERVAL_IN_MS; - #featureFlagRefreshTimer: RefreshTimer; + #ffRefreshInterval: number = DEFAULT_REFRESH_INTERVAL_IN_MS; + #ffRefreshTimer: RefreshTimer; - // Selectors - #featureFlagSelectors: PagedSettingSelector[] = []; + /** + * Selectors of key-values obtained from @see AzureAppConfigurationOptions.selectors + */ + #kvSelectors: PagedSettingSelector[] = []; + /** + * Selectors of feature flags obtained from @see AzureAppConfigurationOptions.featureFlagOptions.selectors + */ + #ffSelectors: PagedSettingSelector[] = []; // Load balancing #lastSuccessfulEndpoint: string = ""; @@ -94,7 +101,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { this.#options = options; this.#clientManager = clientManager; - // Enable request tracing if not opt-out + // enable request tracing if not opt-out this.#requestTracingEnabled = requestTracingEnabled(); if (this.#requestTracingEnabled) { this.#featureFlagTracing = new FeatureFlagTracingOptions(); @@ -104,40 +111,40 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { this.#sortedTrimKeyPrefixes = [...options.trimKeyPrefixes].sort((a, b) => b.localeCompare(a)); } + // if no selector is specified, always load key values using the default selector: key="*" and label="\0" + this.#kvSelectors = getValidKeyValueSelectors(options?.selectors); + if (options?.refreshOptions?.enabled) { - const { watchedSettings, refreshIntervalInMs } = options.refreshOptions; - // validate watched settings + const { refreshIntervalInMs, watchedSettings } = options.refreshOptions; if (watchedSettings === undefined || watchedSettings.length === 0) { - throw new Error("Refresh is enabled but no watched settings are specified."); + this.#watchAll = true; // if no watched settings is specified, then watch all + } else { + for (const setting of watchedSettings) { + if (setting.key.includes("*") || setting.key.includes(",")) { + throw new Error("The characters '*' and ',' are not supported in key of watched settings."); + } + if (setting.label?.includes("*") || setting.label?.includes(",")) { + throw new Error("The characters '*' and ',' are not supported in label of watched settings."); + } + this.#sentinels.push(setting); + } } // custom refresh interval if (refreshIntervalInMs !== undefined) { if (refreshIntervalInMs < MIN_REFRESH_INTERVAL_IN_MS) { throw new Error(`The refresh interval cannot be less than ${MIN_REFRESH_INTERVAL_IN_MS} milliseconds.`); - } else { - this.#refreshInterval = refreshIntervalInMs; - } - } - - for (const setting of watchedSettings) { - if (setting.key.includes("*") || setting.key.includes(",")) { - throw new Error("The characters '*' and ',' are not supported in key of watched settings."); - } - if (setting.label?.includes("*") || setting.label?.includes(",")) { - throw new Error("The characters '*' and ',' are not supported in label of watched settings."); + this.#kvRefreshInterval = refreshIntervalInMs; } - this.#sentinels.push(setting); } - - this.#refreshTimer = new RefreshTimer(this.#refreshInterval); + this.#kvRefreshTimer = new RefreshTimer(this.#kvRefreshInterval); } // feature flag options if (options?.featureFlagOptions?.enabled) { - // validate feature flag selectors - this.#featureFlagSelectors = getValidFeatureFlagSelectors(options.featureFlagOptions.selectors); + // validate feature flag selectors, only load feature flags when enabled + this.#ffSelectors = getValidFeatureFlagSelectors(options.featureFlagOptions.selectors); if (options.featureFlagOptions.refresh?.enabled) { const { refreshIntervalInMs } = options.featureFlagOptions.refresh; @@ -146,11 +153,11 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { if (refreshIntervalInMs < MIN_REFRESH_INTERVAL_IN_MS) { throw new Error(`The feature flag refresh interval cannot be less than ${MIN_REFRESH_INTERVAL_IN_MS} milliseconds.`); } else { - this.#featureFlagRefreshInterval = refreshIntervalInMs; + this.#ffRefreshInterval = refreshIntervalInMs; } } - this.#featureFlagRefreshTimer = new RefreshTimer(this.#featureFlagRefreshInterval); + this.#ffRefreshTimer = new RefreshTimer(this.#ffRefreshInterval); } } @@ -158,40 +165,6 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { this.#adapters.push(new JsonKeyValueAdapter()); } - // #region ReadonlyMap APIs - get(key: string): T | undefined { - return this.#configMap.get(key); - } - - forEach(callbackfn: (value: any, key: string, map: ReadonlyMap) => void, thisArg?: any): void { - this.#configMap.forEach(callbackfn, thisArg); - } - - has(key: string): boolean { - return this.#configMap.has(key); - } - - get size(): number { - return this.#configMap.size; - } - - entries(): MapIterator<[string, any]> { - return this.#configMap.entries(); - } - - keys(): MapIterator { - return this.#configMap.keys(); - } - - values(): MapIterator { - return this.#configMap.values(); - } - - [Symbol.iterator](): MapIterator<[string, any]> { - return this.#configMap[Symbol.iterator](); - } - // #endregion - get #refreshEnabled(): boolean { return !!this.#options?.refreshOptions?.enabled; } @@ -215,181 +188,42 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { }; } - async #executeWithFailoverPolicy(funcToExecute: (client: AppConfigurationClient) => Promise): Promise { - let clientWrappers = await this.#clientManager.getClients(); - if (this.#options?.loadBalancingEnabled && this.#lastSuccessfulEndpoint !== "" && clientWrappers.length > 1) { - let nextClientIndex = 0; - // Iterate through clients to find the index of the client with the last successful endpoint - for (const clientWrapper of clientWrappers) { - nextClientIndex++; - if (clientWrapper.endpoint === this.#lastSuccessfulEndpoint) { - break; - } - } - // If we found the last successful client, rotate the list so that the next client is at the beginning - if (nextClientIndex < clientWrappers.length) { - clientWrappers = [...clientWrappers.slice(nextClientIndex), ...clientWrappers.slice(0, nextClientIndex)]; - } - } - - let successful: boolean; - for (const clientWrapper of clientWrappers) { - successful = false; - try { - const result = await funcToExecute(clientWrapper.client); - this.#isFailoverRequest = false; - this.#lastSuccessfulEndpoint = clientWrapper.endpoint; - successful = true; - clientWrapper.updateBackoffStatus(successful); - return result; - } catch (error) { - if (isFailoverableError(error)) { - clientWrapper.updateBackoffStatus(successful); - this.#isFailoverRequest = true; - continue; - } - - throw error; - } - } - - this.#clientManager.refreshClients(); - throw new Error("All clients failed to get configuration settings."); + // #region ReadonlyMap APIs + get(key: string): T | undefined { + return this.#configMap.get(key); } - async #loadSelectedKeyValues(): Promise { - // validate selectors - const selectors = getValidKeyValueSelectors(this.#options?.selectors); - - 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; - }; - - return await this.#executeWithFailoverPolicy(funcToExecute) as ConfigurationSetting[]; + forEach(callbackfn: (value: any, key: string, map: ReadonlyMap) => void, thisArg?: any): void { + this.#configMap.forEach(callbackfn, thisArg); } - /** - * Update etag of watched settings from loaded data. If a watched setting is not covered by any selector, a request will be sent to retrieve it. - */ - async #updateWatchedKeyValuesEtag(existingSettings: ConfigurationSetting[]): Promise { - if (!this.#refreshEnabled) { - return; - } - - for (const sentinel of this.#sentinels) { - const matchedSetting = existingSettings.find(s => s.key === sentinel.key && s.label === sentinel.label); - if (matchedSetting) { - sentinel.etag = matchedSetting.etag; - } else { - // Send a request to retrieve key-value since it may be either not loaded or loaded with a different label or different casing - const { key, label } = sentinel; - const response = await this.#getConfigurationSetting({ key, label }); - if (response) { - sentinel.etag = response.etag; - } else { - sentinel.etag = undefined; - } - } - } + has(key: string): boolean { + return this.#configMap.has(key); } - async #loadSelectedAndWatchedKeyValues() { - const keyValues: [key: string, value: unknown][] = []; - const loadedSettings = await this.#loadSelectedKeyValues(); - await this.#updateWatchedKeyValuesEtag(loadedSettings); - - // process key-values, watched settings have higher priority - for (const setting of loadedSettings) { - const [key, value] = await this.#processKeyValues(setting); - keyValues.push([key, value]); - } - - this.#clearLoadedKeyValues(); // clear existing key-values in case of configuration setting deletion - for (const [k, v] of keyValues) { - this.#configMap.set(k, v); - } + get size(): number { + return this.#configMap.size; } - async #clearLoadedKeyValues() { - for (const key of this.#configMap.keys()) { - if (key !== FEATURE_MANAGEMENT_KEY_NAME) { - this.#configMap.delete(key); - } - } + entries(): MapIterator<[string, any]> { + return this.#configMap.entries(); } - async #loadFeatureFlags() { - // 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) - ); - - 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; - } - - this.#featureFlagSelectors = selectors; - return featureFlagSettings; - }; - - const featureFlagSettings = await this.#executeWithFailoverPolicy(funcToExecute) as ConfigurationSetting[]; - - if (this.#requestTracingEnabled && this.#featureFlagTracing !== undefined) { - this.#featureFlagTracing.resetFeatureFlagTracing(); - } + keys(): MapIterator { + return this.#configMap.keys(); + } - // parse feature flags - const featureFlags = await Promise.all( - featureFlagSettings.map(setting => this.#parseFeatureFlag(setting)) - ); + values(): MapIterator { + return this.#configMap.values(); + } - // feature_management is a reserved key, and feature_flags is an array of feature flags - this.#configMap.set(FEATURE_MANAGEMENT_KEY_NAME, { [FEATURE_FLAGS_KEY_NAME]: featureFlags }); + [Symbol.iterator](): MapIterator<[string, any]> { + return this.#configMap[Symbol.iterator](); } + // #endregion /** - * Load the configuration store for the first time. + * Loads the configuration store for the first time. */ async load() { await this.#loadSelectedAndWatchedKeyValues(); @@ -401,7 +235,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { } /** - * Construct hierarchical data object from map. + * Constructs hierarchical data object from map. */ constructConfigurationObject(options?: ConfigurationObjectConstructionOptions): Record { const separator = options?.separator ?? "."; @@ -444,7 +278,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { } /** - * Refresh the configuration store. + * Refreshes the configuration. */ async refresh(): Promise { if (!this.#refreshEnabled && !this.#featureFlagRefreshEnabled) { @@ -462,6 +296,26 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { } } + /** + * Registers a callback function to be called when the configuration is refreshed. + */ + onRefresh(listener: () => any, thisArg?: any): Disposable { + if (!this.#refreshEnabled && !this.#featureFlagRefreshEnabled) { + throw new Error("Refresh is not enabled for key-values or feature flags."); + } + + const boundedListener = listener.bind(thisArg); + this.#onRefreshListeners.push(boundedListener); + + const remove = () => { + const index = this.#onRefreshListeners.indexOf(boundedListener); + if (index >= 0) { + this.#onRefreshListeners.splice(index, 1); + } + }; + return new Disposable(remove); + } + async #refreshTasks(): Promise { const refreshTasks: Promise[] = []; if (this.#refreshEnabled) { @@ -492,17 +346,141 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { } /** - * Refresh key-values. + * Loads configuration settings from App Configuration, either key-value settings or feature flag settings. + * Additionally, updates the `pageEtags` property of the corresponding @see PagedSettingSelector after loading. + * + * @param loadFeatureFlag - Determines which type of configurationsettings to load: + * If true, loads feature flag using the feature flag selectors; + * If false, loads key-value using the key-value selectors. Defaults to false. + */ + async #loadConfigurationSettings(loadFeatureFlag: boolean = false): Promise { + const selectors = loadFeatureFlag ? this.#ffSelectors : this.#kvSelectors; + const funcToExecute = async (client) => { + const loadedSettings: ConfigurationSetting[] = []; + // deep copy selectors to avoid modification if current client fails + const selectorsToUpdate = JSON.parse( + JSON.stringify(selectors) + ); + + for (const selector of selectorsToUpdate) { + const listOptions: ListConfigurationSettingsOptions = { + keyFilter: 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 (loadFeatureFlag === isFeatureFlag(setting)) { + loadedSettings.push(setting); + } + } + } + selector.pageEtags = pageEtags; + } + + if (loadFeatureFlag) { + this.#ffSelectors = selectorsToUpdate; + } else { + this.#kvSelectors = selectorsToUpdate; + } + return loadedSettings; + }; + + return await this.#executeWithFailoverPolicy(funcToExecute) as ConfigurationSetting[]; + } + + /** + * Loads selected key-values and watched settings (sentinels) for refresh from App Configuration to the local configuration. + */ + async #loadSelectedAndWatchedKeyValues() { + const keyValues: [key: string, value: unknown][] = []; + const loadedSettings = await this.#loadConfigurationSettings(); + if (this.#refreshEnabled && !this.#watchAll) { + await this.#updateWatchedKeyValuesEtag(loadedSettings); + } + + // process key-values, watched settings have higher priority + for (const setting of loadedSettings) { + const [key, value] = await this.#processKeyValues(setting); + keyValues.push([key, value]); + } + + this.#clearLoadedKeyValues(); // clear existing key-values in case of configuration setting deletion + for (const [k, v] of keyValues) { + this.#configMap.set(k, v); // reset the configuration + } + } + + /** + * Updates etag of watched settings from loaded data. If a watched setting is not covered by any selector, a request will be sent to retrieve it. + */ + async #updateWatchedKeyValuesEtag(existingSettings: ConfigurationSetting[]): Promise { + for (const sentinel of this.#sentinels) { + const matchedSetting = existingSettings.find(s => s.key === sentinel.key && s.label === sentinel.label); + if (matchedSetting) { + sentinel.etag = matchedSetting.etag; + } else { + // Send a request to retrieve key-value since it may be either not loaded or loaded with a different label or different casing + const { key, label } = sentinel; + const response = await this.#getConfigurationSetting({ key, label }); + if (response) { + sentinel.etag = response.etag; + } else { + sentinel.etag = undefined; + } + } + } + } + + /** + * Clears all existing key-values in the local configuration except feature flags. + */ + async #clearLoadedKeyValues() { + for (const key of this.#configMap.keys()) { + if (key !== FEATURE_MANAGEMENT_KEY_NAME) { + this.#configMap.delete(key); + } + } + } + + /** + * Loads feature flags from App Configuration to the local configuration. + */ + async #loadFeatureFlags() { + const loadFeatureFlag = true; + const featureFlagSettings = await this.#loadConfigurationSettings(loadFeatureFlag); + + // parse feature flags + const featureFlags = await Promise.all( + featureFlagSettings.map(setting => this.#parseFeatureFlag(setting)) + ); + + // feature_management is a reserved key, and feature_flags is an array of feature flags + this.#configMap.set(FEATURE_MANAGEMENT_KEY_NAME, { [FEATURE_FLAGS_KEY_NAME]: featureFlags }); + } + + /** + * Refreshes key-values. * @returns true if key-values are refreshed, false otherwise. */ async #refreshKeyValues(): Promise { // if still within refresh interval/backoff, return - if (!this.#refreshTimer.canRefresh()) { + if (!this.#kvRefreshTimer.canRefresh()) { return Promise.resolve(false); } // try refresh if any of watched settings is changed. let needRefresh = false; + if (this.#watchAll) { + needRefresh = await this.#checkConfigurationSettingsChange(this.#kvSelectors); + } for (const sentinel of this.#sentinels.values()) { const response = await this.#getConfigurationSetting(sentinel, { onlyIfChanged: true @@ -521,25 +499,39 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { await this.#loadSelectedAndWatchedKeyValues(); } - this.#refreshTimer.reset(); + this.#kvRefreshTimer.reset(); return Promise.resolve(needRefresh); } /** - * Refresh feature flags. + * Refreshes feature flags. * @returns true if feature flags are refreshed, false otherwise. */ async #refreshFeatureFlags(): Promise { // if still within refresh interval/backoff, return - if (!this.#featureFlagRefreshTimer.canRefresh()) { + if (!this.#ffRefreshTimer.canRefresh()) { return Promise.resolve(false); } - // check if any feature flag is changed + const needRefresh = await this.#checkConfigurationSettingsChange(this.#ffSelectors); + if (needRefresh) { + await this.#loadFeatureFlags(); + } + + this.#ffRefreshTimer.reset(); + return Promise.resolve(needRefresh); + } + + /** + * Checks whether the key-value collection has changed. + * @param selectors - The @see PagedSettingSelector of the kev-value collection. + * @returns true if key-value collection has changed, false otherwise. + */ + async #checkConfigurationSettingsChange(selectors: PagedSettingSelector[]): Promise { const funcToExecute = async (client) => { - for (const selector of this.#featureFlagSelectors) { + for (const selector of selectors) { const listOptions: ListConfigurationSettingsOptions = { - keyFilter: `${featureFlagPrefix}${selector.keyFilter}`, + keyFilter: selector.keyFilter, labelFilter: selector.labelFilter, pageEtags: selector.pageEtags }; @@ -559,30 +551,76 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { return false; }; - const needRefresh: boolean = await this.#executeWithFailoverPolicy(funcToExecute); - if (needRefresh) { - await this.#loadFeatureFlags(); - } + const isChanged = await this.#executeWithFailoverPolicy(funcToExecute); + return isChanged; + } - this.#featureFlagRefreshTimer.reset(); - return Promise.resolve(needRefresh); + /** + * Gets 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 { + const funcToExecute = async (client) => { + return getConfigurationSettingWithTrace( + this.#requestTraceOptions, + client, + configurationSettingId, + customOptions + ); + }; + + let response: GetConfigurationSettingResponse | undefined; + try { + response = await this.#executeWithFailoverPolicy(funcToExecute); + } catch (error) { + if (isRestError(error) && error.statusCode === 404) { + response = undefined; + } else { + throw error; + } + } + return response; } - onRefresh(listener: () => any, thisArg?: any): Disposable { - if (!this.#refreshEnabled && !this.#featureFlagRefreshEnabled) { - throw new Error("Refresh is not enabled for key-values or feature flags."); + async #executeWithFailoverPolicy(funcToExecute: (client: AppConfigurationClient) => Promise): Promise { + let clientWrappers = await this.#clientManager.getClients(); + if (this.#options?.loadBalancingEnabled && this.#lastSuccessfulEndpoint !== "" && clientWrappers.length > 1) { + let nextClientIndex = 0; + // Iterate through clients to find the index of the client with the last successful endpoint + for (const clientWrapper of clientWrappers) { + nextClientIndex++; + if (clientWrapper.endpoint === this.#lastSuccessfulEndpoint) { + break; + } + } + // If we found the last successful client, rotate the list so that the next client is at the beginning + if (nextClientIndex < clientWrappers.length) { + clientWrappers = [...clientWrappers.slice(nextClientIndex), ...clientWrappers.slice(0, nextClientIndex)]; + } } - const boundedListener = listener.bind(thisArg); - this.#onRefreshListeners.push(boundedListener); + let successful: boolean; + for (const clientWrapper of clientWrappers) { + successful = false; + try { + const result = await funcToExecute(clientWrapper.client); + this.#isFailoverRequest = false; + this.#lastSuccessfulEndpoint = clientWrapper.endpoint; + successful = true; + clientWrapper.updateBackoffStatus(successful); + return result; + } catch (error) { + if (isFailoverableError(error)) { + clientWrapper.updateBackoffStatus(successful); + this.#isFailoverRequest = true; + continue; + } - const remove = () => { - const index = this.#onRefreshListeners.indexOf(boundedListener); - if (index >= 0) { - this.#onRefreshListeners.splice(index, 1); + throw error; } - }; - return new Disposable(remove); + } + + this.#clientManager.refreshClients(); + throw new Error("All clients failed to get configuration settings."); } async #processKeyValues(setting: ConfigurationSetting): Promise<[string, unknown]> { @@ -611,32 +649,6 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { return key; } - /** - * 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 { - const funcToExecute = async (client) => { - return getConfigurationSettingWithTrace( - this.#requestTraceOptions, - client, - configurationSettingId, - customOptions - ); - }; - - let response: GetConfigurationSettingResponse | undefined; - try { - response = await this.#executeWithFailoverPolicy(funcToExecute); - } catch (error) { - if (isRestError(error) && error.statusCode === 404) { - response = undefined; - } else { - throw error; - } - } - return response; - } - async #parseFeatureFlag(setting: ConfigurationSetting): Promise { const rawFlag = setting.value; if (rawFlag === undefined) { @@ -877,7 +889,7 @@ function getValidSelectors(selectors: SettingSelector[]): SettingSelector[] { } function getValidKeyValueSelectors(selectors?: SettingSelector[]): SettingSelector[] { - if (!selectors || selectors.length === 0) { + if (selectors === undefined || selectors.length === 0) { // Default selector: key: *, label: \0 return [{ keyFilter: KeyFilter.Any, labelFilter: LabelFilter.Null }]; } @@ -885,10 +897,13 @@ function getValidKeyValueSelectors(selectors?: SettingSelector[]): SettingSelect } function getValidFeatureFlagSelectors(selectors?: SettingSelector[]): SettingSelector[] { - if (!selectors || selectors.length === 0) { + if (selectors === undefined || selectors.length === 0) { // selectors must be explicitly provided. throw new Error("Feature flag selectors must be provided."); } else { + selectors.forEach(selector => { + selector.keyFilter = `${featureFlagPrefix}${selector.keyFilter}`; + }); return getValidSelectors(selectors); } } diff --git a/src/RefreshOptions.ts b/src/RefreshOptions.ts index 37425112..d5e4da5f 100644 --- a/src/RefreshOptions.ts +++ b/src/RefreshOptions.ts @@ -22,6 +22,9 @@ export interface RefreshOptions { /** * One or more configuration settings to be watched for changes on the server. * Any modifications to watched settings will refresh all settings loaded by the configuration provider when refresh() is called. + * + * @remarks + * If no watched setting is specified, all configuration settings will be watched. */ watchedSettings?: WatchedSetting[]; } diff --git a/test/refresh.test.ts b/test/refresh.test.ts index 6c537fb4..6457cb1a 100644 --- a/test/refresh.test.ts +++ b/test/refresh.test.ts @@ -58,25 +58,6 @@ describe("dynamic refresh", function () { return expect(refreshCall).eventually.rejectedWith("Refresh is not enabled for key-values or feature flags."); }); - it("should only allow non-empty list of watched settings when refresh is enabled", async () => { - const connectionString = createMockedConnectionString(); - const loadWithEmptyWatchedSettings = load(connectionString, { - refreshOptions: { - enabled: true, - watchedSettings: [] - } - }); - const loadWithUndefinedWatchedSettings = load(connectionString, { - refreshOptions: { - enabled: true - } - }); - return Promise.all([ - expect(loadWithEmptyWatchedSettings).eventually.rejectedWith("Refresh is enabled but no watched settings are specified."), - expect(loadWithUndefinedWatchedSettings).eventually.rejectedWith("Refresh is enabled but no watched settings are specified.") - ]); - }); - it("should not allow refresh interval less than 1 second", async () => { const connectionString = createMockedConnectionString(); const loadWithInvalidRefreshInterval = load(connectionString, { @@ -354,6 +335,73 @@ describe("dynamic refresh", function () { expect(settings.get("app.settings.fontColor")).eq("red"); }); + it("should refresh key value based on page eTag, if no watched setting is specified", async () => { + const connectionString = createMockedConnectionString(); + const settings = await load(connectionString, { + refreshOptions: { + enabled: true, + refreshIntervalInMs: 2000 + } + }); + expect(listKvRequestCount).eq(1); + expect(getKvRequestCount).eq(0); + expect(settings).not.undefined; + expect(settings.get("app.settings.fontColor")).eq("red"); + expect(settings.get("app.settings.fontSize")).eq("40"); + + // change setting + updateSetting("app.settings.fontColor", "blue"); + + // after refreshInterval, should really refresh + await sleepInMs(2 * 1000 + 1); + await settings.refresh(); + expect(listKvRequestCount).eq(3); // 1 + 2 more requests: one conditional request to detect change and one request to reload all key values + expect(getKvRequestCount).eq(0); + expect(settings.get("app.settings.fontColor")).eq("blue"); + }); + + it("should refresh key value based on page Etag, only on change", async () => { + const connectionString = createMockedConnectionString(); + const settings = await load(connectionString, { + refreshOptions: { + enabled: true, + refreshIntervalInMs: 2000 + } + }); + expect(listKvRequestCount).eq(1); + expect(getKvRequestCount).eq(0); + + let refreshSuccessfulCount = 0; + settings.onRefresh(() => { + refreshSuccessfulCount++; + }); + + expect(settings).not.undefined; + expect(settings.get("app.settings.fontColor")).eq("red"); + + await sleepInMs(2 * 1000 + 1); + await settings.refresh(); + expect(listKvRequestCount).eq(2); // one more conditional request to detect change + expect(getKvRequestCount).eq(0); + expect(refreshSuccessfulCount).eq(0); // no change in key values, because page etags are the same. + + // change key value + restoreMocks(); + const changedKVs = [ + { value: "blue", key: "app.settings.fontColor" }, + { value: "40", key: "app.settings.fontSize" } + ].map(createMockedKeyValue); + mockAppConfigurationClientListConfigurationSettings([changedKVs], listKvCallback); + mockAppConfigurationClientGetConfigurationSetting(changedKVs, getKvCallback); + + await sleepInMs(2 * 1000 + 1); + await settings.refresh(); + expect(listKvRequestCount).eq(4); // 2 + 2 more requests: one conditional request to detect change and one request to reload all key values + expect(getKvRequestCount).eq(0); + expect(refreshSuccessfulCount).eq(1); // change in key values, because page etags are different. + expect(settings.get("app.settings.fontColor")).eq("blue"); + }); + it("should not refresh any more when there is refresh in progress", async () => { const connectionString = createMockedConnectionString(); const settings = await load(connectionString, { @@ -449,7 +497,7 @@ describe("dynamic refresh feature flags", function () { }); - it("should refresh feature flags only on change, based on page etags", async () => { + it("should refresh feature flags based on page etags, only on change", async () => { // mock multiple pages of feature flags const page1 = [ createMockedFeatureFlag("Alpha_1", { enabled: true }), From 8600654aed7974ae7bb0a5521ebb62bc1584635d Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Thu, 19 Dec 2024 18:47:11 +0800 Subject: [PATCH 31/35] fix timeout (#144) --- src/ConfigurationClientManager.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ConfigurationClientManager.ts b/src/ConfigurationClientManager.ts index b11e9350..6510b7ff 100644 --- a/src/ConfigurationClientManager.ts +++ b/src/ConfigurationClientManager.ts @@ -140,13 +140,16 @@ export class ConfigurationClientManager { async #discoverFallbackClients(host: string) { let result; + let timeout; try { result = await Promise.race([ - new Promise((_, reject) => setTimeout(() => reject(new Error("SRV record query timed out.")), SRV_QUERY_TIMEOUT)), + new Promise((_, reject) => timeout = setTimeout(() => reject(new Error("SRV record query timed out.")), SRV_QUERY_TIMEOUT)), this.#querySrvTargetHost(host) ]); } catch (error) { throw new Error(`Failed to build fallback clients, ${error.message}`); + } finally { + clearTimeout(timeout); } const srvTargetHosts = shuffleList(result) as string[]; From f5d48b24ed7fe12a201c9a30a6216b186840d2e0 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Tue, 7 Jan 2025 16:19:57 +0800 Subject: [PATCH 32/35] version bump 2.0.0-preview.2 (#146) --- package-lock.json | 4 ++-- package.json | 2 +- src/version.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 44b53a05..2b821f0f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@azure/app-configuration-provider", - "version": "2.0.0-preview.1", + "version": "2.0.0-preview.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@azure/app-configuration-provider", - "version": "2.0.0-preview.1", + "version": "2.0.0-preview.2", "license": "MIT", "dependencies": { "@azure/app-configuration": "^1.6.1", diff --git a/package.json b/package.json index 0842f457..16148a76 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@azure/app-configuration-provider", - "version": "2.0.0-preview.1", + "version": "2.0.0-preview.2", "description": "The JavaScript configuration provider for Azure App Configuration", "main": "dist/index.js", "module": "./dist-esm/index.js", diff --git a/src/version.ts b/src/version.ts index beea578c..bb9c7aa8 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,4 +1,4 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -export const VERSION = "2.0.0-preview.1"; +export const VERSION = "2.0.0-preview.2"; From 5956b72ab99f640eaa9f95880201b2a9356bf4b3 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Mon, 10 Feb 2025 10:42:14 +0800 Subject: [PATCH 33/35] load all feature flag with no label by default (#158) --- src/AzureAppConfigurationImpl.ts | 4 +-- src/featureManagement/FeatureFlagOptions.ts | 5 ++-- test/featureFlag.test.ts | 29 ++++++++++----------- test/utils/testHelper.ts | 4 +-- 4 files changed, 20 insertions(+), 22 deletions(-) diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index 916ece49..6d7a4a98 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -898,8 +898,8 @@ function getValidKeyValueSelectors(selectors?: SettingSelector[]): SettingSelect function getValidFeatureFlagSelectors(selectors?: SettingSelector[]): SettingSelector[] { if (selectors === undefined || selectors.length === 0) { - // selectors must be explicitly provided. - throw new Error("Feature flag selectors must be provided."); + // Default selector: key: *, label: \0 + return [{ keyFilter: `${featureFlagPrefix}${KeyFilter.Any}`, labelFilter: LabelFilter.Null }]; } else { selectors.forEach(selector => { selector.keyFilter = `${featureFlagPrefix}${selector.keyFilter}`; diff --git a/src/featureManagement/FeatureFlagOptions.ts b/src/featureManagement/FeatureFlagOptions.ts index eedf9ec7..55ceda4d 100644 --- a/src/featureManagement/FeatureFlagOptions.ts +++ b/src/featureManagement/FeatureFlagOptions.ts @@ -10,16 +10,15 @@ import { SettingSelector } from "../types.js"; export interface FeatureFlagOptions { /** * Specifies whether feature flags will be loaded from Azure App Configuration. - */ enabled: boolean; /** - * Specifies the selectors used to filter feature flags. + * Specifies what feature flags to include in the configuration provider. * * @remarks * keyFilter of selector will be prefixed with "appconfig.featureflag/" when request is sent. - * If no selectors are specified then no feature flags will be retrieved. + * If no selectors are specified then all feature flags with no label will be included. */ selectors?: SettingSelector[]; diff --git a/test/featureFlag.test.ts b/test/featureFlag.test.ts index 2544e617..2d6a7e02 100644 --- a/test/featureFlag.test.ts +++ b/test/featureFlag.test.ts @@ -3,6 +3,7 @@ import * as chai from "chai"; import * as chaiAsPromised from "chai-as-promised"; +import { featureFlagContentType } from "@azure/app-configuration"; import { load } from "./exportedApi.js"; import { MAX_TIME_OUT, createMockedConnectionString, createMockedEndpoint, createMockedFeatureFlag, createMockedKeyValue, mockAppConfigurationClientListConfigurationSettings, restoreMocks } from "./utils/testHelper.js"; chai.use(chaiAsPromised); @@ -49,9 +50,9 @@ const mockedKVs = [{ }, { key: ".appconfig.featureflag/variant", value: sampleVariantValue, - contentType: "application/vnd.microsoft.appconfig.ff+json;charset=utf-8", + contentType: featureFlagContentType, }].map(createMockedKeyValue).concat([ - createMockedFeatureFlag("Beta", { enabled: true }), + createMockedFeatureFlag("FlagWithTestLabel", { enabled: true }, {label: "Test"}), createMockedFeatureFlag("Alpha_1", { enabled: true }), createMockedFeatureFlag("Alpha_2", { enabled: false }), createMockedFeatureFlag("Telemetry_1", { enabled: true, telemetry: { enabled: true } }, { etag: "ETag"}), @@ -213,15 +214,22 @@ describe("feature flags", function () { const connectionString = createMockedConnectionString(); const settings = await load(connectionString, { featureFlagOptions: { - enabled: true, - selectors: [{ - keyFilter: "*" - }] + enabled: true } }); expect(settings).not.undefined; expect(settings.get("feature_management")).not.undefined; expect(settings.get("feature_management").feature_flags).not.undefined; + // it should only load feature flags with no label by default + expect((settings.get("feature_management").feature_flags as any[]).find(ff => ff.id === "FlagWithTestLabel")).to.be.undefined; + + const settings2 = await load(connectionString, { + featureFlagOptions: { + enabled: true, + selectors: [ { keyFilter: "*", labelFilter: "Test" } ] + } + }); + expect((settings2.get("feature_management").feature_flags as any[]).find(ff => ff.id === "FlagWithTestLabel")).not.undefined; }); it("should not load feature flags if disabled", async () => { @@ -242,15 +250,6 @@ describe("feature flags", function () { expect(settings.get("feature_management")).undefined; }); - it("should throw error if selectors not specified", async () => { - const connectionString = createMockedConnectionString(); - return expect(load(connectionString, { - featureFlagOptions: { - enabled: true - } - })).eventually.rejectedWith("Feature flag selectors must be provided."); - }); - it("should load feature flags with custom selector", async () => { const connectionString = createMockedConnectionString(); const settings = await load(connectionString, { diff --git a/test/utils/testHelper.ts b/test/utils/testHelper.ts index a5812694..85f7ac80 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, ConfigurationSetting, featureFlagContentType } from "@azure/app-configuration"; import { ClientSecretCredential } from "@azure/identity"; import { KeyVaultSecret, SecretClient } from "@azure/keyvault-secrets"; import * as uuid from "uuid"; @@ -240,7 +240,7 @@ const createMockedFeatureFlag = (name: string, flagProps?: any, props?: any) => "client_filters": [] } }, flagProps)), - contentType: "application/vnd.microsoft.appconfig.ff+json;charset=utf-8", + contentType: featureFlagContentType, lastModified: new Date(), tags: {}, etag: uuid.v4(), From ca7c5bbfe6a435ca8cb028fcd938e7cd0c1125d9 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Mon, 10 Feb 2025 17:10:04 +0800 Subject: [PATCH 34/35] Add feature management package version tracing (#153) * add fm package version tracing * update comment * update --- rollup.config.mjs | 2 +- src/AzureAppConfigurationImpl.ts | 21 ++++++++++++++++++++- src/ConfigurationClientManager.ts | 3 +++ src/requestTracing/constants.ts | 4 ++++ src/requestTracing/utils.ts | 7 ++++++- 5 files changed, 34 insertions(+), 3 deletions(-) diff --git a/rollup.config.mjs b/rollup.config.mjs index 8ad51640..1fa9626f 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -4,7 +4,7 @@ import dts from "rollup-plugin-dts"; export default [ { - external: ["@azure/app-configuration", "@azure/keyvault-secrets", "@azure/core-rest-pipeline", "crypto", "dns/promises"], + external: ["@azure/app-configuration", "@azure/keyvault-secrets", "@azure/core-rest-pipeline", "crypto", "dns/promises", "@microsoft/feature-management"], input: "src/index.ts", output: [ { diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index 6d7a4a98..b89e7a81 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -33,6 +33,7 @@ import { CONDITIONS_KEY_NAME, CLIENT_FILTERS_KEY_NAME } from "./featureManagement/constants.js"; +import { FM_PACKAGE_NAME } from "./requestTracing/constants.js"; import { AzureKeyVaultKeyValueAdapter } from "./keyvault/AzureKeyVaultKeyValueAdapter.js"; import { RefreshTimer } from "./refresh/RefreshTimer.js"; import { RequestTracingOptions, getConfigurationSettingWithTrace, listConfigurationSettingsWithTrace, requestTracingEnabled } from "./requestTracing/utils.js"; @@ -65,6 +66,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { #isInitialLoadCompleted: boolean = false; #isFailoverRequest: boolean = false; #featureFlagTracing: FeatureFlagTracingOptions | undefined; + #fmVersion: string | undefined; // Refresh #refreshInProgress: boolean = false; @@ -184,7 +186,8 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { initialLoadCompleted: this.#isInitialLoadCompleted, replicaCount: this.#clientManager.getReplicaCount(), isFailoverRequest: this.#isFailoverRequest, - featureFlagTracing: this.#featureFlagTracing + featureFlagTracing: this.#featureFlagTracing, + fmVersion: this.#fmVersion }; } @@ -226,6 +229,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { * Loads the configuration store for the first time. */ async load() { + await this.#inspectFmPackage(); await this.#loadSelectedAndWatchedKeyValues(); if (this.#featureFlagEnabled) { await this.#loadFeatureFlags(); @@ -316,6 +320,21 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { return new Disposable(remove); } + /** + * Inspects the feature management package version. + */ + async #inspectFmPackage() { + if (this.#requestTracingEnabled && !this.#fmVersion) { + try { + // get feature management package version + const fmPackage = await import(FM_PACKAGE_NAME); + this.#fmVersion = fmPackage?.VERSION; + } catch (error) { + // ignore the error + } + } + } + async #refreshTasks(): Promise { const refreshTasks: Promise[] = []; if (this.#refreshEnabled) { diff --git a/src/ConfigurationClientManager.ts b/src/ConfigurationClientManager.ts index 92e16a3a..7ad8e597 100644 --- a/src/ConfigurationClientManager.ts +++ b/src/ConfigurationClientManager.ts @@ -85,6 +85,9 @@ export class ConfigurationClientManager { this.#isFailoverable = false; return; } + if (this.#dns) { + return; + } try { this.#dns = await import("dns/promises"); diff --git a/src/requestTracing/constants.ts b/src/requestTracing/constants.ts index 5a88b0fc..74ca58bb 100644 --- a/src/requestTracing/constants.ts +++ b/src/requestTracing/constants.ts @@ -55,6 +55,10 @@ export const FAILOVER_REQUEST_TAG = "Failover"; export const FEATURES_KEY = "Features"; export const LOAD_BALANCE_CONFIGURED_TAG = "LB"; +// Feature management package +export const FM_PACKAGE_NAME = "@microsoft/feature-management"; +export const FM_VERSION_KEY = "FMJsVer"; + // Feature flag usage tracing export const FEATURE_FILTER_TYPE_KEY = "Filter"; export const CUSTOM_FILTER_KEY = "CSTM"; diff --git a/src/requestTracing/utils.ts b/src/requestTracing/utils.ts index b56c460c..2e8b1124 100644 --- a/src/requestTracing/utils.ts +++ b/src/requestTracing/utils.ts @@ -27,7 +27,8 @@ import { REPLICA_COUNT_KEY, FAILOVER_REQUEST_TAG, FEATURES_KEY, - LOAD_BALANCE_CONFIGURED_TAG + LOAD_BALANCE_CONFIGURED_TAG, + FM_VERSION_KEY } from "./constants"; export interface RequestTracingOptions { @@ -37,6 +38,7 @@ export interface RequestTracingOptions { replicaCount: number; isFailoverRequest: boolean; featureFlagTracing: FeatureFlagTracingOptions | undefined; + fmVersion: string | undefined; } // Utils @@ -119,6 +121,9 @@ export function createCorrelationContextHeader(requestTracingOptions: RequestTra if (requestTracingOptions.replicaCount > 0) { keyValues.set(REPLICA_COUNT_KEY, requestTracingOptions.replicaCount.toString()); } + if (requestTracingOptions.fmVersion) { + keyValues.set(FM_VERSION_KEY, requestTracingOptions.fmVersion); + } // Compact tags: Features=LB+... if (appConfigOptions?.loadBalancingEnabled) { From 07026ac7a080d3ef9ddae651494ca6f26e15466c Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Wed, 12 Feb 2025 12:52:15 +0800 Subject: [PATCH 35/35] unify code style (#159) --- src/AzureAppConfigurationImpl.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index b89e7a81..90e283f0 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -919,12 +919,11 @@ function getValidFeatureFlagSelectors(selectors?: SettingSelector[]): SettingSel if (selectors === undefined || selectors.length === 0) { // Default selector: key: *, label: \0 return [{ keyFilter: `${featureFlagPrefix}${KeyFilter.Any}`, labelFilter: LabelFilter.Null }]; - } else { - selectors.forEach(selector => { - selector.keyFilter = `${featureFlagPrefix}${selector.keyFilter}`; - }); - return getValidSelectors(selectors); } + selectors.forEach(selector => { + selector.keyFilter = `${featureFlagPrefix}${selector.keyFilter}`; + }); + return getValidSelectors(selectors); } function isFailoverableError(error: any): boolean {