From 1f85e6a06631f986fbae4931d3f042a836c5e0bc Mon Sep 17 00:00:00 2001 From: Kwame Efah <37164746+efahk@users.noreply.github.com> Date: Wed, 5 Nov 2025 00:20:33 +0000 Subject: [PATCH 01/15] Add initial support for feature flags --- history.md | 4 + lib/flags/index.js | 12 + lib/flags/local_flags.d.ts | 161 ++++++++++++ lib/flags/local_flags.js | 411 +++++++++++++++++++++++++++++ lib/flags/remote_flags.d.ts | 83 ++++++ lib/flags/remote_flags.js | 246 ++++++++++++++++++ lib/flags/types.d.ts | 132 ++++++++++ lib/flags/utils.js | 82 ++++++ lib/mixpanel-node.d.ts | 15 ++ lib/mixpanel-node.js | 18 ++ package-lock.json | 174 +++++++++++++ package.json | 1 + test/flags/local_flags.js | 506 ++++++++++++++++++++++++++++++++++++ test/flags/remote_flags.js | 445 +++++++++++++++++++++++++++++++ vitest.config.mjs | 2 +- 15 files changed, 2291 insertions(+), 1 deletion(-) create mode 100644 lib/flags/index.js create mode 100644 lib/flags/local_flags.d.ts create mode 100644 lib/flags/local_flags.js create mode 100644 lib/flags/remote_flags.d.ts create mode 100644 lib/flags/remote_flags.js create mode 100644 lib/flags/types.d.ts create mode 100644 lib/flags/utils.js create mode 100644 test/flags/local_flags.js create mode 100644 test/flags/remote_flags.js diff --git a/history.md b/history.md index 98b0e7a..aeef7d1 100644 --- a/history.md +++ b/history.md @@ -1,3 +1,7 @@ +0.19.0 / 2025-03-12 +================== +* adds initial support for feature flags + 0.18.1 / 2025-03-12 ================== * add secret to config types (thanks gierschv) diff --git a/lib/flags/index.js b/lib/flags/index.js new file mode 100644 index 0000000..f6dfd3a --- /dev/null +++ b/lib/flags/index.js @@ -0,0 +1,12 @@ +/** + * Mixpanel Feature Flags + * Exports for local and remote feature flag evaluation + */ + +const LocalFeatureFlagsProvider = require('./local_flags'); +const RemoteFeatureFlagsProvider = require('./remote_flags'); + +module.exports = { + LocalFeatureFlagsProvider, + RemoteFeatureFlagsProvider, +}; diff --git a/lib/flags/local_flags.d.ts b/lib/flags/local_flags.d.ts new file mode 100644 index 0000000..a3c892b --- /dev/null +++ b/lib/flags/local_flags.d.ts @@ -0,0 +1,161 @@ +/** + * TypeScript definitions for Local Feature Flags Provider + */ + +import { LocalFlagsConfig, FlagContext, SelectedVariant } from './types'; + +/** + * Local Feature Flags Provider + * Evaluates feature flags client-side using locally cached definitions + */ +export default class LocalFeatureFlagsProvider { + constructor( + token: string, + config: LocalFlagsConfig, + tracker: (distinct_id: string, event: string, properties: object, callback: (err?: Error) => void) => void + ); + + /** + * Start polling for flag definitions + * Fetches immediately and then at regular intervals if polling is enabled + * @param callback - Optional callback (err) + */ + startPolling(callback?: (err: Error | null) => void): void; + + /** + * Stop polling for flag definitions + */ + stopPolling(): void; + + /** + * Get the variant value for a feature flag (callback mode) + * @param flagKey - Feature flag key + * @param fallbackValue - Value to return if flag evaluation fails + * @param context - Evaluation context (must include distinct_id) + * @param reportExposure - Whether to track exposure event (default: true) + * @param callback - Callback function (err, value) + */ + getVariantValue( + flagKey: string, + fallbackValue: T, + context: FlagContext, + reportExposure: boolean, + callback: (err: Error | null, value: T) => void + ): void; + + /** + * Get the variant value for a feature flag (callback mode without reportExposure) + * @param flagKey - Feature flag key + * @param fallbackValue - Value to return if flag evaluation fails + * @param context - Evaluation context (must include distinct_id) + * @param callback - Callback function (err, value) + */ + getVariantValue( + flagKey: string, + fallbackValue: T, + context: FlagContext, + callback: (err: Error | null, value: T) => void + ): void; + + /** + * Get the variant value for a feature flag (synchronous mode) + * @param flagKey - Feature flag key + * @param fallbackValue - Value to return if flag evaluation fails + * @param context - Evaluation context (must include distinct_id) + * @param reportExposure - Whether to track exposure event (default: true) + */ + getVariantValue( + flagKey: string, + fallbackValue: T, + context: FlagContext, + reportExposure?: boolean + ): T; + + /** + * Get the complete variant information for a feature flag (callback mode) + * @param flagKey - Feature flag key + * @param fallbackVariant - Variant to return if flag evaluation fails + * @param context - Evaluation context (must include distinct_id) + * @param reportExposure - Whether to track exposure event (default: true) + * @param callback - Callback function (err, selectedVariant) + */ + getVariant( + flagKey: string, + fallbackVariant: SelectedVariant, + context: FlagContext, + reportExposure: boolean, + callback: (err: Error | null, variant: SelectedVariant) => void + ): void; + + /** + * Get the complete variant information for a feature flag (callback mode without reportExposure) + * @param flagKey - Feature flag key + * @param fallbackVariant - Variant to return if flag evaluation fails + * @param context - Evaluation context (must include distinct_id) + * @param callback - Callback function (err, selectedVariant) + */ + getVariant( + flagKey: string, + fallbackVariant: SelectedVariant, + context: FlagContext, + callback: (err: Error | null, variant: SelectedVariant) => void + ): void; + + /** + * Get the complete variant information for a feature flag (synchronous mode) + * @param flagKey - Feature flag key + * @param fallbackVariant - Variant to return if flag evaluation fails + * @param context - Evaluation context (must include distinct_id) + * @param reportExposure - Whether to track exposure event (default: true) + */ + getVariant( + flagKey: string, + fallbackVariant: SelectedVariant, + context: FlagContext, + reportExposure?: boolean + ): SelectedVariant; + + /** + * Check if a feature flag is enabled (callback mode) + * @param flagKey - Feature flag key + * @param context - Evaluation context (must include distinct_id) + * @param callback - Callback function (err, isEnabled) + */ + isEnabled( + flagKey: string, + context: FlagContext, + callback: (err: Error | null, isEnabled: boolean) => void + ): void; + + /** + * Check if a feature flag is enabled (synchronous mode) + * @param flagKey - Feature flag key + * @param context - Evaluation context (must include distinct_id) + */ + isEnabled( + flagKey: string, + context: FlagContext + ): boolean; + + /** + * Get all feature flag variants for the current user context + * Exposure events are not automatically tracked when this method is used + * @param context - Evaluation context (must include distinct_id) + */ + getAllVariants( + context: FlagContext + ): {[key: string]: SelectedVariant}; + + /** + * Manually tracks a feature flag exposure event to Mixpanel + * This provides flexibility for reporting individual exposure events when using getAllVariants + * @param flagKey - The key of the feature flag + * @param variant - The selected variant for the feature flag + * @param context - The user context used to evaluate the feature flag + */ + trackExposureEvent( + flagKey: string, + variant: SelectedVariant, + context: FlagContext + ): void; +} diff --git a/lib/flags/local_flags.js b/lib/flags/local_flags.js new file mode 100644 index 0000000..2271beb --- /dev/null +++ b/lib/flags/local_flags.js @@ -0,0 +1,411 @@ +/** + * Local Feature Flags Provider + * Evaluates feature flags client-side using locally cached definitions + */ + +/** + * @typedef {import('./types').SelectedVariant} SelectedVariant + * @typedef {import('./types').FlagContext} FlagContext + * @typedef {import('./types').LocalFlagsConfig} LocalFlagsConfig + * @typedef {import('./types').ExperimentationFlag} ExperimentationFlag + * @typedef {import('./types').LocalFlagsResponse} LocalFlagsResponse + * */ + +const https = require('https'); +const packageInfo = require('../../package.json'); +const { + EXPOSURE_EVENT, + REQUEST_HEADERS, + prepareCommonQueryParams, + normalizedHash, +} = require('./utils'); + +class LocalFeatureFlagsProvider { + /** + * @param {string} token - Mixpanel project token + * @param {LocalFlagsConfig} config - Local flags configuration + * @param {Function} tracker - Function to track events (signature: track(distinct_id, event, properties, callback)) + */ + constructor(token, config, tracker) { + this.token = token; + this.config = { + api_host: 'api.mixpanel.com', + request_timeout_in_seconds: 10, + enable_polling: true, + polling_interval_in_seconds: 60, + ...config, + }; + this.tracker = tracker; + this.logger = config.logger; + + this.flagDefinitions = new Map(); + this.pollingInterval = null; + } + + /** + * Start polling for flag definitions. + * Fetches immediately and then at regular intervals if polling is enabled + * @returns {Promise} + */ + async startPollingForDefinitions() { + try { + await this._fetchFlagDefinitions(); + + if (this.config.enable_polling && !this.pollingInterval) { + this.pollingInterval = setInterval(async () => { + try { + await this._fetchFlagDefinitions(); + } catch (err) { + this.logger?.error(`Error polling for flag definition: ${err.message}`); + } + }, this.config.polling_interval_in_seconds * 1000); + } + } catch (err) { + this.logger?.error(`Initial flag definitions fetch failed: ${err.message}`); + } + } + + /** + * Stop polling for flag definitions + */ + stopPollingForDefinitions() { + if (this.pollingInterval) { + clearInterval(this.pollingInterval); + this.pollingInterval = null; + } else { + this.logger?.warn('stopPollingForDefinitions called but polling was not active'); + } + } + + /** + * Check if a feature flag is enabled + * This method is intended only for flags defined as Mixpanel Feature Gates (boolean flags) + * @param {string} flagKey - Feature flag key + * @param {FlagContext} context - Evaluation context (must include distinct_id) + * @returns {boolean} + */ + isEnabled(flagKey, context) { + const value = this.getVariantValue(flagKey, false, context); + return value === true; + } + + /** + * Get the variant value for a feature flag + * @param {string} flagKey - Feature flag key + * @param {*} fallbackValue - Value to return if the user context is not in the rollout for a flag or if evaluation fails + * @param {FlagContext} context - Evaluation context + * @param {boolean} [reportExposure=true] - Whether to track exposure event + * @returns {*} The variant value + */ + getVariantValue(flagKey, fallbackValue, context, reportExposure = true) { + const result = this.getVariant(flagKey, { variant_value: fallbackValue }, context, reportExposure); + return result.variant_value; + } + + /** + * Get the complete variant information for a feature flag + * @param {string} flagKey - Feature flag key + * @param {SelectedVariant} fallbackVariant - Variant to return if flag evaluation fails + * @param {FlagContext} context - Evaluation context (must include distinct_id) + * @param {boolean} [reportExposure=true] - Whether to track exposure event + * @returns {SelectedVariant} + */ + getVariant(flagKey, fallbackVariant, context, reportExposure = true) { + const flag = this.flagDefinitions.get(flagKey); + + if (!flag) { + this.logger?.warn(`Cannot find flag definition for key: '${flagKey}`); + return fallbackVariant; + } + + const contextValue = context[flag.context]; + if (!contextValue) { + this.logger?.warn( + `The context, '${flag.context}' for flag, '${flagKey}' is not present in the supplied context dictionary` + ); + return fallbackVariant; + } + + let selectedVariant = null; + + const testUserVariant = this._getVariantOverrideForTestUser(flag, context); + if (testUserVariant) { + selectedVariant = testUserVariant; + } else { + const rollout = this._getAssignedRollout(flag, contextValue, context); + if (rollout) { + selectedVariant = this._getAssignedVariant(flag, contextValue, flagKey, rollout); + } + } + + if (selectedVariant) { + if (reportExposure) { + this._trackExposure(flagKey, selectedVariant, context); + } + return selectedVariant; + } + + return fallbackVariant; + } + + /** + * Get all feature flag variants for the current user context + * Exposure events are not automatically tracked when this method is used + * @param {FlagContext} context - Evaluation context (must include distinct_id) + * @returns {{[key: string]: SelectedVariant}} + */ + getAllVariants(context) { + const variants = {}; + const fallback = null; + + for (const flagKey of this.flagDefinitions.keys()) { + const variant = this.getVariant(flagKey, fallback, context, false); + if (variant !== fallback) { + variants[flagKey] = variant; + } + } + + return variants; + } + + /** + * Manually tracks a feature flag exposure event to Mixpanel + * This provides flexibility for reporting individual exposure events when using getAllVariants + * @param {string} flagKey - The key of the feature flag + * @param {SelectedVariant} variant - The selected variant for the feature flag + * @param {FlagContext} context - The user context used to evaluate the feature flag + */ + trackExposureEvent(flagKey, variant, context) { + this._trackExposure(flagKey, variant, context); + } + + /** + * Fetch flag definitions from API + * @returns {Promise} + */ + _fetchFlagDefinitions() { + return new Promise((resolve, reject) => { + const commonParams = prepareCommonQueryParams(this.token, packageInfo.version); + const params = new URLSearchParams(commonParams); + + const path = `/flags/definitions?${params.toString()}`; + + const requestOptions = { + host: this.config.api_host, + port: 443, + path: path, + method: 'GET', + headers: { + ...REQUEST_HEADERS, + 'Authorization': 'Basic ' + Buffer.from(this.token + ':').toString('base64'), + }, + timeout: this.config.request_timeout_in_seconds * 1000, + }; + + const request = https.request(requestOptions, (res) => { + let data = ''; + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + if (res.statusCode !== 200) { + this.logger?.error(`HTTP ${res.statusCode} error fetching flag definitions: ${data}`); + return reject(new Error(`HTTP ${res.statusCode}: ${data}`)); + } + + try { + const response = JSON.parse(data); + + const newDefinitions = new Map(); + response.flags.forEach(flag => { + newDefinitions.set(flag.key, flag); + }); + + this.flagDefinitions = newDefinitions; + + resolve(response); + } catch (parseErr) { + this.logger?.error(`Failed to parse JSON response for flag definitions: ${parseErr.message}`); + reject(parseErr); + } + }); + }); + + request.on('error', (err) => { + this.logger?.error(`Network error fetching flag definitions: ${err.message}`); + reject(err); + }); + + request.on('timeout', () => { + this.logger?.error(`Request timeout fetching flag definitions`); + request.destroy(); + reject(new Error('Request timeout')); + }); + + request.end(); + }); + } + + /** + * Find a variant by key (case-insensitive) and return complete SelectedVariant + * @param {string} variantKey - Variant key to find + * @param {ExperimentationFlag} flag - Flag definition + * @returns {SelectedVariant|null} + */ + _getMatchingVariant(variantKey, flag) { + for (const variant of flag.ruleset.variants) { + if (variantKey.toLowerCase() === variant.key.toLowerCase()) { + return { + variant_key: variant.key, + variant_value: variant.value, + experiment_id: flag.experiment_id, + is_experiment_active: flag.is_experiment_active, + is_qa_tester: true, + }; + } + } + return null; + } + + _getVariantOverrideForTestUser(flag, context) { + if (!flag.ruleset.test || !flag.ruleset.test.users) { + return null; + } + + const distinctId = context.distinct_id; + if (!distinctId) { + return null; + } + + const variantKey = flag.ruleset.test.users[distinctId]; + if (!variantKey) { + return null; + } + + return this._getMatchingVariant(variantKey, flag); + } + + _getAssignedRollout(flag, contextValue, context) { + for (let index = 0; index < flag.ruleset.rollout.length; index++) { + const rollout = flag.ruleset.rollout[index]; + + let salt; + if (flag.hash_salt !== null && flag.hash_salt !== undefined) { + salt = flag.key + flag.hash_salt + index.toString(); + } else { + salt = flag.key + "rollout"; + } + + const rolloutHash = normalizedHash(String(contextValue), salt); + + if (rolloutHash < rollout.rollout_percentage && + this._isRuntimeEvaluationSatisfied(rollout, context)) { + return rollout; + } + } + + return null; + } + + _getAssignedVariant(flag, contextValue, flagKey, rollout) { + if (rollout.variant_override) { + const variant = this._getMatchingVariant(rollout.variant_override.key, flag); + if (variant) { + return { ...variant, is_qa_tester: false }; + } + } + + const storedSalt = flag.hash_salt !== null && flag.hash_salt !== undefined ? flag.hash_salt : ""; + const salt = flagKey + storedSalt + "variant"; + const variantHash = normalizedHash(String(contextValue), salt); + + // Deep copy variants and apply splits + const variants = flag.ruleset.variants.map(v => ({ ...v })); + if (rollout.variant_splits) { + for (const variant of variants) { + if (variant.key in rollout.variant_splits) { + variant.split = rollout.variant_splits[variant.key]; + } + } + } + + let selected = variants[0]; + let cumulative = 0.0; + for (const variant of variants) { + selected = variant; + cumulative += variant.split || 0.0; + if (variantHash < cumulative) { + break; + } + } + + return { + variant_key: selected.key, + variant_value: selected.value, + experiment_id: flag.experiment_id, + is_experiment_active: flag.is_experiment_active, + is_qa_tester: false, + }; + } + + _isRuntimeEvaluationSatisfied(rollout, context) { + if (!rollout.runtime_evaluation_definition) { + return true; + } + + const customProperties = context.custom_properties; + if (!customProperties || typeof customProperties !== 'object') { + return false; + } + + for (const [key, expectedValue] of Object.entries(rollout.runtime_evaluation_definition)) { + if (!(key in customProperties)) { + return false; + } + + const actualValue = customProperties[key]; + if (String(actualValue).toLowerCase() !== String(expectedValue).toLowerCase()) { + return false; + } + } + + return true; + } + + _trackExposure(flagKey, selectedVariant, context) { + if (!context.distinct_id) { + this.logger?.error('Cannot track exposure event without a distinct_id in the context'); + return; + } + + const properties = { + 'Experiment name': flagKey, + 'Variant name': selectedVariant.variant_key, + '$experiment_type': 'feature_flag', + 'Flag evaluation mode': 'local', + }; + + if (selectedVariant.experiment_id !== undefined) { + properties['$experiment_id'] = selectedVariant.experiment_id; + } + + if (selectedVariant.is_experiment_active !== undefined) { + properties['$is_experiment_active'] = selectedVariant.is_experiment_active; + } + + if (selectedVariant.is_qa_tester !== undefined) { + properties['$is_qa_tester'] = selectedVariant.is_qa_tester; + } + + // Use the tracker function provided (bound to the main mixpanel instance) + this.tracker(context.distinct_id, EXPOSURE_EVENT, properties, (err) => { + if (err) { + this.logger?.error(`Failed to track exposure event for flag '${flagKey}': ${err.message}`); + } + }); + } +} + +module.exports = LocalFeatureFlagsProvider; diff --git a/lib/flags/remote_flags.d.ts b/lib/flags/remote_flags.d.ts new file mode 100644 index 0000000..6033069 --- /dev/null +++ b/lib/flags/remote_flags.d.ts @@ -0,0 +1,83 @@ +/** + * TypeScript definitions for Remote Feature Flags Provider + */ + +import { CustomLogger } from '../mixpanel-node' +import { RemoteFlagsConfig, FlagContext, SelectedVariant } from './types'; + +/** + * Remote Feature Flags Provider + * Evaluates feature flags via server-side API requests + */ +export default class RemoteFeatureFlagsProvider { + constructor( + token: string, + config: RemoteFlagsConfig, + logger: CustomLogger, + tracker: (distinct_id: string, event: string, properties: object, callback: (err?: Error) => void) => void + ); + + /** + * Get the variant value for a feature flag + * @param flagKey - Feature flag key + * @param fallbackValue - Value to return if flag evaluation fails + * @param context - Evaluation context (must include distinct_id) + * @param reportExposure - Whether to track exposure event (default: true) + * @returns Promise resolving to variant value + */ + getVariantValue( + flagKey: string, + fallbackValue: T, + context: FlagContext, + reportExposure?: boolean + ): Promise; + + /** + * Get the complete variant information for a feature flag + * @param flagKey - Feature flag key + * @param fallbackVariant - Variant to return if flag evaluation fails + * @param context - Evaluation context (must include distinct_id) + * @param reportExposure - Whether to track exposure event (default: true) + * @returns Promise resolving to selected variant + */ + getVariant( + flagKey: string, + fallbackVariant: SelectedVariant, + context: FlagContext, + reportExposure?: boolean + ): Promise; + + /** + * Check if a feature flag is enabled + * @param flagKey - Feature flag key + * @param context - Evaluation context (must include distinct_id) + * @returns Promise resolving to whether the flag is enabled + */ + isEnabled( + flagKey: string, + context: FlagContext + ): Promise; + + /** + * Get all feature flag variants for the current user context from remote server + * Exposure events are not automatically tracked when this method is used + * @param context - Evaluation context (must include distinct_id) + * @returns Promise resolving to dictionary mapping flag keys to variants, or null if the call fails + */ + getAllVariants( + context: FlagContext + ): Promise<{[key: string]: SelectedVariant} | null>; + + /** + * Manually tracks a feature flag exposure event to Mixpanel + * This provides flexibility for reporting individual exposure events when using getAllVariants + * @param flagKey - The key of the feature flag + * @param variant - The selected variant for the feature flag + * @param context - The user context used to evaluate the feature flag + */ + trackExposureEvent( + flagKey: string, + variant: SelectedVariant, + context: FlagContext + ): void; +} diff --git a/lib/flags/remote_flags.js b/lib/flags/remote_flags.js new file mode 100644 index 0000000..2fc403b --- /dev/null +++ b/lib/flags/remote_flags.js @@ -0,0 +1,246 @@ +/** + * Remote Feature Flags Provider + * Evaluates feature flags via server-side API requests + */ + +/** + * @typedef {import('./types').SelectedVariant} SelectedVariant + * @typedef {import('./types').FlagContext} FlagContext + * @typedef {import('./types').RemoteFlagsConfig} RemoteFlagsConfig + * @typedef {import('./types').RemoteFlagsResponse} RemoteFlagsResponse + */ + +const https = require('https'); +const packageInfo = require('../../package.json'); +const { + EXPOSURE_EVENT, + REQUEST_HEADERS, + prepareCommonQueryParams, + generateTraceparent, +} = require('./utils'); + +class RemoteFeatureFlagsProvider { + /** + * @param {string} token - Mixpanel project token + * @param {RemoteFlagsConfig} config - Remote flags configuration + * @param {Object} logger - Logger instance + * @param {Function} tracker - Function to track events (signature: track(distinct_id, event, properties, callback)) + */ + constructor(token, config, logger, tracker) { + this.token = token; + this.config = { + api_host: 'api.mixpanel.com', + request_timeout_in_seconds: 10, + ...config, + }; + this.tracker = tracker; + this.logger = logger; + } + + /** + * Get the variant value for a feature flag + * If the user context is eligible for the rollout, one of the flag variants will be selected and an exposure event will be tracked to Mixpanel. + * If the user context is not eligible, the fallback value is returned. + * @param {string} flagKey - Feature flag key + * @param {*} fallbackValue - Value to return if flag evaluation fails + * @param {FlagContext} context - Evaluation context + * @param {boolean} reportExposure - Whether to track exposure event + * @returns {Promise<*>} - Variant value + */ + async getVariantValue(flagKey, fallbackValue, context, reportExposure = true) { + try { + const selectedVariant = await this.getVariant(flagKey, { variant_value: fallbackValue }, context, reportExposure); + return selectedVariant.variant_value; + } catch (err) { + this.logger?.error(`Failed to get variant value for flag '${flagKey}': ${err.message}`); + return fallbackValue; + } + } + + /** + * Get the complete variant information for a feature flag + * If the user context is eligible for the rollout, one of the flag variants will be selected and an exposure event will be tracked to Mixpanel. + * If the user context is not eligible, the fallback value is returned. + * @param {string} flagKey - Feature flag key + * @param {SelectedVariant} fallbackVariant - Variant to return if flag evaluation fails + * @param {FlagContext} context - Evaluation context + * @param {boolean} reportExposure - Whether to track exposure event in the event that the user context is eligible for the rollout. + * @returns {Promise} - Selected variant + */ + async getVariant(flagKey, fallbackVariant, context, reportExposure = true) { + try { + const startTime = Date.now(); + const response = await this._fetchFlags(context, flagKey); + const latencyMs = Date.now() - startTime; + + const flags = response.flags || {}; + const selectedVariant = flags[flagKey]; + if (!selectedVariant) { + return fallbackVariant; + } + + if (reportExposure) { + this._trackExposure(flagKey, selectedVariant, context, latencyMs); + } + + return selectedVariant; + } catch (err) { + this.logger?.error(`Failed to get variant for flag '${flagKey}': ${err.message}`); + return fallbackVariant; + } + } + + /** + * Check if a feature flag is enabled. + * This method is intended only for flags defined as Mixpanel Feature Gates (boolean flags) + * @param {string} flagKey - Feature flag key + * @param {FlagContext} context - User's evaluation context + * @returns {Promise} - Whether the flag is enabled + */ + async isEnabled(flagKey, context) { + try { + const value = await this.getVariantValue(flagKey, false, context); + return value === true; + } catch (err) { + this.logger?.error(`Failed to check if flag '${flagKey}' is enabled: ${err.message}`); + return false; + } + } + + /** + * Get all feature flag variants for the current user context from remote server + * Exposure events are not automatically tracked when this method is used + * @param {FlagContext} context - User's evaluation context + * @returns {Promise<{[key: string]: SelectedVariant}|null>} - Dictionary mapping flag keys to variants, or null if the call fails + */ + async getAllVariants(context) { + try { + const response = await this._fetchFlags(context); + return response.flags || {}; + } catch (err) { + this.logger?.error(`Failed to get all remote variants: ${err.message}`); + return null; + } + } + + /** + * Manually tracks a feature flag exposure event to Mixpanel + * This provides flexibility for reporting individual exposure events when using getAllVariants + * If using getVariantValue or getVariant, exposure events are tracked automatically by default. + * @param {string} flagKey - The key of the feature flag + * @param {SelectedVariant} variant - The selected variant for the feature flag + * @param {FlagContext} context - The user context used to evaluate the feature flag + */ + trackExposureEvent(flagKey, variant, context) { + this._trackExposure(flagKey, variant, context, null); + } + + /** + * Fetch flags from remote flags evaluation API + * @param {FlagContext} context - Evaluation context + * @param {string} [flagKey] - Optional flag key (if omitted, fetches all flags) + * @returns {Promise} - API response containing flags dictionary + */ + _fetchFlags(context, flagKey = null) { + return new Promise((resolve, reject) => { + const commonParams = prepareCommonQueryParams(this.token, packageInfo.version); + const params = new URLSearchParams(commonParams); + + if (flagKey !== null) { + params.append('flag_key', flagKey); + } + + params.append('context', JSON.stringify(context)); + + const path = `/flags?${params.toString()}`; + + const requestOptions = { + host: this.config.api_host, + port: 443, + path: path, + method: 'GET', + headers: { + ...REQUEST_HEADERS, + 'Authorization': 'Basic ' + Buffer.from(this.token + ':').toString('base64'), + 'traceparent': generateTraceparent(), + }, + timeout: this.config.request_timeout_in_seconds * 1000, + }; + + const request = https.request(requestOptions, (res) => { + let data = ''; + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + if (res.statusCode !== 200) { + this.logger?.error(`HTTP ${res.statusCode} error fetching flags: ${data}`); + return reject(new Error(`HTTP ${res.statusCode}: ${data}`)); + } + + try { + const result = JSON.parse(data); + resolve(result); + } catch (parseErr) { + this.logger?.error(`Failed to parse JSON response: ${parseErr.message}`); + reject(parseErr); + } + }); + }); + + request.on('error', (err) => { + this.logger?.error(`Network error fetching flags: ${err.message}`); + reject(err); + }); + + request.on('timeout', () => { + this.logger?.error('Request timeout fetching flags'); + request.destroy(); + reject(new Error('Request timeout')); + }); + + request.end(); + }); + } + + _trackExposure(flagKey, selectedVariant, context, latencyMs=null) { + if (!context.distinct_id) { + this.logger?.error('Cannot track exposure event without a distinct_id in the context'); + return; + } + + const properties = { + 'Experiment name': flagKey, + 'Variant name': selectedVariant.variant_key, + '$experiment_type': 'feature_flag', + 'Flag evaluation mode': 'remote', + }; + + if (latencyMs !== null && latencyMs !== undefined) { + properties['Variant fetch latency (ms)'] = latencyMs; + } + + if (selectedVariant.experiment_id !== undefined) { + properties['$experiment_id'] = selectedVariant.experiment_id; + } + + if (selectedVariant.is_experiment_active !== undefined) { + properties['$is_experiment_active'] = selectedVariant.is_experiment_active; + } + + if (selectedVariant.is_qa_tester !== undefined) { + properties['$is_qa_tester'] = selectedVariant.is_qa_tester; + } + + // Use the tracker function provided (bound to the main mixpanel instance) + this.tracker(context.distinct_id, EXPOSURE_EVENT, properties, (err) => { + if (err) { + this.logger?.error(`[flags]Failed to track exposure event for flag '${flagKey}': ${err.message}`); + } + }); + } +} + +module.exports = RemoteFeatureFlagsProvider; diff --git a/lib/flags/types.d.ts b/lib/flags/types.d.ts new file mode 100644 index 0000000..0e0b549 --- /dev/null +++ b/lib/flags/types.d.ts @@ -0,0 +1,132 @@ +/** + * TypeScript type definitions for Mixpanel feature flags + */ + +/** + * Base configuration for feature flags + */ +export interface FlagsConfig { + /** API host for Mixpanel (default: 'api.mixpanel.com') */ + api_host?: string; + /** Request timeout in seconds (default: 10) */ + request_timeout_in_seconds?: number; +} + +/** + * Configuration for local feature flags (client-side evaluation) + */ +export interface LocalFlagsConfig extends FlagsConfig { + /** Enable automatic polling for flag definition updates (default: true) */ + enable_polling?: boolean; + /** Polling interval in seconds (default: 60) */ + polling_interval_in_seconds?: number; +} + +/** + * Configuration for remote feature flags (server-side evaluation) + */ +export interface RemoteFlagsConfig extends FlagsConfig {} + +/** + * Represents a variant in a feature flag + */ +export interface Variant { + /** Variant key/name */ + key: string; + /** Variant value (can be any type) */ + value: any; + /** Whether this is the control variant */ + is_control: boolean; + /** Percentage split for this variant (0.0-1.0) */ + split?: number; +} + +/** + * Variant override configuration + */ +export interface VariantOverride { + /** Key of the variant to override to */ + key: string; +} + +/** + * Rollout configuration for a feature flag + */ +export interface Rollout { + /** Percentage of users to include in this rollout (0.0-1.0) */ + rollout_percentage: number; + /** Runtime evaluation conditions (property-based targeting) */ + runtime_evaluation_definition?: Record; + /** Variant override for this rollout */ + variant_override?: VariantOverride; + /** Variant split percentages (variant_key -> percentage) */ + variant_splits?: Record; +} + +/** + * Test users configuration for a feature flag + */ +export interface FlagTestUsers { + /** Map of distinct_id to variant_key */ + users: Record; +} + +/** + * Rule set for a feature flag + */ +export interface RuleSet { + /** Available variants for this flag */ + variants: Variant[]; + /** Rollout configurations */ + rollout: Rollout[]; + /** Test users configuration */ + test?: FlagTestUsers; +} + +/** + * Complete feature flag definition + */ +export interface ExperimentationFlag { + /** Flag ID */ + id: string; + /** Flag name */ + name: string; + /** Flag key (used for lookups) */ + key: string; + /** Flag status */ + status: string; + /** Project ID */ + project_id: number; + /** Rule set for this flag */ + ruleset: RuleSet; + /** Context type (e.g., 'user', 'group') */ + context: string; + /** Associated experiment ID */ + experiment_id?: string; + /** Whether the associated experiment is active */ + is_experiment_active?: boolean; + /** Hash salt for variant assignment */ + hash_salt?: string; +} + +export interface SelectedVariant { + variant_key?: string | null; + variant_value: any; + experiment_id?: string; + is_experiment_active?: boolean; + is_qa_tester?: boolean; +} + +export interface RemoteFlagsResponse { + code: number; + flags: Record; +} + +export interface LocalFlagsResponse { + flags: ExperimentationFlag[]; +} + +export interface FlagContext { + distinct_id: string; + [key: string]: any; +} diff --git a/lib/flags/utils.js b/lib/flags/utils.js new file mode 100644 index 0000000..0a826f9 --- /dev/null +++ b/lib/flags/utils.js @@ -0,0 +1,82 @@ +/** + * Utility functions for Mixpanel feature flags + */ + +const crypto = require('crypto'); + +// Constants +const EXPOSURE_EVENT = '$experiment_started'; + +const REQUEST_HEADERS = { + 'X-Scheme': 'https', + 'X-Forwarded-Proto': 'https', + 'Content-Type': 'application/json', +}; + +/** + * FNV-1a 64-bit hash function used for consistent variant assignment + * @param {Buffer} data - Data to hash + * @returns {BigInt} - Hash value as BigInt + */ +function fnv1a64(data) { + const FNV_PRIME = BigInt('0x100000001B3'); + let hash = BigInt('0xCBF29CE484222325'); + + for (let i = 0; i < data.length; i++) { + hash ^= BigInt(data[i]); + hash *= FNV_PRIME; + hash &= BigInt('0xFFFFFFFFFFFFFFFF'); + } + + return hash; +} + +/** + * Normalized hash function that returns a value between 0.0 and 1.0 + * Used for variant assignment based on rollout percentages + * @param {string} key - The key to hash (usually distinct_id or other identifier) + * @param {string} salt - Salt value (usually flag-specific hash_salt) + * @returns {number} - Hash value normalized to 0.0-1.0 range + */ +function normalizedHash(key, salt) { + const combined = Buffer.from(key + salt, 'utf-8'); + const hashValue = fnv1a64(combined); + return Number(hashValue % BigInt(100)) / 100.0; +} + +/** + * Prepare common query parameters for feature flags API requests + * @param {string} token - Mixpanel project token + * @param {string} sdkVersion - SDK version string + * @returns {Object} - Query parameters object + */ +function prepareCommonQueryParams(token, sdkVersion) { + return { + mp_lib: 'node', + lib_version: sdkVersion, + token: token, + }; +} + +/** + * Generate W3C traceparent header for distributed tracing + * Format: 00-{trace-id}-{parent-id}-{trace-flags} + * @returns {string} - traceparent header value + */ +function generateTraceparent() { + const version = '00'; + const traceId = crypto.randomBytes(16).toString('hex'); + const parentId = crypto.randomBytes(8).toString('hex'); + const traceFlags = '01'; // sampled + + return `${version}-${traceId}-${parentId}-${traceFlags}`; +} + +module.exports = { + EXPOSURE_EVENT, + REQUEST_HEADERS, + fnv1a64, + normalizedHash, + prepareCommonQueryParams, + generateTraceparent, +}; diff --git a/lib/mixpanel-node.d.ts b/lib/mixpanel-node.d.ts index 4346c88..164d4c1 100644 --- a/lib/mixpanel-node.d.ts +++ b/lib/mixpanel-node.d.ts @@ -1,3 +1,7 @@ +import LocalFeatureFlagsProvider from './flags/local_flags'; +import RemoteFeatureFlagsProvider from './flags/remote_flags'; +import { LocalFlagsConfig, RemoteFlagsConfig } from './flags/types'; + declare const mixpanel: mixpanel.Mixpanel; declare namespace mixpanel { @@ -25,6 +29,8 @@ declare namespace mixpanel { keepAlive: boolean; geolocate: boolean; logger: CustomLogger; + local_flags_config?: LocalFlagsConfig; + remote_flags_config?: RemoteFlagsConfig; } export interface PropertyDict { @@ -84,6 +90,10 @@ declare namespace mixpanel { people: People; groups: Groups; + + local_flags?: LocalFeatureFlagsProvider; + + remote_flags?: RemoteFeatureFlagsProvider; } interface People { @@ -152,6 +162,11 @@ declare namespace mixpanel { delete_group(groupKey: string, groupId: string, modifiers?: Modifiers, callback?: Callback): void; delete_group(groupKey: string, groupId: string, callback: Callback): void; } + + // Export feature flags types for convenience + export { LocalFlagsConfig, RemoteFlagsConfig, FlagContext, SelectedVariant } from './flags/types'; + export { default as LocalFeatureFlagsProvider } from './flags/local_flags'; + export { default as RemoteFeatureFlagsProvider } from './flags/remote_flags'; } export = mixpanel; diff --git a/lib/mixpanel-node.js b/lib/mixpanel-node.js index 5d67c43..5f2fbba 100644 --- a/lib/mixpanel-node.js +++ b/lib/mixpanel-node.js @@ -18,6 +18,7 @@ const packageInfo = require('../package.json') const {async_all, ensure_timestamp, assert_logger} = require('./utils'); const {MixpanelGroups} = require('./groups'); const {MixpanelPeople} = require('./people'); +const {LocalFeatureFlagsProvider, RemoteFeatureFlagsProvider} = require('./flags'); const DEFAULT_CONFIG = { test: false, @@ -465,6 +466,23 @@ var create_client = function(token, config) { metrics.set_config(config); } + // Initialize feature flags providers if configs are provided + if (config && config.local_flags_config) { + metrics.local_flags = new LocalFeatureFlagsProvider( + token, + config.local_flags_config, + metrics.track.bind(metrics) + ); + } + + if (config && config.remote_flags_config) { + metrics.remote_flags = new RemoteFeatureFlagsProvider( + token, + config.remote_flags_config, + metrics.track.bind(metrics) + ); + } + return metrics; }; diff --git a/package-lock.json b/package-lock.json index 1bb934a..5be0e80 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ }, "devDependencies": { "@vitest/coverage-v8": "^1.6.0", + "nock": "^14.0.10", "proxyquire": "^1.7.11", "vitest": "^1.6.0" }, @@ -520,6 +521,49 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mswjs/interceptors": { + "version": "0.39.8", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.39.8.tgz", + "integrity": "sha512-2+BzZbjRO7Ct61k8fMNHEtoKjeWI9pIlHFTqBwZ5icHpqszIgEZbjb1MW5Z0+bITTCTl3gk4PDBxs9tA/csXvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" + } + }, + "node_modules/@open-draft/until": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "dev": true, + "license": "MIT" + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.18.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", @@ -1233,6 +1277,13 @@ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", "dev": true }, + "node_modules/is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "dev": true, + "license": "MIT" + }, "node_modules/is-object": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz", @@ -1337,6 +1388,13 @@ "node": ">=8" } }, + "node_modules/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, + "license": "ISC" + }, "node_modules/local-pkg": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", @@ -1481,6 +1539,21 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/nock": { + "version": "14.0.10", + "resolved": "https://registry.npmjs.org/nock/-/nock-14.0.10.tgz", + "integrity": "sha512-Q7HjkpyPeLa0ZVZC5qpxBt5EyLczFJ91MEewQiIi9taWuA0KB/MDJlUWtON+7dGouVdADTQsf9RA7TZk6D8VMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mswjs/interceptors": "^0.39.5", + "json-stringify-safe": "^5.0.1", + "propagate": "^2.0.0" + }, + "engines": { + "node": ">=18.20.0 <20 || >=20.12.1" + } + }, "node_modules/npm-run-path": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", @@ -1520,6 +1593,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/outvariant": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", + "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", + "dev": true, + "license": "MIT" + }, "node_modules/p-limit": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", @@ -1642,6 +1722,16 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/proxyquire": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/proxyquire/-/proxyquire-1.8.0.tgz", @@ -1748,6 +1838,13 @@ "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", "dev": true }, + "node_modules/strict-event-emitter": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "dev": true, + "license": "MIT" + }, "node_modules/strip-final-newline": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", @@ -2278,6 +2375,42 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "@mswjs/interceptors": { + "version": "0.39.8", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.39.8.tgz", + "integrity": "sha512-2+BzZbjRO7Ct61k8fMNHEtoKjeWI9pIlHFTqBwZ5icHpqszIgEZbjb1MW5Z0+bITTCTl3gk4PDBxs9tA/csXvA==", + "dev": true, + "requires": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "strict-event-emitter": "^0.5.1" + } + }, + "@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "dev": true + }, + "@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "dev": true, + "requires": { + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" + } + }, + "@open-draft/until": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "dev": true + }, "@rollup/rollup-android-arm-eabi": { "version": "4.18.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", @@ -2776,6 +2909,12 @@ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", "dev": true }, + "is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "dev": true + }, "is-object": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz", @@ -2857,6 +2996,12 @@ "istanbul-lib-report": "^3.0.0" } }, + "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 + }, "local-pkg": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", @@ -2964,6 +3109,17 @@ "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "dev": true }, + "nock": { + "version": "14.0.10", + "resolved": "https://registry.npmjs.org/nock/-/nock-14.0.10.tgz", + "integrity": "sha512-Q7HjkpyPeLa0ZVZC5qpxBt5EyLczFJ91MEewQiIi9taWuA0KB/MDJlUWtON+7dGouVdADTQsf9RA7TZk6D8VMw==", + "dev": true, + "requires": { + "@mswjs/interceptors": "^0.39.5", + "json-stringify-safe": "^5.0.1", + "propagate": "^2.0.0" + } + }, "npm-run-path": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", @@ -2991,6 +3147,12 @@ "mimic-fn": "^4.0.0" } }, + "outvariant": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", + "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", + "dev": true + }, "p-limit": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", @@ -3071,6 +3233,12 @@ } } }, + "propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "dev": true + }, "proxyquire": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/proxyquire/-/proxyquire-1.8.0.tgz", @@ -3159,6 +3327,12 @@ "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", "dev": true }, + "strict-event-emitter": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "dev": true + }, "strip-final-newline": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", diff --git a/package.json b/package.json index 300d8cc..6e2b94b 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "types": "./lib/mixpanel-node.d.ts", "devDependencies": { "@vitest/coverage-v8": "^1.6.0", + "nock": "^14.0.10", "proxyquire": "^1.7.11", "vitest": "^1.6.0" }, diff --git a/test/flags/local_flags.js b/test/flags/local_flags.js new file mode 100644 index 0000000..207601e --- /dev/null +++ b/test/flags/local_flags.js @@ -0,0 +1,506 @@ +const nock = require('nock'); +const LocalFeatureFlagsProvider = require('../../lib/flags/local_flags'); + +const mockFlagDefinitionsResponse = (flags) => { + const response = { + code: 200, + flags: flags, + }; + + nock('https://localhost') + .get('/flags/definitions') + .query(true) + .reply(200, response); +}; + +const mockFailedFlagDefinitionsResponse = (statusCode) => { + nock('https://localhost') + .get('/flags/definitions') + .query(true) + .reply(statusCode); +}; + +const createTestFlag = ({ + flagKey = 'test_flag', + context = 'distinct_id', + variants = null, + variantOverride = null, + rolloutPercentage = 100.0, + runtimeEvaluation = null, + testUsers = null, + experimentId = null, + isExperimentActive = null, + variantSplits = null, + hashSalt = null +} = {}) => { + const defaultVariants = [ + { key: 'control', value: 'control', is_control: true, split: 50.0 }, + { key: 'treatment', value: 'treatment', is_control: false, split: 50.0 } + ]; + + const rollout = [{ + rollout_percentage: rolloutPercentage, + runtime_evaluation_definition: runtimeEvaluation, + variant_override: variantOverride, + variant_splits: variantSplits + }]; + + const testConfig = testUsers ? { users: testUsers } : null; + + return { + id: 'test-id', + name: 'Test Flag', + key: flagKey, + status: 'active', + project_id: 123, + context: context, + experiment_id: experimentId, + is_experiment_active: isExperimentActive, + hash_salt: hashSalt, + ruleset: { + variants: variants || defaultVariants, + rollout: rollout, + test: testConfig + } + }; +}; + +describe('LocalFeatureFlagsProvider', () => { + const TEST_TOKEN = 'test-token'; + const TEST_CONTEXT = { + distinct_id: 'test-user', + }; + + let mockTracker; + let mockLogger; + + beforeEach(() => { + mockTracker = vi.fn(); + + mockLogger = { + error: vi.fn(), + warn: vi.fn(), + debug: vi.fn() + }; + }); + + afterEach(() => { + vi.restoreAllMocks(); + nock.cleanAll(); + }); + + describe('getVariant', () => { + let provider; + + beforeEach(() => { + const config = { + api_host: 'localhost', + enable_polling: false, + logger: mockLogger, + }; + + provider = new LocalFeatureFlagsProvider(TEST_TOKEN, config, mockTracker); + }); + + afterEach(() => { + provider.stopPollingForDefinitions(); + }); + + it('should return fallback when no flag definitions', async () => { + mockFlagDefinitionsResponse([]); + await provider.startPollingForDefinitions(); + + const result = provider.getVariant('nonexistent_flag', { variant_value: 'control' }, TEST_CONTEXT); + expect(result.variant_value).toBe('control'); + expect(mockTracker).not.toHaveBeenCalled(); + }); + + it('should return fallback if flag definition call fails', async () => { + mockFailedFlagDefinitionsResponse(500); + + await provider.startPollingForDefinitions(); + const result = provider.getVariant('nonexistent_flag', { variant_value: 'control' }, TEST_CONTEXT); + expect(result.variant_value).toBe('control'); + }); + + it('should return fallback when flag does not exist', async () => { + const otherFlag = createTestFlag({ flagKey: 'other_flag' }); + mockFlagDefinitionsResponse([otherFlag]); + await provider.startPollingForDefinitions(); + + const result = provider.getVariant('nonexistent_flag', { variant_value: 'control' }, TEST_CONTEXT); + expect(result.variant_value).toBe('control'); + }); + + it('should return fallback when no context', async () => { + const flag = createTestFlag({ context: 'distinct_id' }); + mockFlagDefinitionsResponse([flag]); + await provider.startPollingForDefinitions(); + + const result = provider.getVariant('test_flag', { variant_value: 'fallback' }, {}); + expect(result.variant_value).toBe('fallback'); + }); + + it('should return fallback when wrong context key', async () => { + const flag = createTestFlag({ context: 'user_id' }); + mockFlagDefinitionsResponse([flag]); + await provider.startPollingForDefinitions(); + + const result = provider.getVariant('test_flag', { variant_value: 'fallback' }, { distinct_id: 'user123' }); + expect(result.variant_value).toBe('fallback'); + }); + + it('should return test user variant when configured', async () => { + const variants = [ + { key: 'control', value: 'false', is_control: true, split: 50.0 }, + { key: 'treatment', value: 'true', is_control: false, split: 50.0 } + ]; + const flag = createTestFlag({ + variants: variants, + testUsers: { 'test_user': 'treatment' } + }); + + mockFlagDefinitionsResponse([flag]); + await provider.startPollingForDefinitions(); + + const result = provider.getVariant('test_flag', { variant_value: 'control' }, { distinct_id: 'test_user' }); + expect(result.variant_value).toBe('true'); + }); + + it('should return correct variant when test user variant not configured', async () => { + const variants = [ + { key: 'control', value: 'false', is_control: true, split: 50.0 }, + { key: 'treatment', value: 'true', is_control: false, split: 50.0 } + ]; + const flag = createTestFlag({ + variants: variants, + testUsers: { 'test_user': 'nonexistent_variant' } + }); + + mockFlagDefinitionsResponse([flag]); + await provider.startPollingForDefinitions(); + + const result = provider.getVariant('test_flag', { variant_value: 'fallback' }, { distinct_id: 'test_user' }); + expect(['false', 'true']).toContain(result.variant_value); + }); + + it('should return fallback when rollout percentage zero', async () => { + const flag = createTestFlag({ rolloutPercentage: 0.0 }); + mockFlagDefinitionsResponse([flag]); + await provider.startPollingForDefinitions(); + + const result = provider.getVariant('test_flag', { variant_value: 'fallback' }, TEST_CONTEXT); + expect(result.variant_value).toBe('fallback'); + }); + + it('should return variant when rollout percentage hundred', async () => { + const flag = createTestFlag({ rolloutPercentage: 100.0 }); + mockFlagDefinitionsResponse([flag]); + await provider.startPollingForDefinitions(); + + const result = provider.getVariant('test_flag', { variant_value: 'fallback' }, TEST_CONTEXT); + expect(result.variant_value).not.toBe('fallback'); + expect(['control', 'treatment']).toContain(result.variant_value); + }); + + it('should respect runtime evaluation when satisfied', async () => { + const runtimeEval = { plan: 'premium', region: 'US' }; + const flag = createTestFlag({ runtimeEvaluation: runtimeEval }); + + mockFlagDefinitionsResponse([flag]); + await provider.startPollingForDefinitions(); + + const context = { + distinct_id: 'user123', + custom_properties: { + plan: 'premium', + region: 'US' + } + }; + + const result = provider.getVariant('test_flag', { variant_value: 'fallback' }, context); + expect(result.variant_value).not.toBe('fallback'); + }); + + it('should return fallback when runtime evaluation not satisfied', async () => { + const runtimeEval = { plan: 'premium', region: 'US' }; + const flag = createTestFlag({ runtimeEvaluation: runtimeEval }); + + mockFlagDefinitionsResponse([flag]); + await provider.startPollingForDefinitions(); + + const context = { + distinct_id: 'user123', + custom_properties: { + plan: 'basic', + region: 'US' + } + }; + + const result = provider.getVariant('test_flag', { variant_value: 'fallback' }, context); + expect(result.variant_value).toBe('fallback'); + }); + + it('should pick correct variant with hundred percent split', async () => { + const variants = [ + { key: 'A', value: 'variant_a', is_control: false, split: 100.0 }, + { key: 'B', value: 'variant_b', is_control: false, split: 0.0 }, + { key: 'C', value: 'variant_c', is_control: false, split: 0.0 } + ]; + const flag = createTestFlag({ variants: variants, rolloutPercentage: 100.0 }); + + mockFlagDefinitionsResponse([flag]); + await provider.startPollingForDefinitions(); + + const result = provider.getVariant('test_flag', { variant_value: 'fallback' }, TEST_CONTEXT); + expect(result.variant_value).toBe('variant_a'); + }); + + it('should pick correct variant with half migrated group splits', async () => { + const variants = [ + { key: 'A', value: 'variant_a', is_control: false, split: 100.0 }, + { key: 'B', value: 'variant_b', is_control: false, split: 0.0 }, + { key: 'C', value: 'variant_c', is_control: false, split: 0.0 } + ]; + const variantSplits = { A: 0.0, B: 100.0, C: 0.0 }; + const flag = createTestFlag({ + variants: variants, + rolloutPercentage: 100.0, + variantSplits: variantSplits + }); + + mockFlagDefinitionsResponse([flag]); + await provider.startPollingForDefinitions(); + + const result = provider.getVariant('test_flag', { variant_value: 'fallback' }, TEST_CONTEXT); + expect(result.variant_value).toBe('variant_b'); + }); + + it('should pick correct variant with full migrated group splits', async () => { + const variants = [ + { key: 'A', value: 'variant_a', is_control: false }, + { key: 'B', value: 'variant_b', is_control: false }, + { key: 'C', value: 'variant_c', is_control: false } + ]; + const variantSplits = { A: 0.0, B: 0.0, C: 100.0 }; + const flag = createTestFlag({ + variants: variants, + rolloutPercentage: 100.0, + variantSplits: variantSplits + }); + + mockFlagDefinitionsResponse([flag]); + await provider.startPollingForDefinitions(); + + const result = provider.getVariant('test_flag', { variant_value: 'fallback' }, TEST_CONTEXT); + expect(result.variant_value).toBe('variant_c'); + }); + + it('should pick overridden variant', async () => { + const variants = [ + { key: 'A', value: 'variant_a', is_control: false, split: 100.0 }, + { key: 'B', value: 'variant_b', is_control: false, split: 0.0 } + ]; + const flag = createTestFlag({ + variants: variants, + variantOverride: { key: 'B' } + }); + + mockFlagDefinitionsResponse([flag]); + await provider.startPollingForDefinitions(); + + const result = provider.getVariant('test_flag', { variant_value: 'control' }, TEST_CONTEXT); + expect(result.variant_value).toBe('variant_b'); + }); + + it('should track exposure when variant selected', async () => { + const flag = createTestFlag(); + mockFlagDefinitionsResponse([flag]); + await provider.startPollingForDefinitions(); + + provider.getVariant('test_flag', { variant_value: 'fallback' }, TEST_CONTEXT); + expect(mockTracker).toHaveBeenCalledTimes(1); + }); + + it('should track exposure with correct properties', async () => { + const flag = createTestFlag({ + experimentId: 'exp-123', + isExperimentActive: true, + testUsers: { 'qa_user': 'treatment' } + }); + + mockFlagDefinitionsResponse([flag]); + await provider.startPollingForDefinitions(); + + provider.getVariant('test_flag', { variant_value: 'fallback' }, { distinct_id: 'qa_user' }); + + expect(mockTracker).toHaveBeenCalledTimes(1); + + const call = mockTracker.mock.calls[0]; + const properties = call[2]; + + expect(properties['$experiment_id']).toBe('exp-123'); + expect(properties['$is_experiment_active']).toBe(true); + expect(properties['$is_qa_tester']).toBe(true); + }); + + it('should not track exposure on fallback', async () => { + mockFlagDefinitionsResponse([]); + await provider.startPollingForDefinitions(); + + provider.getVariant('nonexistent_flag', { variant_value: 'fallback' }, TEST_CONTEXT); + expect(mockTracker).not.toHaveBeenCalled(); + }); + + it('should not track exposure without distinct_id', async () => { + const flag = createTestFlag({ context: 'company' }); + mockFlagDefinitionsResponse([flag]); + await provider.startPollingForDefinitions(); + + provider.getVariant('test_flag', { variant_value: 'fallback' }, { company_id: 'company123' }); + expect(mockTracker).not.toHaveBeenCalled(); + }); + }); + + describe('getAllVariants', () => { + let provider; + + beforeEach(() => { + const config = { + api_host: 'localhost', + enable_polling: false, + logger: mockLogger, + }; + + provider = new LocalFeatureFlagsProvider(TEST_TOKEN, config, mockTracker); + }); + + afterEach(() => { + provider.stopPollingForDefinitions(); + }); + + it('should return empty object when no flag definitions', async () => { + mockFlagDefinitionsResponse([]); + await provider.startPollingForDefinitions(); + + const result = provider.getAllVariants(TEST_CONTEXT); + + expect(result).toEqual({}); + }); + + it('should return all variants when two flags have 100% rollout', async () => { + const flag1 = createTestFlag({ flagKey: 'flag1', rolloutPercentage: 100.0 }); + const flag2 = createTestFlag({ flagKey: 'flag2', rolloutPercentage: 100.0 }); + + mockFlagDefinitionsResponse([flag1, flag2]); + await provider.startPollingForDefinitions(); + + const result = provider.getAllVariants(TEST_CONTEXT); + + expect(Object.keys(result).length).toBe(2); + expect(result).toHaveProperty('flag1'); + expect(result).toHaveProperty('flag2'); + }); + + it('should return partial results when one flag has 0% rollout', async () => { + const flag1 = createTestFlag({ flagKey: 'flag1', rolloutPercentage: 100.0 }); + const flag2 = createTestFlag({ flagKey: 'flag2', rolloutPercentage: 0.0 }); + + mockFlagDefinitionsResponse([flag1, flag2]); + await provider.startPollingForDefinitions(); + + const result = provider.getAllVariants(TEST_CONTEXT); + + expect(Object.keys(result).length).toBe(1); + expect(result).toHaveProperty('flag1'); + expect(result).not.toHaveProperty('flag2'); + }); + }); + + describe('getVariantValue', () => { + let provider; + + beforeEach(() => { + const config = { + api_host: 'localhost', + enable_polling: false, + logger: mockLogger, + }; + + provider = new LocalFeatureFlagsProvider(TEST_TOKEN, config, mockTracker); + }); + + afterEach(() => { + provider.stopPollingForDefinitions(); + }); + + it('should return variant value when flag exists', async () => { + const variants = [ + { key: 'treatment', value: 'blue', is_control: false, split: 100.0 } + ]; + const flag = createTestFlag({ variants: variants, rolloutPercentage: 100.0 }); + + mockFlagDefinitionsResponse([flag]); + await provider.startPollingForDefinitions(); + + const result = provider.getVariantValue('test_flag', 'default', TEST_CONTEXT); + + expect(result).toBe('blue'); + }); + + it('should return fallback value when flag doesn\'t exist', async () => { + mockFlagDefinitionsResponse([]); + await provider.startPollingForDefinitions(); + + const result = provider.getVariantValue('nonexistent_flag', 'default_value', TEST_CONTEXT); + + expect(result).toBe('default_value'); + }); + }); + + describe('isEnabled', () => { + let provider; + + beforeEach(() => { + const config = { + api_host: 'localhost', + enable_polling: false, + logger: mockLogger, + }; + + provider = new LocalFeatureFlagsProvider(TEST_TOKEN, config, mockTracker); + }); + + afterEach(() => { + provider.stopPollingForDefinitions(); + }); + + it('should return true when variant value is boolean true', async () => { + const variants = [ + { key: 'treatment', value: true, is_control: false, split: 100.0 } + ]; + const flag = createTestFlag({ variants: variants, rolloutPercentage: 100.0 }); + + mockFlagDefinitionsResponse([flag]); + await provider.startPollingForDefinitions(); + + const result = provider.isEnabled('test_flag', TEST_CONTEXT); + + expect(result).toBe(true); + }); + + it('should return false when variant value is boolean false', async () => { + const variants = [ + { key: 'control', value: false, is_control: true, split: 100.0 } + ]; + const flag = createTestFlag({ variants: variants, rolloutPercentage: 100.0 }); + + mockFlagDefinitionsResponse([flag]); + await provider.startPollingForDefinitions(); + + const result = provider.isEnabled('test_flag', TEST_CONTEXT); + + expect(result).toBe(false); + }); + }); +}); diff --git a/test/flags/remote_flags.js b/test/flags/remote_flags.js new file mode 100644 index 0000000..6350c55 --- /dev/null +++ b/test/flags/remote_flags.js @@ -0,0 +1,445 @@ +const nock = require('nock'); +const RemoteFeatureFlagsProvider = require('../../lib/flags/remote_flags'); + +const mockSuccessResponse = (flags_with_selected_variant) => { + const remote_response = { + code: 200, + flags: flags_with_selected_variant, + }; + + nock('https://localhost') + .get('/flags') + .query(true) + .reply(200, remote_response); +}; + +describe('RemoteFeatureFlagProvider', () => { + const flagsEndpointHostName = "localhost"; + const TEST_TOKEN = 'test-token'; + + const TEST_CONTEXT = { + distinct_id: 'test-user', + }; + + let provider; + let mockTracker; + + beforeEach(() => { + mockTracker = vi.fn(); + + let mockLogger = { + error: vi.fn(), + warn: vi.fn(), + info: vi.fn() + }; + + let config = { + api_host: flagsEndpointHostName, + }; + + provider = new RemoteFeatureFlagsProvider(TEST_TOKEN, config, mockLogger, mockTracker); + }); + + afterEach(() => { + vi.restoreAllMocks(); + nock.cleanAll(); + }); + + describe('getVariant', () => { + it('should return variant when served', async () => { + mockSuccessResponse({ + 'new-feature': { + variant_key: 'on', + variant_value: true, + } + }); + + const expectedVariant = { + variant_key: 'on', + variant_value: true, + } + + const result = await provider.getVariant('new-feature', null, TEST_CONTEXT); + + expect(result).toEqual(expectedVariant); + }); + + it('should select fallback variant when no flags are served', async () => { + nock('https://localhost') + .get('/flags') + .query(true) + .reply(200, { code: 200, flags: {} }); + + const fallbackVariant = { + variant_key: 'control', + variant_value: false, + }; + + const result = await provider.getVariant('any-flag', fallbackVariant, TEST_CONTEXT); + + expect(result).toEqual(fallbackVariant); + expect(mockTracker).not.toHaveBeenCalled(); + }); + + it('should select fallback variant if flag does not exist in served flags', async () => { + mockSuccessResponse({ + 'different-flag': { + variant_key: 'on', + variant_value: true, + } + }); + + const fallbackVariant = { + variant_key: 'control', + variant_value: false, + }; + + const result = await provider.getVariant('missing-flag', fallbackVariant, TEST_CONTEXT); + + expect(result).toEqual(fallbackVariant); + expect(mockTracker).not.toHaveBeenCalled(); + }); + + it('No exposure events are tracked when fallback variant is selected', async () => { + nock('https://localhost') + .get('/flags') + .query(true) + .reply(200, { code: 200, flags: {} }); + + const fallbackVariant = { + variant_key: 'control', + variant_value: false, + }; + + await provider.getVariant('any-flag', fallbackVariant, TEST_CONTEXT); + + expect(mockTracker).not.toHaveBeenCalled(); + }); + + it('Exposure event is tracked when a variant is selected', async () => { + mockSuccessResponse({ + 'test-flag': { + variant_key: 'treatment', + variant_value: true, + } + }); + + const fallbackVariant = { + variant_key: 'control', + variant_value: false, + }; + + const result = await provider.getVariant('test-flag', fallbackVariant, TEST_CONTEXT); + + expect(result).toEqual({ + variant_key: 'treatment', + variant_value: true, + }); + + expect(mockTracker).toHaveBeenCalledTimes(1); + + expect(mockTracker).toHaveBeenCalledWith( + 'test-user', + '$experiment_started', + expect.objectContaining({ + 'Experiment name': 'test-flag', + 'Variant name': 'treatment', + '$experiment_type': 'feature_flag', + 'Flag evaluation mode': 'remote' + }), + expect.any(Function) + ); + }); + }); + + describe('getVariantValue', () => { + it('should return variant value when flag exists', async () => { + mockSuccessResponse({ + 'test-flag': { + variant_key: 'treatment', + variant_value: 'blue', + } + }); + + const result = await provider.getVariantValue('test-flag', 'default', TEST_CONTEXT); + + expect(result).toEqual('blue'); + }); + + it('should return fallback value when flag doesn\'t exist', async () => { + mockSuccessResponse({ + 'different-flag': { + variant_key: 'on', + variant_value: true, + } + }); + + const result = await provider.getVariantValue('missing-flag', 'default-value', TEST_CONTEXT); + + expect(result).toEqual('default-value'); + }); + + it('should track exposure event by default', async () => { + mockSuccessResponse({ + 'test-flag': { + variant_key: 'treatment', + variant_value: 'value', + } + }); + + await provider.getVariantValue('test-flag', 'default', TEST_CONTEXT); + + expect(mockTracker).toHaveBeenCalledTimes(1); + expect(mockTracker).toHaveBeenCalledWith( + 'test-user', + '$experiment_started', + expect.objectContaining({ + 'Experiment name': 'test-flag', + 'Variant name': 'treatment', + }), + expect.any(Function) + ); + }); + + it('should NOT track exposure event when reportExposure is false', async () => { + mockSuccessResponse({ + 'test-flag': { + variant_key: 'treatment', + variant_value: 'value', + } + }); + + await provider.getVariantValue('test-flag', 'default', TEST_CONTEXT, false); + + expect(mockTracker).not.toHaveBeenCalled(); + }); + + it('should handle different variant value types', async () => { + // Test string + mockSuccessResponse({ + 'string-flag': { + variant_key: 'treatment', + variant_value: 'text-value', + } + }); + let result = await provider.getVariantValue('string-flag', 'default', TEST_CONTEXT); + expect(result).toEqual('text-value'); + + // Test number + nock.cleanAll(); + mockSuccessResponse({ + 'number-flag': { + variant_key: 'treatment', + variant_value: 42, + } + }); + result = await provider.getVariantValue('number-flag', 0, TEST_CONTEXT); + expect(result).toEqual(42); + + // Test object + nock.cleanAll(); + mockSuccessResponse({ + 'object-flag': { + variant_key: 'treatment', + variant_value: { key: 'value' }, + } + }); + result = await provider.getVariantValue('object-flag', {}, TEST_CONTEXT); + expect(result).toEqual({ key: 'value' }); + }); + + it('should return fallback on network error', async () => { + nock('https://localhost') + .get('/flags') + .query(true) + .replyWithError('Network error'); + + const result = await provider.getVariantValue('test-flag', 'fallback', TEST_CONTEXT); + + expect(result).toEqual('fallback'); + }); + + it('should return fallback when no flags are served', async () => { + nock('https://localhost') + .get('/flags') + .query(true) + .reply(200, { code: 200, flags: {} }); + + const result = await provider.getVariantValue('test-flag', 'fallback', TEST_CONTEXT); + + expect(result).toEqual('fallback'); + }); + + it('should NOT track exposure when fallback is returned', async () => { + nock('https://localhost') + .get('/flags') + .query(true) + .reply(200, { code: 200, flags: {} }); + + await provider.getVariantValue('test-flag', 'fallback', TEST_CONTEXT); + + expect(mockTracker).not.toHaveBeenCalled(); + }); + }); + + describe('getAllVariants', () => { + it('should return all variants from API', async () => { + mockSuccessResponse({ + 'flag-1': { + variant_key: 'treatment', + variant_value: true, + }, + 'flag-2': { + variant_key: 'control', + variant_value: false, + }, + 'flag-3': { + variant_key: 'blue', + variant_value: 'blue-theme', + } + }); + + const result = await provider.getAllVariants(TEST_CONTEXT); + + expect(result).toEqual({ + 'flag-1': { + variant_key: 'treatment', + variant_value: true, + }, + 'flag-2': { + variant_key: 'control', + variant_value: false, + }, + 'flag-3': { + variant_key: 'blue', + variant_value: 'blue-theme', + } + }); + }); + + it('should return empty object when no flags served', async () => { + nock('https://localhost') + .get('/flags') + .query(true) + .reply(200, { code: 200, flags: {} }); + + const result = await provider.getAllVariants(TEST_CONTEXT); + + expect(result).toEqual({}); + }); + + it('should NOT track any exposure events', async () => { + mockSuccessResponse({ + 'flag-1': { + variant_key: 'treatment', + variant_value: true, + }, + 'flag-2': { + variant_key: 'control', + variant_value: false, + } + }); + + await provider.getAllVariants(TEST_CONTEXT); + + expect(mockTracker).not.toHaveBeenCalled(); + }); + }); + + describe('isEnabled', () => { + it('should return true when variant value is boolean true', async () => { + mockSuccessResponse({ + 'test-flag': { + variant_key: 'on', + variant_value: true, + } + }); + + const result = await provider.isEnabled('test-flag', TEST_CONTEXT); + + expect(result).toBe(true); + }); + + it('should return false when variant value is boolean false', async () => { + mockSuccessResponse({ + 'test-flag': { + variant_key: 'off', + variant_value: false, + } + }); + + const result = await provider.isEnabled('test-flag', TEST_CONTEXT); + + expect(result).toBe(false); + }); + + it('should return false for truthy non-boolean values', async () => { + // Test string "true" + mockSuccessResponse({ + 'string-flag': { + variant_key: 'treatment', + variant_value: 'true', + } + }); + let result = await provider.isEnabled('string-flag', TEST_CONTEXT); + expect(result).toBe(false); + + // Test number 1 + nock.cleanAll(); + mockSuccessResponse({ + 'number-flag': { + variant_key: 'treatment', + variant_value: 1, + } + }); + result = await provider.isEnabled('number-flag', TEST_CONTEXT); + expect(result).toBe(false); + }); + + it('should return false when flag doesn\'t exist', async () => { + mockSuccessResponse({ + 'different-flag': { + variant_key: 'on', + variant_value: true, + } + }); + + const result = await provider.isEnabled('missing-flag', TEST_CONTEXT); + + expect(result).toBe(false); + }); + + it('should track exposure event', async () => { + mockSuccessResponse({ + 'test-flag': { + variant_key: 'on', + variant_value: true, + } + }); + + await provider.isEnabled('test-flag', TEST_CONTEXT); + + expect(mockTracker).toHaveBeenCalledTimes(1); + expect(mockTracker).toHaveBeenCalledWith( + 'test-user', + '$experiment_started', + expect.objectContaining({ + 'Experiment name': 'test-flag', + 'Variant name': 'on', + }), + expect.any(Function) + ); + }); + + it('should return false on network error', async () => { + nock('https://localhost') + .get('/flags') + .query(true) + .replyWithError('Network error'); + + const result = await provider.isEnabled('test-flag', TEST_CONTEXT); + + expect(result).toBe(false); + }); + }); +}); diff --git a/vitest.config.mjs b/vitest.config.mjs index c8df25d..389bc88 100644 --- a/vitest.config.mjs +++ b/vitest.config.mjs @@ -4,7 +4,7 @@ export default defineConfig({ test: { globals: true, include: [ - 'test/**.js' + 'test/**/*.js' ], coverage: { exclude: [ From 33522588c66db8038f7edeb295f0afad44e0bc7a Mon Sep 17 00:00:00 2001 From: Kwame Efah <37164746+efahk@users.noreply.github.com> Date: Thu, 6 Nov 2025 03:16:24 +0000 Subject: [PATCH 02/15] Updating types --- lib/flags/local_flags.d.ts | 87 ++++---------------------------------- lib/flags/local_flags.js | 5 ++- lib/flags/remote_flags.js | 2 +- lib/mixpanel-node.js | 6 ++- 4 files changed, 16 insertions(+), 84 deletions(-) diff --git a/lib/flags/local_flags.d.ts b/lib/flags/local_flags.d.ts index a3c892b..8da4c16 100644 --- a/lib/flags/local_flags.d.ts +++ b/lib/flags/local_flags.d.ts @@ -3,6 +3,7 @@ */ import { LocalFlagsConfig, FlagContext, SelectedVariant } from './types'; +import { CustomLogger } from '../mixpanel-node'; /** * Local Feature Flags Provider @@ -12,53 +13,23 @@ export default class LocalFeatureFlagsProvider { constructor( token: string, config: LocalFlagsConfig, - tracker: (distinct_id: string, event: string, properties: object, callback: (err?: Error) => void) => void + tracker: (distinct_id: string, event: string, properties: object, callback: (err?: Error) => void) => void, + logger: CustomLogger ); /** * Start polling for flag definitions * Fetches immediately and then at regular intervals if polling is enabled - * @param callback - Optional callback (err) */ - startPolling(callback?: (err: Error | null) => void): void; + startPollingForDefinitions(): Promise; /** * Stop polling for flag definitions */ - stopPolling(): void; + stopPollingForDefinitions(): void; /** - * Get the variant value for a feature flag (callback mode) - * @param flagKey - Feature flag key - * @param fallbackValue - Value to return if flag evaluation fails - * @param context - Evaluation context (must include distinct_id) - * @param reportExposure - Whether to track exposure event (default: true) - * @param callback - Callback function (err, value) - */ - getVariantValue( - flagKey: string, - fallbackValue: T, - context: FlagContext, - reportExposure: boolean, - callback: (err: Error | null, value: T) => void - ): void; - - /** - * Get the variant value for a feature flag (callback mode without reportExposure) - * @param flagKey - Feature flag key - * @param fallbackValue - Value to return if flag evaluation fails - * @param context - Evaluation context (must include distinct_id) - * @param callback - Callback function (err, value) - */ - getVariantValue( - flagKey: string, - fallbackValue: T, - context: FlagContext, - callback: (err: Error | null, value: T) => void - ): void; - - /** - * Get the variant value for a feature flag (synchronous mode) + * Get the variant value for a feature flag * @param flagKey - Feature flag key * @param fallbackValue - Value to return if flag evaluation fails * @param context - Evaluation context (must include distinct_id) @@ -72,37 +43,7 @@ export default class LocalFeatureFlagsProvider { ): T; /** - * Get the complete variant information for a feature flag (callback mode) - * @param flagKey - Feature flag key - * @param fallbackVariant - Variant to return if flag evaluation fails - * @param context - Evaluation context (must include distinct_id) - * @param reportExposure - Whether to track exposure event (default: true) - * @param callback - Callback function (err, selectedVariant) - */ - getVariant( - flagKey: string, - fallbackVariant: SelectedVariant, - context: FlagContext, - reportExposure: boolean, - callback: (err: Error | null, variant: SelectedVariant) => void - ): void; - - /** - * Get the complete variant information for a feature flag (callback mode without reportExposure) - * @param flagKey - Feature flag key - * @param fallbackVariant - Variant to return if flag evaluation fails - * @param context - Evaluation context (must include distinct_id) - * @param callback - Callback function (err, selectedVariant) - */ - getVariant( - flagKey: string, - fallbackVariant: SelectedVariant, - context: FlagContext, - callback: (err: Error | null, variant: SelectedVariant) => void - ): void; - - /** - * Get the complete variant information for a feature flag (synchronous mode) + * Get the complete variant information for a feature flag * @param flagKey - Feature flag key * @param fallbackVariant - Variant to return if flag evaluation fails * @param context - Evaluation context (must include distinct_id) @@ -116,19 +57,7 @@ export default class LocalFeatureFlagsProvider { ): SelectedVariant; /** - * Check if a feature flag is enabled (callback mode) - * @param flagKey - Feature flag key - * @param context - Evaluation context (must include distinct_id) - * @param callback - Callback function (err, isEnabled) - */ - isEnabled( - flagKey: string, - context: FlagContext, - callback: (err: Error | null, isEnabled: boolean) => void - ): void; - - /** - * Check if a feature flag is enabled (synchronous mode) + * Check if a feature flag is enabled * @param flagKey - Feature flag key * @param context - Evaluation context (must include distinct_id) */ diff --git a/lib/flags/local_flags.js b/lib/flags/local_flags.js index 2271beb..1c4d13a 100644 --- a/lib/flags/local_flags.js +++ b/lib/flags/local_flags.js @@ -25,8 +25,9 @@ class LocalFeatureFlagsProvider { * @param {string} token - Mixpanel project token * @param {LocalFlagsConfig} config - Local flags configuration * @param {Function} tracker - Function to track events (signature: track(distinct_id, event, properties, callback)) + * @param {CustomLogger} logger - Logger */ - constructor(token, config, tracker) { + constructor(token, config, tracker, logger) { this.token = token; this.config = { api_host: 'api.mixpanel.com', @@ -36,7 +37,7 @@ class LocalFeatureFlagsProvider { ...config, }; this.tracker = tracker; - this.logger = config.logger; + this.logger = logger; this.flagDefinitions = new Map(); this.pollingInterval = null; diff --git a/lib/flags/remote_flags.js b/lib/flags/remote_flags.js index 2fc403b..5e42e54 100644 --- a/lib/flags/remote_flags.js +++ b/lib/flags/remote_flags.js @@ -23,7 +23,7 @@ class RemoteFeatureFlagsProvider { /** * @param {string} token - Mixpanel project token * @param {RemoteFlagsConfig} config - Remote flags configuration - * @param {Object} logger - Logger instance + * @param {CustomLogger} logger - Logger instance * @param {Function} tracker - Function to track events (signature: track(distinct_id, event, properties, callback)) */ constructor(token, config, logger, tracker) { diff --git a/lib/mixpanel-node.js b/lib/mixpanel-node.js index 5f2fbba..aaa169b 100644 --- a/lib/mixpanel-node.js +++ b/lib/mixpanel-node.js @@ -471,7 +471,8 @@ var create_client = function(token, config) { metrics.local_flags = new LocalFeatureFlagsProvider( token, config.local_flags_config, - metrics.track.bind(metrics) + metrics.track.bind(metrics), + config.logger ); } @@ -479,7 +480,8 @@ var create_client = function(token, config) { metrics.remote_flags = new RemoteFeatureFlagsProvider( token, config.remote_flags_config, - metrics.track.bind(metrics) + metrics.track.bind(metrics), + config.logger ); } From 9b4b447e723beb4fb25e37974b8ca97cbd161ad6 Mon Sep 17 00:00:00 2001 From: Kwame Efah <37164746+efahk@users.noreply.github.com> Date: Thu, 6 Nov 2025 03:33:41 +0000 Subject: [PATCH 03/15] Test updates --- lib/flags/remote_flags.js | 4 ++-- lib/flags/utils.js | 5 ++--- test/flags/local_flags.js | 12 ++++-------- test/flags/remote_flags.js | 2 +- test/flags/utils.js | 26 ++++++++++++++++++++++++++ 5 files changed, 35 insertions(+), 14 deletions(-) create mode 100644 test/flags/utils.js diff --git a/lib/flags/remote_flags.js b/lib/flags/remote_flags.js index 5e42e54..7467e51 100644 --- a/lib/flags/remote_flags.js +++ b/lib/flags/remote_flags.js @@ -23,10 +23,10 @@ class RemoteFeatureFlagsProvider { /** * @param {string} token - Mixpanel project token * @param {RemoteFlagsConfig} config - Remote flags configuration - * @param {CustomLogger} logger - Logger instance * @param {Function} tracker - Function to track events (signature: track(distinct_id, event, properties, callback)) + * @param {CustomLogger} logger - Logger instance */ - constructor(token, config, logger, tracker) { + constructor(token, config, tracker, logger) { this.token = token; this.config = { api_host: 'api.mixpanel.com', diff --git a/lib/flags/utils.js b/lib/flags/utils.js index 0a826f9..b8d7623 100644 --- a/lib/flags/utils.js +++ b/lib/flags/utils.js @@ -18,7 +18,7 @@ const REQUEST_HEADERS = { * @param {Buffer} data - Data to hash * @returns {BigInt} - Hash value as BigInt */ -function fnv1a64(data) { +function _fnv1a64(data) { const FNV_PRIME = BigInt('0x100000001B3'); let hash = BigInt('0xCBF29CE484222325'); @@ -40,7 +40,7 @@ function fnv1a64(data) { */ function normalizedHash(key, salt) { const combined = Buffer.from(key + salt, 'utf-8'); - const hashValue = fnv1a64(combined); + const hashValue = _fnv1a64(combined); return Number(hashValue % BigInt(100)) / 100.0; } @@ -75,7 +75,6 @@ function generateTraceparent() { module.exports = { EXPOSURE_EVENT, REQUEST_HEADERS, - fnv1a64, normalizedHash, prepareCommonQueryParams, generateTraceparent, diff --git a/test/flags/local_flags.js b/test/flags/local_flags.js index 207601e..2e0990f 100644 --- a/test/flags/local_flags.js +++ b/test/flags/local_flags.js @@ -96,10 +96,9 @@ describe('LocalFeatureFlagsProvider', () => { const config = { api_host: 'localhost', enable_polling: false, - logger: mockLogger, }; - provider = new LocalFeatureFlagsProvider(TEST_TOKEN, config, mockTracker); + provider = new LocalFeatureFlagsProvider(TEST_TOKEN, config, mockTracker, mockLogger); }); afterEach(() => { @@ -369,10 +368,9 @@ describe('LocalFeatureFlagsProvider', () => { const config = { api_host: 'localhost', enable_polling: false, - logger: mockLogger, }; - provider = new LocalFeatureFlagsProvider(TEST_TOKEN, config, mockTracker); + provider = new LocalFeatureFlagsProvider(TEST_TOKEN, config, mockTracker, mockLogger); }); afterEach(() => { @@ -424,10 +422,9 @@ describe('LocalFeatureFlagsProvider', () => { const config = { api_host: 'localhost', enable_polling: false, - logger: mockLogger, }; - provider = new LocalFeatureFlagsProvider(TEST_TOKEN, config, mockTracker); + provider = new LocalFeatureFlagsProvider(TEST_TOKEN, config, mockTracker, mockLogger); }); afterEach(() => { @@ -465,10 +462,9 @@ describe('LocalFeatureFlagsProvider', () => { const config = { api_host: 'localhost', enable_polling: false, - logger: mockLogger, }; - provider = new LocalFeatureFlagsProvider(TEST_TOKEN, config, mockTracker); + provider = new LocalFeatureFlagsProvider(TEST_TOKEN, config, mockTracker, mockLogger); }); afterEach(() => { diff --git a/test/flags/remote_flags.js b/test/flags/remote_flags.js index 6350c55..0a21e9a 100644 --- a/test/flags/remote_flags.js +++ b/test/flags/remote_flags.js @@ -37,7 +37,7 @@ describe('RemoteFeatureFlagProvider', () => { api_host: flagsEndpointHostName, }; - provider = new RemoteFeatureFlagsProvider(TEST_TOKEN, config, mockLogger, mockTracker); + provider = new RemoteFeatureFlagsProvider(TEST_TOKEN, config, mockTracker, mockLogger); }); afterEach(() => { diff --git a/test/flags/utils.js b/test/flags/utils.js new file mode 100644 index 0000000..59a56e4 --- /dev/null +++ b/test/flags/utils.js @@ -0,0 +1,26 @@ +const { expect } = require('chai'); +const { + generateTraceparent, + normalizedHash +} = require('../../lib/flags/utils'); + +describe('Utils', function() { + describe('generateTraceparent', function() { + it('should generate traceparent in W3C format', function() { + const traceparent = generateTraceparent(); + // W3C format: 00-{32 hex chars}-{16 hex chars}-01 + const pattern = /^00-[0-9a-f]{32}-[0-9a-f]{16}-01$/; + expect(traceparent).to.match(pattern); + }); + }); + + describe('normalizedHash', function() { + it('should match known test vectors', function() { + const hash1 = normalizedHash("abc", "variant"); + expect(hash1).equals(0.72) + + const hash2 = normalizedHash("def", "variant"); + expect(hash2).equals(0.21, 0.01); + }); + }); +}); From e9bf8f4dd51d7bbed1db4b055713a5ec8b374386 Mon Sep 17 00:00:00 2001 From: Kwame Efah <37164746+efahk@users.noreply.github.com> Date: Thu, 6 Nov 2025 03:34:22 +0000 Subject: [PATCH 04/15] update version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6e2b94b..46a56cb 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "api", "stats" ], - "version": "0.18.1", + "version": "0.19.0", "homepage": "https://github.com/mixpanel/mixpanel-node", "author": "Carl Sverre", "license": "MIT", From d6661d3d2aed92ac4d1f10cd146fefc722681690 Mon Sep 17 00:00:00 2001 From: Kwame Efah <37164746+efahk@users.noreply.github.com> Date: Thu, 6 Nov 2025 03:42:46 +0000 Subject: [PATCH 05/15] update track --- lib/flags/local_flags.js | 4 ++-- lib/flags/remote_flags.js | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/flags/local_flags.js b/lib/flags/local_flags.js index 1c4d13a..850b9c7 100644 --- a/lib/flags/local_flags.js +++ b/lib/flags/local_flags.js @@ -382,6 +382,7 @@ class LocalFeatureFlagsProvider { } const properties = { + 'distinct_id': context.distinct_id, 'Experiment name': flagKey, 'Variant name': selectedVariant.variant_key, '$experiment_type': 'feature_flag', @@ -400,8 +401,7 @@ class LocalFeatureFlagsProvider { properties['$is_qa_tester'] = selectedVariant.is_qa_tester; } - // Use the tracker function provided (bound to the main mixpanel instance) - this.tracker(context.distinct_id, EXPOSURE_EVENT, properties, (err) => { + this.tracker(EXPOSURE_EVENT, properties, (err) => { if (err) { this.logger?.error(`Failed to track exposure event for flag '${flagKey}': ${err.message}`); } diff --git a/lib/flags/remote_flags.js b/lib/flags/remote_flags.js index 7467e51..b53e814 100644 --- a/lib/flags/remote_flags.js +++ b/lib/flags/remote_flags.js @@ -212,6 +212,7 @@ class RemoteFeatureFlagsProvider { } const properties = { + 'distinct_id': context.distinct_id, 'Experiment name': flagKey, 'Variant name': selectedVariant.variant_key, '$experiment_type': 'feature_flag', @@ -235,7 +236,7 @@ class RemoteFeatureFlagsProvider { } // Use the tracker function provided (bound to the main mixpanel instance) - this.tracker(context.distinct_id, EXPOSURE_EVENT, properties, (err) => { + this.tracker(EXPOSURE_EVENT, properties, (err) => { if (err) { this.logger?.error(`[flags]Failed to track exposure event for flag '${flagKey}': ${err.message}`); } From dc28dd5b96a873ebdf9ff57c4f9e0f65c7699c48 Mon Sep 17 00:00:00 2001 From: Kwame Efah <37164746+efahk@users.noreply.github.com> Date: Thu, 6 Nov 2025 03:50:22 +0000 Subject: [PATCH 06/15] Test updates --- test/flags/local_flags.js | 2 +- test/flags/remote_flags.js | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/test/flags/local_flags.js b/test/flags/local_flags.js index 2e0990f..a9c62a5 100644 --- a/test/flags/local_flags.js +++ b/test/flags/local_flags.js @@ -336,7 +336,7 @@ describe('LocalFeatureFlagsProvider', () => { expect(mockTracker).toHaveBeenCalledTimes(1); const call = mockTracker.mock.calls[0]; - const properties = call[2]; + const properties = call[1]; expect(properties['$experiment_id']).toBe('exp-123'); expect(properties['$is_experiment_active']).toBe(true); diff --git a/test/flags/remote_flags.js b/test/flags/remote_flags.js index 0a21e9a..32acb1e 100644 --- a/test/flags/remote_flags.js +++ b/test/flags/remote_flags.js @@ -139,9 +139,9 @@ describe('RemoteFeatureFlagProvider', () => { expect(mockTracker).toHaveBeenCalledTimes(1); expect(mockTracker).toHaveBeenCalledWith( - 'test-user', '$experiment_started', expect.objectContaining({ + 'distinct_id': 'test-user', 'Experiment name': 'test-flag', 'Variant name': 'treatment', '$experiment_type': 'feature_flag', @@ -191,7 +191,6 @@ describe('RemoteFeatureFlagProvider', () => { expect(mockTracker).toHaveBeenCalledTimes(1); expect(mockTracker).toHaveBeenCalledWith( - 'test-user', '$experiment_started', expect.objectContaining({ 'Experiment name': 'test-flag', @@ -421,7 +420,6 @@ describe('RemoteFeatureFlagProvider', () => { expect(mockTracker).toHaveBeenCalledTimes(1); expect(mockTracker).toHaveBeenCalledWith( - 'test-user', '$experiment_started', expect.objectContaining({ 'Experiment name': 'test-flag', From 756aeef1a875a61dea311f258e68322a8f80ccc8 Mon Sep 17 00:00:00 2001 From: Kwame Efah <37164746+efahk@users.noreply.github.com> Date: Fri, 7 Nov 2025 00:36:11 +0000 Subject: [PATCH 07/15] addressing pr comments --- lib/flags/local_flags.d.ts | 3 +++ lib/flags/local_flags.js | 11 ++++++++--- lib/flags/remote_flags.d.ts | 4 +++- lib/flags/remote_flags.js | 3 ++- lib/flags/utils.js | 2 +- 5 files changed, 17 insertions(+), 6 deletions(-) diff --git a/lib/flags/local_flags.d.ts b/lib/flags/local_flags.d.ts index 8da4c16..6acd487 100644 --- a/lib/flags/local_flags.d.ts +++ b/lib/flags/local_flags.d.ts @@ -58,6 +58,9 @@ export default class LocalFeatureFlagsProvider { /** * Check if a feature flag is enabled + * This method is intended only for flags defined as Mixpanel Feature Gates (boolean flags) + * This checks that the variant value of a selected variant is concretely the boolean 'true' + * It does not coerce other truthy values. * @param flagKey - Feature flag key * @param context - Evaluation context (must include distinct_id) */ diff --git a/lib/flags/local_flags.js b/lib/flags/local_flags.js index 850b9c7..f6a03d9 100644 --- a/lib/flags/local_flags.js +++ b/lib/flags/local_flags.js @@ -81,6 +81,8 @@ class LocalFeatureFlagsProvider { /** * Check if a feature flag is enabled * This method is intended only for flags defined as Mixpanel Feature Gates (boolean flags) + * This checks that the variant value of a selected variant is concretely the boolean 'true' + * It does not coerce other truthy values. * @param {string} flagKey - Feature flag key * @param {FlagContext} context - Evaluation context (must include distinct_id) * @returns {boolean} @@ -122,7 +124,7 @@ class LocalFeatureFlagsProvider { const contextValue = context[flag.context]; if (!contextValue) { this.logger?.warn( - `The context, '${flag.context}' for flag, '${flagKey}' is not present in the supplied context dictionary` + `The variant assignment key, '${flag.context}' for flag, '${flagKey}' is not present in the supplied user context dictionary` ); return fallbackVariant; } @@ -263,7 +265,6 @@ class LocalFeatureFlagsProvider { variant_value: variant.value, experiment_id: flag.experiment_id, is_experiment_active: flag.is_experiment_active, - is_qa_tester: true, }; } } @@ -285,7 +286,11 @@ class LocalFeatureFlagsProvider { return null; } - return this._getMatchingVariant(variantKey, flag); + let selected_variant = this._getMatchingVariant(variantKey, flag); + if (selected_variant) { + selected_variant.is_qa_tester = true; + } + return selected_variant; } _getAssignedRollout(flag, contextValue, context) { diff --git a/lib/flags/remote_flags.d.ts b/lib/flags/remote_flags.d.ts index 6033069..ce6653c 100644 --- a/lib/flags/remote_flags.d.ts +++ b/lib/flags/remote_flags.d.ts @@ -48,7 +48,9 @@ export default class RemoteFeatureFlagsProvider { ): Promise; /** - * Check if a feature flag is enabled + * Check if a feature flag is enabled. + * This checks that the variant value of a selected variant is concretely the boolean 'true', which will be the case for flags setup as FeatureGates + * It does not coerce other truthy values. * @param flagKey - Feature flag key * @param context - Evaluation context (must include distinct_id) * @returns Promise resolving to whether the flag is enabled diff --git a/lib/flags/remote_flags.js b/lib/flags/remote_flags.js index b53e814..53ba798 100644 --- a/lib/flags/remote_flags.js +++ b/lib/flags/remote_flags.js @@ -91,8 +91,9 @@ class RemoteFeatureFlagsProvider { } /** - * Check if a feature flag is enabled. * This method is intended only for flags defined as Mixpanel Feature Gates (boolean flags) + * This checks that the variant value of a selected variant is concretely the boolean 'true' + * It does not coerce other truthy values. * @param {string} flagKey - Feature flag key * @param {FlagContext} context - User's evaluation context * @returns {Promise} - Whether the flag is enabled diff --git a/lib/flags/utils.js b/lib/flags/utils.js index b8d7623..fa6bbd3 100644 --- a/lib/flags/utils.js +++ b/lib/flags/utils.js @@ -53,7 +53,7 @@ function normalizedHash(key, salt) { function prepareCommonQueryParams(token, sdkVersion) { return { mp_lib: 'node', - lib_version: sdkVersion, + $lib_version: sdkVersion, token: token, }; } From d6b25943b4794b08b109e9a0781bf082a1aa2813 Mon Sep 17 00:00:00 2001 From: Kwame Efah <37164746+efahk@users.noreply.github.com> Date: Mon, 10 Nov 2025 23:53:27 +0000 Subject: [PATCH 08/15] Addressing PR feedback --- lib/flags/local_flags.js | 14 +++++++------- lib/flags/utils.js | 5 ++--- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/lib/flags/local_flags.js b/lib/flags/local_flags.js index f6a03d9..6e2bdfd 100644 --- a/lib/flags/local_flags.js +++ b/lib/flags/local_flags.js @@ -121,14 +121,15 @@ class LocalFeatureFlagsProvider { return fallbackVariant; } - const contextValue = context[flag.context]; - if (!contextValue) { + if (!Object.hasOwn(context, flag.context)) { this.logger?.warn( `The variant assignment key, '${flag.context}' for flag, '${flagKey}' is not present in the supplied user context dictionary` ); return fallbackVariant; } + const contextValue = context[flag.context]; + let selectedVariant = null; const testUserVariant = this._getVariantOverrideForTestUser(flag, context); @@ -159,11 +160,10 @@ class LocalFeatureFlagsProvider { */ getAllVariants(context) { const variants = {}; - const fallback = null; for (const flagKey of this.flagDefinitions.keys()) { - const variant = this.getVariant(flagKey, fallback, context, false); - if (variant !== fallback) { + const variant = this.getVariant(flagKey, null, context, false); + if (variant !== null) { variants[flagKey] = variant; } } @@ -184,7 +184,7 @@ class LocalFeatureFlagsProvider { /** * Fetch flag definitions from API - * @returns {Promise} + * @returns {Promise} */ _fetchFlagDefinitions() { return new Promise((resolve, reject) => { @@ -272,7 +272,7 @@ class LocalFeatureFlagsProvider { } _getVariantOverrideForTestUser(flag, context) { - if (!flag.ruleset.test || !flag.ruleset.test.users) { + if (!flag.ruleset.test?.users) { return null; } diff --git a/lib/flags/utils.js b/lib/flags/utils.js index fa6bbd3..e6dded2 100644 --- a/lib/flags/utils.js +++ b/lib/flags/utils.js @@ -8,13 +8,12 @@ const crypto = require('crypto'); const EXPOSURE_EVENT = '$experiment_started'; const REQUEST_HEADERS = { - 'X-Scheme': 'https', - 'X-Forwarded-Proto': 'https', 'Content-Type': 'application/json', }; /** * FNV-1a 64-bit hash function used for consistent variant assignment + * https://www.ietf.org/archive/id/draft-eastlake-fnv-21.html#section-6.1.2 * @param {Buffer} data - Data to hash * @returns {BigInt} - Hash value as BigInt */ @@ -36,7 +35,7 @@ function _fnv1a64(data) { * Used for variant assignment based on rollout percentages * @param {string} key - The key to hash (usually distinct_id or other identifier) * @param {string} salt - Salt value (usually flag-specific hash_salt) - * @returns {number} - Hash value normalized to 0.0-1.0 range + * @returns {number} - Hash value normalized to the non-inclusive range, [0.0, 1.0) */ function normalizedHash(key, salt) { const combined = Buffer.from(key + salt, 'utf-8'); From fea580fab45ec311c0238a639ee55ea35b8119b5 Mon Sep 17 00:00:00 2001 From: Kwame Efah Date: Mon, 10 Nov 2025 16:37:39 -0800 Subject: [PATCH 09/15] Refactor common code into base class and add more tests --- lib/flags/flags.d.ts | 38 ++++++++++++++++ lib/flags/flags.js | 92 +++++++++++++++++++++++++++++++++++++++ lib/flags/local_flags.js | 92 ++++++++++----------------------------- lib/flags/remote_flags.js | 89 +++++++++---------------------------- lib/flags/utils.js | 1 - package-lock.json | 4 +- test/flags/utils.js | 82 ++++++++++++++++++++++++++++++++++ 7 files changed, 258 insertions(+), 140 deletions(-) create mode 100644 lib/flags/flags.d.ts create mode 100644 lib/flags/flags.js diff --git a/lib/flags/flags.d.ts b/lib/flags/flags.d.ts new file mode 100644 index 0000000..b89b2c1 --- /dev/null +++ b/lib/flags/flags.d.ts @@ -0,0 +1,38 @@ +/** + * TypeScript type definitions for Base Feature Flags Provider + */ + +import { CustomLogger } from '../mixpanel-node'; + +/** + * Configuration for feature flags API requests + */ +export interface FeatureFlagsConfig { + token: string; + api_host: string; + request_timeout_in_seconds: number; +} + +/** + * Base Feature Flags Provider + * Contains common methods for feature flag evaluation + */ +export class FeatureFlagsProvider { + config: FeatureFlagsConfig; + endpoint: string; + logger: CustomLogger | null; + + /** + * @param config - Common configuration for feature flag providers + * @param endpoint - API endpoint path (i.e., '/flags' or '/flags/definitions') + * @param logger - Logger instance + */ + constructor(config: FeatureFlagsConfig, endpoint: string, logger: CustomLogger | null); + + /** + * Common HTTP request handler for flags API endpoints + * @param additionalParams - Additional query parameters to append + * @returns Parsed JSON response + */ + callFlagsEndpoint(additionalParams?: Record | null): Promise; +} diff --git a/lib/flags/flags.js b/lib/flags/flags.js new file mode 100644 index 0000000..efef58b --- /dev/null +++ b/lib/flags/flags.js @@ -0,0 +1,92 @@ +/** + * Base Feature Flags Provider + * Contains common methods for feature flag evaluation + */ + +const https = require('https'); +const packageInfo = require('../../package.json'); +const { prepareCommonQueryParams, generateTraceparent, REQUEST_HEADERS } = require('./utils'); + +class FeatureFlagsProvider { + /** + * @param {Object} config - Configuration object with token, api_host, request_timeout_in_seconds + * @param {string} endpoint - API endpoint path (e.g., '/flags' or '/flags/definitions') + * @param {CustomLogger} logger - Logger instance + */ + constructor(config, endpoint, logger) { + this.config = config; + this.endpoint = endpoint; + this.logger = logger; + } + + /** + * Common HTTP request handler for flags API endpoints + * @param {Object} additionalParams - Additional query parameters to append + * @returns {Promise} - Parsed JSON response + */ + async callFlagsEndpoint(additionalParams = null) { + return new Promise((resolve, reject) => { + const commonParams = prepareCommonQueryParams(this.config.token, packageInfo.version); + const params = new URLSearchParams(commonParams); + + if (additionalParams) { + for (const [key, value] of Object.entries(additionalParams)) { + params.append(key, value); + } + } + + const path = `${this.endpoint}?${params.toString()}`; + + const requestOptions = { + host: this.config.api_host, + port: 443, + path: path, + method: 'GET', + headers: { + ...REQUEST_HEADERS, + 'Authorization': 'Basic ' + Buffer.from(this.config.token + ':').toString('base64'), + 'traceparent': generateTraceparent(), + }, + timeout: this.config.request_timeout_in_seconds * 1000, + }; + + const request = https.request(requestOptions, (res) => { + let data = ''; + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + if (res.statusCode !== 200) { + this.logger?.error(`HTTP ${res.statusCode} error calling flags endpoint: ${data}`); + return reject(new Error(`HTTP ${res.statusCode}: ${data}`)); + } + + try { + const result = JSON.parse(data); + resolve(result); + } catch (parseErr) { + this.logger?.error(`Failed to parse JSON response: ${parseErr.message}`); + reject(parseErr); + } + }); + }); + + request.on('error', (err) => { + this.logger?.error(`Network error calling flags endpoint: ${err.message}`); + reject(err); + }); + + request.on('timeout', () => { + this.logger?.error(`Request timeout calling flags endpoint`); + request.destroy(); + reject(new Error('Request timeout')); + }); + + request.end(); + }); + } +} + +module.exports = FeatureFlagsProvider; diff --git a/lib/flags/local_flags.js b/lib/flags/local_flags.js index 6e2bdfd..53925d2 100644 --- a/lib/flags/local_flags.js +++ b/lib/flags/local_flags.js @@ -11,16 +11,13 @@ * @typedef {import('./types').LocalFlagsResponse} LocalFlagsResponse * */ -const https = require('https'); -const packageInfo = require('../../package.json'); +const FeatureFlagsProvider = require('./flags'); const { EXPOSURE_EVENT, - REQUEST_HEADERS, - prepareCommonQueryParams, normalizedHash, } = require('./utils'); -class LocalFeatureFlagsProvider { +class LocalFeatureFlagsProvider extends FeatureFlagsProvider { /** * @param {string} token - Mixpanel project token * @param {LocalFlagsConfig} config - Local flags configuration @@ -28,16 +25,25 @@ class LocalFeatureFlagsProvider { * @param {CustomLogger} logger - Logger */ constructor(token, config, tracker, logger) { - this.token = token; - this.config = { + const mergedConfig = { api_host: 'api.mixpanel.com', request_timeout_in_seconds: 10, enable_polling: true, polling_interval_in_seconds: 60, ...config, }; + + const providerConfig = { + token: token, + api_host: mergedConfig.api_host, + request_timeout_in_seconds: mergedConfig.request_timeout_in_seconds, + }; + + super(providerConfig, '/flags/definitions', logger); + + this.token = token; + this.config = mergedConfig; this.tracker = tracker; - this.logger = logger; this.flagDefinitions = new Map(); this.pollingInterval = null; @@ -183,72 +189,20 @@ class LocalFeatureFlagsProvider { } /** - * Fetch flag definitions from API + * Fetch flag definitions from API. * @returns {Promise} */ - _fetchFlagDefinitions() { - return new Promise((resolve, reject) => { - const commonParams = prepareCommonQueryParams(this.token, packageInfo.version); - const params = new URLSearchParams(commonParams); - - const path = `/flags/definitions?${params.toString()}`; - - const requestOptions = { - host: this.config.api_host, - port: 443, - path: path, - method: 'GET', - headers: { - ...REQUEST_HEADERS, - 'Authorization': 'Basic ' + Buffer.from(this.token + ':').toString('base64'), - }, - timeout: this.config.request_timeout_in_seconds * 1000, - }; - - const request = https.request(requestOptions, (res) => { - let data = ''; - - res.on('data', (chunk) => { - data += chunk; - }); - - res.on('end', () => { - if (res.statusCode !== 200) { - this.logger?.error(`HTTP ${res.statusCode} error fetching flag definitions: ${data}`); - return reject(new Error(`HTTP ${res.statusCode}: ${data}`)); - } + async _fetchFlagDefinitions() { + const response = await this.callFlagsEndpoint(); - try { - const response = JSON.parse(data); - - const newDefinitions = new Map(); - response.flags.forEach(flag => { - newDefinitions.set(flag.key, flag); - }); - - this.flagDefinitions = newDefinitions; - - resolve(response); - } catch (parseErr) { - this.logger?.error(`Failed to parse JSON response for flag definitions: ${parseErr.message}`); - reject(parseErr); - } - }); - }); - - request.on('error', (err) => { - this.logger?.error(`Network error fetching flag definitions: ${err.message}`); - reject(err); - }); + const newDefinitions = new Map(); + response.flags.forEach(flag => { + newDefinitions.set(flag.key, flag); + }); - request.on('timeout', () => { - this.logger?.error(`Request timeout fetching flag definitions`); - request.destroy(); - reject(new Error('Request timeout')); - }); + this.flagDefinitions = newDefinitions; - request.end(); - }); + return response; } /** diff --git a/lib/flags/remote_flags.js b/lib/flags/remote_flags.js index 53ba798..ef5a0b1 100644 --- a/lib/flags/remote_flags.js +++ b/lib/flags/remote_flags.js @@ -10,16 +10,12 @@ * @typedef {import('./types').RemoteFlagsResponse} RemoteFlagsResponse */ -const https = require('https'); -const packageInfo = require('../../package.json'); +const FeatureFlagsProvider = require('./flags'); const { EXPOSURE_EVENT, - REQUEST_HEADERS, - prepareCommonQueryParams, - generateTraceparent, } = require('./utils'); -class RemoteFeatureFlagsProvider { +class RemoteFeatureFlagsProvider extends FeatureFlagsProvider { /** * @param {string} token - Mixpanel project token * @param {RemoteFlagsConfig} config - Remote flags configuration @@ -27,14 +23,23 @@ class RemoteFeatureFlagsProvider { * @param {CustomLogger} logger - Logger instance */ constructor(token, config, tracker, logger) { - this.token = token; - this.config = { + const mergedConfig = { api_host: 'api.mixpanel.com', request_timeout_in_seconds: 10, ...config, }; + + const providerConfig = { + token: token, + api_host: mergedConfig.api_host, + request_timeout_in_seconds: mergedConfig.request_timeout_in_seconds, + }; + + super(providerConfig, '/flags', logger); + + this.token = token; + this.config = mergedConfig; this.tracker = tracker; - this.logger = logger; } /** @@ -143,67 +148,15 @@ class RemoteFeatureFlagsProvider { * @returns {Promise} - API response containing flags dictionary */ _fetchFlags(context, flagKey = null) { - return new Promise((resolve, reject) => { - const commonParams = prepareCommonQueryParams(this.token, packageInfo.version); - const params = new URLSearchParams(commonParams); + const additionalParams = { + context: JSON.stringify(context), + }; - if (flagKey !== null) { - params.append('flag_key', flagKey); - } + if (flagKey !== null) { + additionalParams.flag_key = flagKey; + } - params.append('context', JSON.stringify(context)); - - const path = `/flags?${params.toString()}`; - - const requestOptions = { - host: this.config.api_host, - port: 443, - path: path, - method: 'GET', - headers: { - ...REQUEST_HEADERS, - 'Authorization': 'Basic ' + Buffer.from(this.token + ':').toString('base64'), - 'traceparent': generateTraceparent(), - }, - timeout: this.config.request_timeout_in_seconds * 1000, - }; - - const request = https.request(requestOptions, (res) => { - let data = ''; - - res.on('data', (chunk) => { - data += chunk; - }); - - res.on('end', () => { - if (res.statusCode !== 200) { - this.logger?.error(`HTTP ${res.statusCode} error fetching flags: ${data}`); - return reject(new Error(`HTTP ${res.statusCode}: ${data}`)); - } - - try { - const result = JSON.parse(data); - resolve(result); - } catch (parseErr) { - this.logger?.error(`Failed to parse JSON response: ${parseErr.message}`); - reject(parseErr); - } - }); - }); - - request.on('error', (err) => { - this.logger?.error(`Network error fetching flags: ${err.message}`); - reject(err); - }); - - request.on('timeout', () => { - this.logger?.error('Request timeout fetching flags'); - request.destroy(); - reject(new Error('Request timeout')); - }); - - request.end(); - }); + return this.callFlagsEndpoint(additionalParams); } _trackExposure(flagKey, selectedVariant, context, latencyMs=null) { diff --git a/lib/flags/utils.js b/lib/flags/utils.js index e6dded2..2f9f6c9 100644 --- a/lib/flags/utils.js +++ b/lib/flags/utils.js @@ -1,7 +1,6 @@ /** * Utility functions for Mixpanel feature flags */ - const crypto = require('crypto'); // Constants diff --git a/package-lock.json b/package-lock.json index 5be0e80..197839c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mixpanel", - "version": "0.18.1", + "version": "0.19.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "mixpanel", - "version": "0.18.1", + "version": "0.19.0", "license": "MIT", "dependencies": { "https-proxy-agent": "5.0.0" diff --git a/test/flags/utils.js b/test/flags/utils.js index 59a56e4..cbdd380 100644 --- a/test/flags/utils.js +++ b/test/flags/utils.js @@ -15,6 +15,12 @@ describe('Utils', function() { }); describe('normalizedHash', function() { + const expectValidHash = (hash) => { + expect(hash).to.be.a('number'); + expect(hash).to.be.at.least(0); + expect(hash).to.be.at.most(1); + }; + it('should match known test vectors', function() { const hash1 = normalizedHash("abc", "variant"); expect(hash1).equals(0.72) @@ -22,5 +28,81 @@ describe('Utils', function() { const hash2 = normalizedHash("def", "variant"); expect(hash2).equals(0.21, 0.01); }); + + it('should produce consistent results', function() { + const hash1 = normalizedHash("test_key", "salt"); + const hash2 = normalizedHash("test_key", "salt"); + const hash3 = normalizedHash("test_key", "salt"); + + expect(hash1).equals(hash2); + expect(hash2).equals(hash3); + }); + + it('should produce different hashes when salt is changed', function() { + const hash1 = normalizedHash("same_key", "salt1"); + const hash2 = normalizedHash("same_key", "salt2"); + const hash3 = normalizedHash("same_key", "different_salt"); + + expect(hash1).to.not.equal(hash2); + expect(hash1).to.not.equal(hash3); + expect(hash2).to.not.equal(hash3); + }); + + it('should produce different hashes when order is changed', function() { + const hash1 = normalizedHash("abc", "salt"); + const hash2 = normalizedHash("bac", "salt"); + const hash3 = normalizedHash("cba", "salt"); + + expect(hash1).to.not.equal(hash2); + expect(hash1).to.not.equal(hash3); + expect(hash2).to.not.equal(hash3); + }); + + describe('should handle edge cases with empty strings', function() { + const testCases = [ + { key: "", salt: "salt", description: "empty key" }, + { key: "key", salt: "", description: "empty salt" }, + { key: "", salt: "", description: "both empty" } + ]; + + testCases.forEach(({ key, salt, description }) => { + it(`should return valid hash for ${description}`, function() { + const hash = normalizedHash(key, salt); + expectValidHash(hash); + }); + }); + + it('empty strings in different positions should produce different results', function() { + const hash1 = normalizedHash("", "salt"); + const hash2 = normalizedHash("key", ""); + expect(hash1).to.not.equal(hash2); + }); + }); + + describe('should handle special characters', function() { + const testCases = [ + { key: "🎉", description: "emoji" }, + { key: "beyoncé", description: "accented characters" }, + { key: "key@#$%^&*()", description: "special symbols" }, + { key: "key with spaces", description: "spaces" } + ]; + + testCases.forEach(({ key, description }) => { + it(`should return valid hash for ${description}`, function() { + const hash = normalizedHash(key, "salt"); + expectValidHash(hash); + }); + }); + + it('different special characters should produce different results', function() { + const hashes = testCases.map(tc => normalizedHash(tc.key, "salt")); + + for (let i = 0; i < hashes.length; i++) { + for (let j = i + 1; j < hashes.length; j++) { + expect(hashes[i]).to.not.equal(hashes[j]); + } + } + }); + }); }); }); From 6ce48c9edc8a7cfa79e18fe0f84f3320dda8ab00 Mon Sep 17 00:00:00 2001 From: Kwame Efah Date: Mon, 10 Nov 2025 16:53:30 -0800 Subject: [PATCH 10/15] minor updates --- lib/flags/flags.d.ts | 2 +- lib/flags/flags.js | 16 +++++++++------- lib/flags/local_flags.js | 5 +---- lib/flags/remote_flags.js | 6 +----- 4 files changed, 12 insertions(+), 17 deletions(-) diff --git a/lib/flags/flags.d.ts b/lib/flags/flags.d.ts index b89b2c1..a08d8a4 100644 --- a/lib/flags/flags.d.ts +++ b/lib/flags/flags.d.ts @@ -18,7 +18,7 @@ export interface FeatureFlagsConfig { * Contains common methods for feature flag evaluation */ export class FeatureFlagsProvider { - config: FeatureFlagsConfig; + providerConfig: FeatureFlagsConfig; endpoint: string; logger: CustomLogger | null; diff --git a/lib/flags/flags.js b/lib/flags/flags.js index efef58b..37d6473 100644 --- a/lib/flags/flags.js +++ b/lib/flags/flags.js @@ -9,13 +9,15 @@ const { prepareCommonQueryParams, generateTraceparent, REQUEST_HEADERS } = requi class FeatureFlagsProvider { /** - * @param {Object} config - Configuration object with token, api_host, request_timeout_in_seconds + * @param {Object} providerConfig - Configuration object with token, api_host, request_timeout_in_seconds * @param {string} endpoint - API endpoint path (e.g., '/flags' or '/flags/definitions') + * @param {Function} tracker - Function to track events (signature: track(distinct_id, event, properties, callback)) * @param {CustomLogger} logger - Logger instance */ - constructor(config, endpoint, logger) { - this.config = config; + constructor(providerConfig, endpoint, tracker, logger) { + this.providerConfig = providerConfig; this.endpoint = endpoint; + this.tracker = tracker; this.logger = logger; } @@ -26,7 +28,7 @@ class FeatureFlagsProvider { */ async callFlagsEndpoint(additionalParams = null) { return new Promise((resolve, reject) => { - const commonParams = prepareCommonQueryParams(this.config.token, packageInfo.version); + const commonParams = prepareCommonQueryParams(this.providerConfig.token, packageInfo.version); const params = new URLSearchParams(commonParams); if (additionalParams) { @@ -38,16 +40,16 @@ class FeatureFlagsProvider { const path = `${this.endpoint}?${params.toString()}`; const requestOptions = { - host: this.config.api_host, + host: this.providerConfig.api_host, port: 443, path: path, method: 'GET', headers: { ...REQUEST_HEADERS, - 'Authorization': 'Basic ' + Buffer.from(this.config.token + ':').toString('base64'), + 'Authorization': 'Basic ' + Buffer.from(this.providerConfig.token + ':').toString('base64'), 'traceparent': generateTraceparent(), }, - timeout: this.config.request_timeout_in_seconds * 1000, + timeout: this.providerConfig.request_timeout_in_seconds * 1000, }; const request = https.request(requestOptions, (res) => { diff --git a/lib/flags/local_flags.js b/lib/flags/local_flags.js index 53925d2..5d30f66 100644 --- a/lib/flags/local_flags.js +++ b/lib/flags/local_flags.js @@ -39,12 +39,9 @@ class LocalFeatureFlagsProvider extends FeatureFlagsProvider { request_timeout_in_seconds: mergedConfig.request_timeout_in_seconds, }; - super(providerConfig, '/flags/definitions', logger); + super(providerConfig, '/flags/definitions', tracker, logger); - this.token = token; this.config = mergedConfig; - this.tracker = tracker; - this.flagDefinitions = new Map(); this.pollingInterval = null; } diff --git a/lib/flags/remote_flags.js b/lib/flags/remote_flags.js index ef5a0b1..b1ce03b 100644 --- a/lib/flags/remote_flags.js +++ b/lib/flags/remote_flags.js @@ -35,11 +35,7 @@ class RemoteFeatureFlagsProvider extends FeatureFlagsProvider { request_timeout_in_seconds: mergedConfig.request_timeout_in_seconds, }; - super(providerConfig, '/flags', logger); - - this.token = token; - this.config = mergedConfig; - this.tracker = tracker; + super(providerConfig, '/flags', tracker, logger); } /** From b743ca5b33ee63a8e580bb17eb6b1c806adf1cac Mon Sep 17 00:00:00 2001 From: Kwame Efah Date: Tue, 11 Nov 2025 16:33:53 -0800 Subject: [PATCH 11/15] refactor exposure tracking into base class & revert version update --- history.md | 4 --- lib/flags/flags.d.ts | 12 ++++++++ lib/flags/flags.js | 57 +++++++++++++++++++++++++++++++++++-- lib/flags/local_flags.d.ts | 13 --------- lib/flags/local_flags.js | 54 ++--------------------------------- lib/flags/remote_flags.d.ts | 13 --------- lib/flags/remote_flags.js | 57 ++----------------------------------- package.json | 2 +- test/flags/utils.js | 2 +- 9 files changed, 74 insertions(+), 140 deletions(-) diff --git a/history.md b/history.md index aeef7d1..98b0e7a 100644 --- a/history.md +++ b/history.md @@ -1,7 +1,3 @@ -0.19.0 / 2025-03-12 -================== -* adds initial support for feature flags - 0.18.1 / 2025-03-12 ================== * add secret to config types (thanks gierschv) diff --git a/lib/flags/flags.d.ts b/lib/flags/flags.d.ts index a08d8a4..8302667 100644 --- a/lib/flags/flags.d.ts +++ b/lib/flags/flags.d.ts @@ -3,6 +3,7 @@ */ import { CustomLogger } from '../mixpanel-node'; +import { SelectedVariant, FlagContext } from './types'; /** * Configuration for feature flags API requests @@ -35,4 +36,15 @@ export class FeatureFlagsProvider { * @returns Parsed JSON response */ callFlagsEndpoint(additionalParams?: Record | null): Promise; + + /** + * Manually tracks a feature flag exposure event to Mixpanel + * This provides flexibility for reporting individual exposure events when using getAllVariants + * If using getVariantValue or getVariant, exposure events are tracked automatically by default. + * @param {string} flagKey - The key of the feature flag + * @param {SelectedVariant} variant - The selected variant for the feature flag + * @param {FlagContext} context - The user context used to evaluate the feature flag + * @param {number|null} latencyMs - Optionally included latency in milliseconds that assignment took. + */ + trackExposureEvent(flagKey: string, variant: SelectedVariant, context: FlagContext, latencyMs?: number | null): void; } diff --git a/lib/flags/flags.js b/lib/flags/flags.js index 37d6473..3cb1a62 100644 --- a/lib/flags/flags.js +++ b/lib/flags/flags.js @@ -5,19 +5,25 @@ const https = require('https'); const packageInfo = require('../../package.json'); -const { prepareCommonQueryParams, generateTraceparent, REQUEST_HEADERS } = require('./utils'); +const { prepareCommonQueryParams, generateTraceparent, EXPOSURE_EVENT, REQUEST_HEADERS } = require('./utils'); +/** + * @typedef {import('./types').SelectedVariant} SelectedVariant + * @typedef {import('./types').FlagContext} FlagContext + */ class FeatureFlagsProvider { /** * @param {Object} providerConfig - Configuration object with token, api_host, request_timeout_in_seconds * @param {string} endpoint - API endpoint path (e.g., '/flags' or '/flags/definitions') * @param {Function} tracker - Function to track events (signature: track(distinct_id, event, properties, callback)) + * @param {string} evaluationMode - The feature flag evaluation mode * @param {CustomLogger} logger - Logger instance */ - constructor(providerConfig, endpoint, tracker, logger) { + constructor(providerConfig, endpoint, tracker, evaluationMode, logger) { this.providerConfig = providerConfig; this.endpoint = endpoint; this.tracker = tracker; + this.evaluationMode = evaluationMode; this.logger = logger; } @@ -89,6 +95,53 @@ class FeatureFlagsProvider { request.end(); }); } + + /** + * Manually tracks a feature flag exposure event to Mixpanel + * This provides flexibility for reporting individual exposure events when using getAllVariants + * If using getVariantValue or getVariant, exposure events are tracked automatically by default. + * @param {string} flagKey - The key of the feature flag + * @param {SelectedVariant} variant - The selected variant for the feature flag + * @param {FlagContext} context - The user context used to evaluate the feature flag + * @param {number|null} latencyMs - Optionally included latency in milliseconds that assignment took. + */ + trackExposureEvent(flagKey, selectedVariant, context, latencyMs=null) { + if (!context.distinct_id) { + this.logger?.error('Cannot track exposure event without a distinct_id in the context'); + return; + } + + const properties = { + 'distinct_id': context.distinct_id, + 'Experiment name': flagKey, + 'Variant name': selectedVariant.variant_key, + '$experiment_type': 'feature_flag', + 'Flag evaluation mode': this.evaluationMode + }; + + if (latencyMs !== null && latencyMs !== undefined) { + properties['Variant fetch latency (ms)'] = latencyMs; + } + + if (selectedVariant.experiment_id !== undefined) { + properties['$experiment_id'] = selectedVariant.experiment_id; + } + + if (selectedVariant.is_experiment_active !== undefined) { + properties['$is_experiment_active'] = selectedVariant.is_experiment_active; + } + + if (selectedVariant.is_qa_tester !== undefined) { + properties['$is_qa_tester'] = selectedVariant.is_qa_tester; + } + + // Use the tracker function provided (bound to the main mixpanel instance) + this.tracker(EXPOSURE_EVENT, properties, (err) => { + if (err) { + this.logger?.error(`[flags]Failed to track exposure event for flag '${flagKey}': ${err.message}`); + } + }); + } } module.exports = FeatureFlagsProvider; diff --git a/lib/flags/local_flags.d.ts b/lib/flags/local_flags.d.ts index 6acd487..728e45a 100644 --- a/lib/flags/local_flags.d.ts +++ b/lib/flags/local_flags.d.ts @@ -77,17 +77,4 @@ export default class LocalFeatureFlagsProvider { getAllVariants( context: FlagContext ): {[key: string]: SelectedVariant}; - - /** - * Manually tracks a feature flag exposure event to Mixpanel - * This provides flexibility for reporting individual exposure events when using getAllVariants - * @param flagKey - The key of the feature flag - * @param variant - The selected variant for the feature flag - * @param context - The user context used to evaluate the feature flag - */ - trackExposureEvent( - flagKey: string, - variant: SelectedVariant, - context: FlagContext - ): void; } diff --git a/lib/flags/local_flags.js b/lib/flags/local_flags.js index 5d30f66..39ae1d5 100644 --- a/lib/flags/local_flags.js +++ b/lib/flags/local_flags.js @@ -12,10 +12,7 @@ * */ const FeatureFlagsProvider = require('./flags'); -const { - EXPOSURE_EVENT, - normalizedHash, -} = require('./utils'); +const { normalizedHash } = require('./utils'); class LocalFeatureFlagsProvider extends FeatureFlagsProvider { /** @@ -39,7 +36,7 @@ class LocalFeatureFlagsProvider extends FeatureFlagsProvider { request_timeout_in_seconds: mergedConfig.request_timeout_in_seconds, }; - super(providerConfig, '/flags/definitions', tracker, logger); + super(providerConfig, '/flags/definitions', tracker, 'local', logger); this.config = mergedConfig; this.flagDefinitions = new Map(); @@ -147,7 +144,7 @@ class LocalFeatureFlagsProvider extends FeatureFlagsProvider { if (selectedVariant) { if (reportExposure) { - this._trackExposure(flagKey, selectedVariant, context); + this.trackExposureEvent(flagKey, selectedVariant, context); } return selectedVariant; } @@ -174,17 +171,6 @@ class LocalFeatureFlagsProvider extends FeatureFlagsProvider { return variants; } - /** - * Manually tracks a feature flag exposure event to Mixpanel - * This provides flexibility for reporting individual exposure events when using getAllVariants - * @param {string} flagKey - The key of the feature flag - * @param {SelectedVariant} variant - The selected variant for the feature flag - * @param {FlagContext} context - The user context used to evaluate the feature flag - */ - trackExposureEvent(flagKey, variant, context) { - this._trackExposure(flagKey, variant, context); - } - /** * Fetch flag definitions from API. * @returns {Promise} @@ -327,42 +313,8 @@ class LocalFeatureFlagsProvider extends FeatureFlagsProvider { return false; } } - return true; } - - _trackExposure(flagKey, selectedVariant, context) { - if (!context.distinct_id) { - this.logger?.error('Cannot track exposure event without a distinct_id in the context'); - return; - } - - const properties = { - 'distinct_id': context.distinct_id, - 'Experiment name': flagKey, - 'Variant name': selectedVariant.variant_key, - '$experiment_type': 'feature_flag', - 'Flag evaluation mode': 'local', - }; - - if (selectedVariant.experiment_id !== undefined) { - properties['$experiment_id'] = selectedVariant.experiment_id; - } - - if (selectedVariant.is_experiment_active !== undefined) { - properties['$is_experiment_active'] = selectedVariant.is_experiment_active; - } - - if (selectedVariant.is_qa_tester !== undefined) { - properties['$is_qa_tester'] = selectedVariant.is_qa_tester; - } - - this.tracker(EXPOSURE_EVENT, properties, (err) => { - if (err) { - this.logger?.error(`Failed to track exposure event for flag '${flagKey}': ${err.message}`); - } - }); - } } module.exports = LocalFeatureFlagsProvider; diff --git a/lib/flags/remote_flags.d.ts b/lib/flags/remote_flags.d.ts index ce6653c..5652736 100644 --- a/lib/flags/remote_flags.d.ts +++ b/lib/flags/remote_flags.d.ts @@ -69,17 +69,4 @@ export default class RemoteFeatureFlagsProvider { getAllVariants( context: FlagContext ): Promise<{[key: string]: SelectedVariant} | null>; - - /** - * Manually tracks a feature flag exposure event to Mixpanel - * This provides flexibility for reporting individual exposure events when using getAllVariants - * @param flagKey - The key of the feature flag - * @param variant - The selected variant for the feature flag - * @param context - The user context used to evaluate the feature flag - */ - trackExposureEvent( - flagKey: string, - variant: SelectedVariant, - context: FlagContext - ): void; } diff --git a/lib/flags/remote_flags.js b/lib/flags/remote_flags.js index b1ce03b..d4c3e36 100644 --- a/lib/flags/remote_flags.js +++ b/lib/flags/remote_flags.js @@ -11,9 +11,6 @@ */ const FeatureFlagsProvider = require('./flags'); -const { - EXPOSURE_EVENT, -} = require('./utils'); class RemoteFeatureFlagsProvider extends FeatureFlagsProvider { /** @@ -35,7 +32,7 @@ class RemoteFeatureFlagsProvider extends FeatureFlagsProvider { request_timeout_in_seconds: mergedConfig.request_timeout_in_seconds, }; - super(providerConfig, '/flags', tracker, logger); + super(providerConfig, '/flags', tracker, 'remote', logger); } /** @@ -81,7 +78,7 @@ class RemoteFeatureFlagsProvider extends FeatureFlagsProvider { } if (reportExposure) { - this._trackExposure(flagKey, selectedVariant, context, latencyMs); + this.trackExposureEvent(flagKey, selectedVariant, context, latencyMs); } return selectedVariant; @@ -125,18 +122,6 @@ class RemoteFeatureFlagsProvider extends FeatureFlagsProvider { } } - /** - * Manually tracks a feature flag exposure event to Mixpanel - * This provides flexibility for reporting individual exposure events when using getAllVariants - * If using getVariantValue or getVariant, exposure events are tracked automatically by default. - * @param {string} flagKey - The key of the feature flag - * @param {SelectedVariant} variant - The selected variant for the feature flag - * @param {FlagContext} context - The user context used to evaluate the feature flag - */ - trackExposureEvent(flagKey, variant, context) { - this._trackExposure(flagKey, variant, context, null); - } - /** * Fetch flags from remote flags evaluation API * @param {FlagContext} context - Evaluation context @@ -154,44 +139,6 @@ class RemoteFeatureFlagsProvider extends FeatureFlagsProvider { return this.callFlagsEndpoint(additionalParams); } - - _trackExposure(flagKey, selectedVariant, context, latencyMs=null) { - if (!context.distinct_id) { - this.logger?.error('Cannot track exposure event without a distinct_id in the context'); - return; - } - - const properties = { - 'distinct_id': context.distinct_id, - 'Experiment name': flagKey, - 'Variant name': selectedVariant.variant_key, - '$experiment_type': 'feature_flag', - 'Flag evaluation mode': 'remote', - }; - - if (latencyMs !== null && latencyMs !== undefined) { - properties['Variant fetch latency (ms)'] = latencyMs; - } - - if (selectedVariant.experiment_id !== undefined) { - properties['$experiment_id'] = selectedVariant.experiment_id; - } - - if (selectedVariant.is_experiment_active !== undefined) { - properties['$is_experiment_active'] = selectedVariant.is_experiment_active; - } - - if (selectedVariant.is_qa_tester !== undefined) { - properties['$is_qa_tester'] = selectedVariant.is_qa_tester; - } - - // Use the tracker function provided (bound to the main mixpanel instance) - this.tracker(EXPOSURE_EVENT, properties, (err) => { - if (err) { - this.logger?.error(`[flags]Failed to track exposure event for flag '${flagKey}': ${err.message}`); - } - }); - } } module.exports = RemoteFeatureFlagsProvider; diff --git a/package.json b/package.json index 46a56cb..6e2b94b 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "api", "stats" ], - "version": "0.19.0", + "version": "0.18.1", "homepage": "https://github.com/mixpanel/mixpanel-node", "author": "Carl Sverre", "license": "MIT", diff --git a/test/flags/utils.js b/test/flags/utils.js index cbdd380..118b0b3 100644 --- a/test/flags/utils.js +++ b/test/flags/utils.js @@ -94,7 +94,7 @@ describe('Utils', function() { }); }); - it('different special characters should produce different results', function() { + it('produces different results for different special characters', function() { const hashes = testCases.map(tc => normalizedHash(tc.key, "salt")); for (let i = 0; i < hashes.length; i++) { From 7c2a1d199fde5d2792f5adcffdd8aa033e193fda Mon Sep 17 00:00:00 2001 From: Kwame Efah Date: Wed, 12 Nov 2025 10:33:06 -0800 Subject: [PATCH 12/15] remove bad expect chai import --- test/flags/utils.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/flags/utils.js b/test/flags/utils.js index 118b0b3..222c0bc 100644 --- a/test/flags/utils.js +++ b/test/flags/utils.js @@ -1,4 +1,3 @@ -const { expect } = require('chai'); const { generateTraceparent, normalizedHash From b2874d33d7d8c838a3f0bddb3e6bea2d26b3ebc3 Mon Sep 17 00:00:00 2001 From: Kwame Efah Date: Wed, 12 Nov 2025 10:35:53 -0800 Subject: [PATCH 13/15] removing version bump from packages lock file --- package-lock.json | 3617 ++++++++++----------------------------------- 1 file changed, 782 insertions(+), 2835 deletions(-) diff --git a/package-lock.json b/package-lock.json index 197839c..8d1a7b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mixpanel", - "version": "0.19.0", - "lockfileVersion": 2, + "version": "0.18.1", + "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mixpanel", - "version": "0.19.0", + "version": "0.18.1", "license": "MIT", "dependencies": { "https-proxy-agent": "5.0.0" @@ -23,9 +23,8 @@ }, "node_modules/@ampproject/remapping": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" @@ -36,27 +35,24 @@ }, "node_modules/@babel/helper-string-parser": { "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", - "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", - "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", "dev": true, + "license": "MIT", "bin": { "parser": "bin/babel-parser.js" }, @@ -66,9 +62,8 @@ }, "node_modules/@babel/types": { "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", - "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.24.7", "@babel/helper-validator-identifier": "^7.24.7", @@ -80,82 +75,16 @@ }, "node_modules/@bcoe/v8-coverage": { "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", - "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", - "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", - "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", - "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", - "cpu": [ - "x64" - ], "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } + "license": "MIT" }, "node_modules/@esbuild/darwin-arm64": { "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", - "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -164,308 +93,18 @@ "node": ">=12" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", - "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", - "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", - "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", - "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", - "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", - "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", - "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", - "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", - "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", - "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", - "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", - "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", - "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", - "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", - "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", - "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", - "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", - "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/@jest/schemas": { "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", "dev": true, + "license": "MIT", "dependencies": { "@sinclair/typebox": "^0.27.8" }, @@ -475,9 +114,8 @@ }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -489,33 +127,29 @@ }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/set-array": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.0.0" } }, "node_modules/@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 + "dev": true, + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -523,8 +157,6 @@ }, "node_modules/@mswjs/interceptors": { "version": "0.39.8", - "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.39.8.tgz", - "integrity": "sha512-2+BzZbjRO7Ct61k8fMNHEtoKjeWI9pIlHFTqBwZ5icHpqszIgEZbjb1MW5Z0+bITTCTl3gk4PDBxs9tA/csXvA==", "dev": true, "license": "MIT", "dependencies": { @@ -541,2006 +173,50 @@ }, "node_modules/@open-draft/deferred-promise": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", - "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", "dev": true, "license": "MIT" }, "node_modules/@open-draft/logger": { "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", - "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", "dev": true, "license": "MIT", "dependencies": { "is-node-process": "^1.2.0", "outvariant": "^1.4.0" - } - }, - "node_modules/@open-draft/until": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", - "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", - "integrity": "sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz", - "integrity": "sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz", - "integrity": "sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz", - "integrity": "sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz", - "integrity": "sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz", - "integrity": "sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz", - "integrity": "sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz", - "integrity": "sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz", - "integrity": "sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz", - "integrity": "sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz", - "integrity": "sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz", - "integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz", - "integrity": "sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz", - "integrity": "sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz", - "integrity": "sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz", - "integrity": "sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true - }, - "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "dev": true - }, - "node_modules/@vitest/coverage-v8": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.6.0.tgz", - "integrity": "sha512-KvapcbMY/8GYIG0rlwwOKCVNRc0OL20rrhFkg/CHNzncV03TE2XWvO5w9uZYoxNiMEBacAJt3unSOiZ7svePew==", - "dev": true, - "dependencies": { - "@ampproject/remapping": "^2.2.1", - "@bcoe/v8-coverage": "^0.2.3", - "debug": "^4.3.4", - "istanbul-lib-coverage": "^3.2.2", - "istanbul-lib-report": "^3.0.1", - "istanbul-lib-source-maps": "^5.0.4", - "istanbul-reports": "^3.1.6", - "magic-string": "^0.30.5", - "magicast": "^0.3.3", - "picocolors": "^1.0.0", - "std-env": "^3.5.0", - "strip-literal": "^2.0.0", - "test-exclude": "^6.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "vitest": "1.6.0" - } - }, - "node_modules/@vitest/coverage-v8/node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@vitest/expect": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.0.tgz", - "integrity": "sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==", - "dev": true, - "dependencies": { - "@vitest/spy": "1.6.0", - "@vitest/utils": "1.6.0", - "chai": "^4.3.10" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.0.tgz", - "integrity": "sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==", - "dev": true, - "dependencies": { - "@vitest/utils": "1.6.0", - "p-limit": "^5.0.0", - "pathe": "^1.1.1" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.0.tgz", - "integrity": "sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==", - "dev": true, - "dependencies": { - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "pretty-format": "^29.7.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.0.tgz", - "integrity": "sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==", - "dev": true, - "dependencies": { - "tinyspy": "^2.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.0.tgz", - "integrity": "sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==", - "dev": true, - "dependencies": { - "diff-sequences": "^29.6.3", - "estree-walker": "^3.0.3", - "loupe": "^2.3.7", - "pretty-format": "^29.7.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", - "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/agent-base": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.1.tgz", - "integrity": "sha512-01q25QQDwLSsyfhrKbn8yuur+JNw0H+0Y4JiGIKd3z9aYk/w/2kxD/Upc+t2ZBBSUNff50VjPsSW2YxM8QYKVg==", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/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, - "engines": { - "node": "*" - } - }, - "node_modules/balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "node_modules/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, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/chai": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", - "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", - "dev": true, - "dependencies": { - "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" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/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, - "dependencies": { - "get-func-name": "^2.0.2" - }, - "engines": { - "node": "*" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "node_modules/confbox": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.7.tgz", - "integrity": "sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==", - "dev": true - }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/debug/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/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, - "dependencies": { - "type-detect": "^4.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/esbuild": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", - "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.20.2", - "@esbuild/android-arm": "0.20.2", - "@esbuild/android-arm64": "0.20.2", - "@esbuild/android-x64": "0.20.2", - "@esbuild/darwin-arm64": "0.20.2", - "@esbuild/darwin-x64": "0.20.2", - "@esbuild/freebsd-arm64": "0.20.2", - "@esbuild/freebsd-x64": "0.20.2", - "@esbuild/linux-arm": "0.20.2", - "@esbuild/linux-arm64": "0.20.2", - "@esbuild/linux-ia32": "0.20.2", - "@esbuild/linux-loong64": "0.20.2", - "@esbuild/linux-mips64el": "0.20.2", - "@esbuild/linux-ppc64": "0.20.2", - "@esbuild/linux-riscv64": "0.20.2", - "@esbuild/linux-s390x": "0.20.2", - "@esbuild/linux-x64": "0.20.2", - "@esbuild/netbsd-x64": "0.20.2", - "@esbuild/openbsd-x64": "0.20.2", - "@esbuild/sunos-x64": "0.20.2", - "@esbuild/win32-arm64": "0.20.2", - "@esbuild/win32-ia32": "0.20.2", - "@esbuild/win32-x64": "0.20.2" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/execa": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", - "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^8.0.1", - "human-signals": "^5.0.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/execa/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==", - "dev": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/execa/node_modules/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, - "engines": { - "node": ">=8" - } - }, - "node_modules/execa/node_modules/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, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/execa/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/fill-keys": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/fill-keys/-/fill-keys-1.0.2.tgz", - "integrity": "sha1-mo+jb06K1jTjv2tPPIiCVRRS6yA=", - "dev": true, - "dependencies": { - "is-object": "~1.0.1", - "merge-descriptors": "~1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/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, - "engines": { - "node": "*" - } - }, - "node_modules/get-stream": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", - "dev": true, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true - }, - "node_modules/https-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", - "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/human-signals": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", - "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", - "dev": true, - "engines": { - "node": ">=16.17.0" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, - "node_modules/is-node-process": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", - "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz", - "integrity": "sha1-iVJojF7C/9awPsyF52ngKQMINHA=", - "dev": true - }, - "node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-report/node_modules/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, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report/node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report/node_modules/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, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-source-maps": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.4.tgz", - "integrity": "sha512-wHOoEsNJTVltaJp8eVkm8w+GVkVNHT2YDYo53YdzQEL2gWm1hBX5cGFR9hQJtuGLebidVX7et3+dmDZrmclduw==", - "dev": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.23", - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-source-maps/node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-reports": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", - "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", - "dev": true, - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/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, - "license": "ISC" - }, - "node_modules/local-pkg": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", - "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==", - "dev": true, - "dependencies": { - "mlly": "^1.4.2", - "pkg-types": "^1.0.3" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/loupe": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", - "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", - "dev": true, - "dependencies": { - "get-func-name": "^2.0.1" - } - }, - "node_modules/magic-string": { - "version": "0.30.10", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz", - "integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" - } - }, - "node_modules/magicast": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.4.tgz", - "integrity": "sha512-TyDF/Pn36bBji9rWKHlZe+PZb6Mx5V8IHCSxk7X4aljM4e/vyDvZZYwHewdVaqiA0nb3ghfHU/6AUpDxWoER2Q==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.24.4", - "@babel/types": "^7.24.0", - "source-map-js": "^1.2.0" - } - }, - "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", - "dev": true - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "node_modules/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/mlly": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.0.tgz", - "integrity": "sha512-U9SDaXGEREBYQgfejV97coK0UL1r+qnF2SyO9A3qcI8MzKnsIFKHNVEkrDyNncQTKQQumsasmeq84eNMdBfsNQ==", - "dev": true, - "dependencies": { - "acorn": "^8.11.3", - "pathe": "^1.1.2", - "pkg-types": "^1.1.0", - "ufo": "^1.5.3" - } - }, - "node_modules/module-not-found-error": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/module-not-found-error/-/module-not-found-error-1.0.1.tgz", - "integrity": "sha1-z4tP9PKWQGdNbN0CsOO8UjwrvcA=", - "dev": true - }, - "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/nock": { - "version": "14.0.10", - "resolved": "https://registry.npmjs.org/nock/-/nock-14.0.10.tgz", - "integrity": "sha512-Q7HjkpyPeLa0ZVZC5qpxBt5EyLczFJ91MEewQiIi9taWuA0KB/MDJlUWtON+7dGouVdADTQsf9RA7TZk6D8VMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@mswjs/interceptors": "^0.39.5", - "json-stringify-safe": "^5.0.1", - "propagate": "^2.0.0" - }, - "engines": { - "node": ">=18.20.0 <20 || >=20.12.1" - } - }, - "node_modules/npm-run-path": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", - "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", - "dev": true, - "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dev": true, - "dependencies": { - "mimic-fn": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/outvariant": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", - "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", - "dev": true, - "license": "MIT" - }, - "node_modules/p-limit": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", - "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true - }, - "node_modules/pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", - "dev": true - }, - "node_modules/pkg-types": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.1.1.tgz", - "integrity": "sha512-ko14TjmDuQJ14zsotODv7dBlwxKhUKQEhuhmbqo1uCi9BB0Z2alo/wAXg6q1dTR5TyuqYyWhjtfe/Tsh+X28jQ==", - "dev": true, - "dependencies": { - "confbox": "^0.1.7", - "mlly": "^1.7.0", - "pathe": "^1.1.2" - } - }, - "node_modules/postcss": { - "version": "8.4.38", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", - "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/propagate": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", - "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/proxyquire": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/proxyquire/-/proxyquire-1.8.0.tgz", - "integrity": "sha1-AtUUpb7ZhvBMuyCTrxZ0FTX3ntw=", - "dev": true, - "dependencies": { - "fill-keys": "^1.0.2", - "module-not-found-error": "^1.0.0", - "resolve": "~1.1.7" - } - }, - "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true - }, - "node_modules/resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", - "dev": true - }, - "node_modules/rollup": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz", - "integrity": "sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==", - "dev": true, - "dependencies": { - "@types/estree": "1.0.5" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.18.0", - "@rollup/rollup-android-arm64": "4.18.0", - "@rollup/rollup-darwin-arm64": "4.18.0", - "@rollup/rollup-darwin-x64": "4.18.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.18.0", - "@rollup/rollup-linux-arm-musleabihf": "4.18.0", - "@rollup/rollup-linux-arm64-gnu": "4.18.0", - "@rollup/rollup-linux-arm64-musl": "4.18.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.18.0", - "@rollup/rollup-linux-riscv64-gnu": "4.18.0", - "@rollup/rollup-linux-s390x-gnu": "4.18.0", - "@rollup/rollup-linux-x64-gnu": "4.18.0", - "@rollup/rollup-linux-x64-musl": "4.18.0", - "@rollup/rollup-win32-arm64-msvc": "4.18.0", - "@rollup/rollup-win32-ia32-msvc": "4.18.0", - "@rollup/rollup-win32-x64-msvc": "4.18.0", - "fsevents": "~2.3.2" - } - }, - "node_modules/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, - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/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, - "engines": { - "node": ">=8" - } - }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true - }, - "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true - }, - "node_modules/std-env": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz", - "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", - "dev": true - }, - "node_modules/strict-event-emitter": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", - "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strip-literal": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.0.tgz", - "integrity": "sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==", - "dev": true, - "dependencies": { - "js-tokens": "^9.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/strip-literal/node_modules/js-tokens": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.0.tgz", - "integrity": "sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ==", - "dev": true - }, - "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tinybench": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.8.0.tgz", - "integrity": "sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==", - "dev": true - }, - "node_modules/tinypool": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", - "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", - "dev": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", - "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", - "dev": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/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, - "engines": { - "node": ">=4" - } - }, - "node_modules/ufo": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.3.tgz", - "integrity": "sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==", - "dev": true - }, - "node_modules/vite": { - "version": "5.2.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz", - "integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==", - "dev": true, - "dependencies": { - "esbuild": "^0.20.1", - "postcss": "^8.4.38", - "rollup": "^4.13.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/vite-node": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.0.tgz", - "integrity": "sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==", - "dev": true, - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.3.4", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "vite": "^5.0.0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vitest": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.0.tgz", - "integrity": "sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==", - "dev": true, - "dependencies": { - "@vitest/expect": "1.6.0", - "@vitest/runner": "1.6.0", - "@vitest/snapshot": "1.6.0", - "@vitest/spy": "1.6.0", - "@vitest/utils": "1.6.0", - "acorn-walk": "^8.3.2", - "chai": "^4.3.10", - "debug": "^4.3.4", - "execa": "^8.0.1", - "local-pkg": "^0.5.0", - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "std-env": "^3.5.0", - "strip-literal": "^2.0.0", - "tinybench": "^2.5.1", - "tinypool": "^0.8.3", - "vite": "^5.0.0", - "vite-node": "1.6.0", - "why-is-node-running": "^2.2.2" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "1.6.0", - "@vitest/ui": "1.6.0", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } - } - }, - "node_modules/why-is-node-running": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", - "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==", - "dev": true, - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "node_modules/yocto-queue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", - "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - }, - "dependencies": { - "@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "requires": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "@babel/helper-string-parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", - "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", - "dev": true - }, - "@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", - "dev": true - }, - "@babel/parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", - "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", - "dev": true - }, - "@babel/types": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", - "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", - "dev": true, - "requires": { - "@babel/helper-string-parser": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7", - "to-fast-properties": "^2.0.0" - } - }, - "@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true - }, - "@esbuild/aix-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", - "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", - "dev": true, - "optional": true - }, - "@esbuild/android-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", - "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", - "dev": true, - "optional": true - }, - "@esbuild/android-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", - "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", - "dev": true, - "optional": true - }, - "@esbuild/android-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", - "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", - "dev": true, - "optional": true - }, - "@esbuild/darwin-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", - "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", - "dev": true, - "optional": true - }, - "@esbuild/darwin-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", - "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", - "dev": true, - "optional": true - }, - "@esbuild/freebsd-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", - "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", - "dev": true, - "optional": true - }, - "@esbuild/freebsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", - "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", - "dev": true, - "optional": true - }, - "@esbuild/linux-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", - "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", - "dev": true, - "optional": true - }, - "@esbuild/linux-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", - "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", - "dev": true, - "optional": true - }, - "@esbuild/linux-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", - "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", - "dev": true, - "optional": true - }, - "@esbuild/linux-loong64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", - "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", - "dev": true, - "optional": true - }, - "@esbuild/linux-mips64el": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", - "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", - "dev": true, - "optional": true - }, - "@esbuild/linux-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", - "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", - "dev": true, - "optional": true - }, - "@esbuild/linux-riscv64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", - "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", - "dev": true, - "optional": true - }, - "@esbuild/linux-s390x": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", - "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", - "dev": true, - "optional": true - }, - "@esbuild/linux-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", - "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", - "dev": true, - "optional": true - }, - "@esbuild/netbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", - "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", - "dev": true, - "optional": true - }, - "@esbuild/openbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", - "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", - "dev": true, - "optional": true - }, - "@esbuild/sunos-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", - "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", - "dev": true, - "optional": true - }, - "@esbuild/win32-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", - "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", - "dev": true, - "optional": true - }, - "@esbuild/win32-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", - "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", - "dev": true, - "optional": true - }, - "@esbuild/win32-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", - "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", - "dev": true, - "optional": true - }, - "@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true - }, - "@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "requires": { - "@sinclair/typebox": "^0.27.8" - } - }, - "@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", - "dev": true, - "requires": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true - }, - "@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true - }, - "@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 - }, - "@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, - "requires": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "@mswjs/interceptors": { - "version": "0.39.8", - "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.39.8.tgz", - "integrity": "sha512-2+BzZbjRO7Ct61k8fMNHEtoKjeWI9pIlHFTqBwZ5icHpqszIgEZbjb1MW5Z0+bITTCTl3gk4PDBxs9tA/csXvA==", - "dev": true, - "requires": { - "@open-draft/deferred-promise": "^2.2.0", - "@open-draft/logger": "^0.3.0", - "@open-draft/until": "^2.0.0", - "is-node-process": "^1.2.0", - "outvariant": "^1.4.3", - "strict-event-emitter": "^0.5.1" - } - }, - "@open-draft/deferred-promise": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", - "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", - "dev": true - }, - "@open-draft/logger": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", - "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", - "dev": true, - "requires": { - "is-node-process": "^1.2.0", - "outvariant": "^1.4.0" - } - }, - "@open-draft/until": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", - "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", - "dev": true - }, - "@rollup/rollup-android-arm-eabi": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", - "integrity": "sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==", - "dev": true, - "optional": true - }, - "@rollup/rollup-android-arm64": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz", - "integrity": "sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==", - "dev": true, - "optional": true - }, - "@rollup/rollup-darwin-arm64": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz", - "integrity": "sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==", - "dev": true, - "optional": true - }, - "@rollup/rollup-darwin-x64": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz", - "integrity": "sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz", - "integrity": "sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-arm-musleabihf": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz", - "integrity": "sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-arm64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz", - "integrity": "sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-arm64-musl": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz", - "integrity": "sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz", - "integrity": "sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-riscv64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz", - "integrity": "sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-s390x-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz", - "integrity": "sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-x64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz", - "integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-x64-musl": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz", - "integrity": "sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==", - "dev": true, - "optional": true - }, - "@rollup/rollup-win32-arm64-msvc": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz", - "integrity": "sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==", - "dev": true, - "optional": true + } }, - "@rollup/rollup-win32-ia32-msvc": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz", - "integrity": "sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==", + "node_modules/@open-draft/until": { + "version": "2.1.0", "dev": true, - "optional": true + "license": "MIT" }, - "@rollup/rollup-win32-x64-msvc": { + "node_modules/@rollup/rollup-darwin-arm64": { "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz", - "integrity": "sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==", + "cpu": [ + "arm64" + ], "dev": true, - "optional": true + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] }, - "@sinclair/typebox": { + "node_modules/@sinclair/typebox": { "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true + "dev": true, + "license": "MIT" }, - "@types/estree": { + "node_modules/@types/estree": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "dev": true + "dev": true, + "license": "MIT" }, - "@vitest/coverage-v8": { + "node_modules/@vitest/coverage-v8": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.6.0.tgz", - "integrity": "sha512-KvapcbMY/8GYIG0rlwwOKCVNRc0OL20rrhFkg/CHNzncV03TE2XWvO5w9uZYoxNiMEBacAJt3unSOiZ7svePew==", "dev": true, - "requires": { + "license": "MIT", + "dependencies": { "@ampproject/remapping": "^2.2.1", "@bcoe/v8-coverage": "^0.2.3", "debug": "^4.3.4", @@ -2555,123 +231,149 @@ "strip-literal": "^2.0.0", "test-exclude": "^6.0.0" }, - "dependencies": { - "istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true - } + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "1.6.0" + } + }, + "node_modules/@vitest/coverage-v8/node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" } }, - "@vitest/expect": { + "node_modules/@vitest/expect": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.0.tgz", - "integrity": "sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==", "dev": true, - "requires": { + "license": "MIT", + "dependencies": { "@vitest/spy": "1.6.0", "@vitest/utils": "1.6.0", "chai": "^4.3.10" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "@vitest/runner": { + "node_modules/@vitest/runner": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.0.tgz", - "integrity": "sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==", "dev": true, - "requires": { + "license": "MIT", + "dependencies": { "@vitest/utils": "1.6.0", "p-limit": "^5.0.0", "pathe": "^1.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "@vitest/snapshot": { + "node_modules/@vitest/snapshot": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.0.tgz", - "integrity": "sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==", "dev": true, - "requires": { + "license": "MIT", + "dependencies": { "magic-string": "^0.30.5", "pathe": "^1.1.1", "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "@vitest/spy": { + "node_modules/@vitest/spy": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.0.tgz", - "integrity": "sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==", "dev": true, - "requires": { + "license": "MIT", + "dependencies": { "tinyspy": "^2.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "@vitest/utils": { + "node_modules/@vitest/utils": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.0.tgz", - "integrity": "sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==", "dev": true, - "requires": { + "license": "MIT", + "dependencies": { "diff-sequences": "^29.6.3", "estree-walker": "^3.0.3", "loupe": "^2.3.7", "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "acorn": { + "node_modules/acorn": { "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", - "dev": true + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } }, - "acorn-walk": { + "node_modules/acorn-walk": { "version": "8.3.2", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", - "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", - "dev": true + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } }, - "agent-base": { + "node_modules/agent-base": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.1.tgz", - "integrity": "sha512-01q25QQDwLSsyfhrKbn8yuur+JNw0H+0Y4JiGIKd3z9aYk/w/2kxD/Upc+t2ZBBSUNff50VjPsSW2YxM8QYKVg==", - "requires": { + "license": "MIT", + "dependencies": { "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" } }, - "assertion-error": { + "node_modules/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 + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } }, - "balanced-match": { + "node_modules/balanced-match": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true + "dev": true, + "license": "MIT" }, - "brace-expansion": { + "node_modules/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": { + "license": "MIT", + "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, - "cac": { + "node_modules/cac": { "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } }, - "chai": { + "node_modules/chai": { "version": "4.4.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", - "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", "dev": true, - "requires": { + "license": "MIT", + "dependencies": { "assertion-error": "^1.1.0", "check-error": "^1.0.3", "deep-eql": "^4.1.3", @@ -2679,65 +381,82 @@ "loupe": "^2.3.6", "pathval": "^1.1.1", "type-detect": "^4.0.8" + }, + "engines": { + "node": ">=4" } }, - "check-error": { + "node_modules/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": { + "license": "MIT", + "dependencies": { "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" } }, - "concat-map": { + "node_modules/concat-map": { "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "dev": true, + "license": "MIT" }, - "confbox": { + "node_modules/confbox": { "version": "0.1.7", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.7.tgz", - "integrity": "sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==", - "dev": true + "dev": true, + "license": "MIT" }, - "debug": { + "node_modules/debug": { "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "requires": { + "license": "MIT", + "dependencies": { "ms": "2.1.2" }, - "dependencies": { - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true } } }, - "deep-eql": { + "node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "license": "MIT" + }, + "node_modules/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": { + "license": "MIT", + "dependencies": { "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" } }, - "diff-sequences": { + "node_modules/diff-sequences": { "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } }, - "esbuild": { + "node_modules/esbuild": { "version": "0.20.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", - "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", "dev": true, - "requires": { + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { "@esbuild/aix-ppc64": "0.20.2", "@esbuild/android-arm": "0.20.2", "@esbuild/android-arm64": "0.20.2", @@ -2763,21 +482,19 @@ "@esbuild/win32-x64": "0.20.2" } }, - "estree-walker": { + "node_modules/estree-walker": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, - "requires": { + "license": "MIT", + "dependencies": { "@types/estree": "^1.0.0" } }, - "execa": { + "node_modules/execa": { "version": "8.0.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", - "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", "dev": true, - "requires": { + "license": "MIT", + "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", "human-signals": "^5.0.0", @@ -2788,486 +505,608 @@ "signal-exit": "^4.1.0", "strip-final-newline": "^3.0.0" }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/cross-spawn": { + "version": "7.0.3", + "dev": true, + "license": "MIT", "dependencies": { - "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" - } - }, - "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 - }, - "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 - }, - "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" - } - } + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/execa/node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "4.1.0", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/execa/node_modules/which": { + "version": "2.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" } }, - "fill-keys": { + "node_modules/fill-keys": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/fill-keys/-/fill-keys-1.0.2.tgz", - "integrity": "sha1-mo+jb06K1jTjv2tPPIiCVRRS6yA=", "dev": true, - "requires": { + "license": "MIT", + "dependencies": { "is-object": "~1.0.1", "merge-descriptors": "~1.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "fs.realpath": { + "node_modules/fs.realpath": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true + "dev": true, + "license": "ISC" }, - "fsevents": { + "node_modules/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 + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } }, - "get-func-name": { + "node_modules/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 + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } }, - "get-stream": { + "node_modules/get-stream": { "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", - "dev": true + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "glob": { + "node_modules/glob": { "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "dev": true, - "requires": { + "license": "ISC", + "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "html-escaper": { + "node_modules/html-escaper": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true + "dev": true, + "license": "MIT" }, - "https-proxy-agent": { + "node_modules/https-proxy-agent": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", - "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", - "requires": { + "license": "MIT", + "dependencies": { "agent-base": "6", "debug": "4" + }, + "engines": { + "node": ">= 6" } }, - "human-signals": { + "node_modules/human-signals": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", - "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", - "dev": true + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } }, - "inflight": { + "node_modules/inflight": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "dev": true, - "requires": { + "license": "ISC", + "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, - "inherits": { + "node_modules/inherits": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true + "dev": true, + "license": "ISC" }, - "is-node-process": { + "node_modules/is-node-process": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", - "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", - "dev": true + "dev": true, + "license": "MIT" }, - "is-object": { + "node_modules/is-object": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz", - "integrity": "sha1-iVJojF7C/9awPsyF52ngKQMINHA=", - "dev": true + "dev": true, + "license": "MIT" }, - "is-stream": { + "node_modules/is-stream": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "isexe": { + "node_modules/isexe": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true + "dev": true, + "license": "ISC" }, - "istanbul-lib-report": { + "node_modules/istanbul-lib-report": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, - "requires": { + "license": "BSD-3-Clause", + "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", "supports-color": "^7.1.0" }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", "dependencies": { - "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 - }, - "istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "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" - } - } + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "istanbul-lib-source-maps": { + "node_modules/istanbul-lib-source-maps": { "version": "5.0.4", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.4.tgz", - "integrity": "sha512-wHOoEsNJTVltaJp8eVkm8w+GVkVNHT2YDYo53YdzQEL2gWm1hBX5cGFR9hQJtuGLebidVX7et3+dmDZrmclduw==", "dev": true, - "requires": { + "license": "BSD-3-Clause", + "dependencies": { "@jridgewell/trace-mapping": "^0.3.23", "debug": "^4.1.1", "istanbul-lib-coverage": "^3.0.0" }, - "dependencies": { - "istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true - } + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" } }, - "istanbul-reports": { + "node_modules/istanbul-reports": { "version": "3.1.7", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", - "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", "dev": true, - "requires": { + "license": "BSD-3-Clause", + "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" } }, - "json-stringify-safe": { + "node_modules/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 + "dev": true, + "license": "ISC" }, - "local-pkg": { + "node_modules/local-pkg": { "version": "0.5.0", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", - "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==", "dev": true, - "requires": { + "license": "MIT", + "dependencies": { "mlly": "^1.4.2", "pkg-types": "^1.0.3" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" } }, - "loupe": { + "node_modules/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": { + "license": "MIT", + "dependencies": { "get-func-name": "^2.0.1" } }, - "magic-string": { + "node_modules/magic-string": { "version": "0.30.10", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz", - "integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==", "dev": true, - "requires": { + "license": "MIT", + "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, - "magicast": { + "node_modules/magicast": { "version": "0.3.4", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.4.tgz", - "integrity": "sha512-TyDF/Pn36bBji9rWKHlZe+PZb6Mx5V8IHCSxk7X4aljM4e/vyDvZZYwHewdVaqiA0nb3ghfHU/6AUpDxWoER2Q==", "dev": true, - "requires": { + "license": "MIT", + "dependencies": { "@babel/parser": "^7.24.4", "@babel/types": "^7.24.0", "source-map-js": "^1.2.0" } }, - "make-dir": { + "node_modules/make-dir": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, - "requires": { + "license": "MIT", + "dependencies": { "semver": "^7.5.3" }, - "dependencies": { - "semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", - "dev": true - } + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.6.2", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, - "merge-descriptors": { + "node_modules/merge-descriptors": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", - "dev": true + "dev": true, + "license": "MIT" }, - "merge-stream": { + "node_modules/merge-stream": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true + "dev": true, + "license": "MIT" }, - "mimic-fn": { + "node_modules/mimic-fn": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "dev": true + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "minimatch": { + "node_modules/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": { + "license": "ISC", + "dependencies": { "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" } }, - "mlly": { + "node_modules/mlly": { "version": "1.7.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.0.tgz", - "integrity": "sha512-U9SDaXGEREBYQgfejV97coK0UL1r+qnF2SyO9A3qcI8MzKnsIFKHNVEkrDyNncQTKQQumsasmeq84eNMdBfsNQ==", "dev": true, - "requires": { + "license": "MIT", + "dependencies": { "acorn": "^8.11.3", "pathe": "^1.1.2", "pkg-types": "^1.1.0", "ufo": "^1.5.3" } }, - "module-not-found-error": { + "node_modules/module-not-found-error": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/module-not-found-error/-/module-not-found-error-1.0.1.tgz", - "integrity": "sha1-z4tP9PKWQGdNbN0CsOO8UjwrvcA=", - "dev": true + "dev": true, + "license": "MIT" }, - "nanoid": { + "node_modules/nanoid": { "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "dev": true + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } }, - "nock": { + "node_modules/nock": { "version": "14.0.10", - "resolved": "https://registry.npmjs.org/nock/-/nock-14.0.10.tgz", - "integrity": "sha512-Q7HjkpyPeLa0ZVZC5qpxBt5EyLczFJ91MEewQiIi9taWuA0KB/MDJlUWtON+7dGouVdADTQsf9RA7TZk6D8VMw==", "dev": true, - "requires": { + "license": "MIT", + "dependencies": { "@mswjs/interceptors": "^0.39.5", "json-stringify-safe": "^5.0.1", "propagate": "^2.0.0" + }, + "engines": { + "node": ">=18.20.0 <20 || >=20.12.1" } }, - "npm-run-path": { + "node_modules/npm-run-path": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", - "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", "dev": true, - "requires": { + "license": "MIT", + "dependencies": { "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "once": { + "node_modules/once": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, - "requires": { + "license": "ISC", + "dependencies": { "wrappy": "1" } }, - "onetime": { + "node_modules/onetime": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", "dev": true, - "requires": { + "license": "MIT", + "dependencies": { "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "outvariant": { + "node_modules/outvariant": { "version": "1.4.3", - "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", - "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", - "dev": true + "dev": true, + "license": "MIT" }, - "p-limit": { + "node_modules/p-limit": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", - "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", "dev": true, - "requires": { + "license": "MIT", + "dependencies": { "yocto-queue": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "path-is-absolute": { + "node_modules/path-is-absolute": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } }, - "path-key": { + "node_modules/path-key": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "pathe": { + "node_modules/pathe": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true + "dev": true, + "license": "MIT" }, - "pathval": { + "node_modules/pathval": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", - "dev": true + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } }, - "picocolors": { + "node_modules/picocolors": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", - "dev": true + "dev": true, + "license": "ISC" }, - "pkg-types": { + "node_modules/pkg-types": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.1.1.tgz", - "integrity": "sha512-ko14TjmDuQJ14zsotODv7dBlwxKhUKQEhuhmbqo1uCi9BB0Z2alo/wAXg6q1dTR5TyuqYyWhjtfe/Tsh+X28jQ==", "dev": true, - "requires": { + "license": "MIT", + "dependencies": { "confbox": "^0.1.7", "mlly": "^1.7.0", "pathe": "^1.1.2" } }, - "postcss": { + "node_modules/postcss": { "version": "8.4.38", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", - "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", "dev": true, - "requires": { + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.0.0", "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" } }, - "pretty-format": { + "node_modules/pretty-format": { "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, - "requires": { + "license": "MIT", + "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - } + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "propagate": { + "node_modules/propagate": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", - "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", - "dev": true + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } }, - "proxyquire": { + "node_modules/proxyquire": { "version": "1.8.0", - "resolved": "https://registry.npmjs.org/proxyquire/-/proxyquire-1.8.0.tgz", - "integrity": "sha1-AtUUpb7ZhvBMuyCTrxZ0FTX3ntw=", "dev": true, - "requires": { + "license": "MIT", + "dependencies": { "fill-keys": "^1.0.2", "module-not-found-error": "^1.0.0", "resolve": "~1.1.7" } }, - "react-is": { + "node_modules/react-is": { "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true + "dev": true, + "license": "MIT" }, - "resolve": { + "node_modules/resolve": { "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", - "dev": true + "dev": true, + "license": "MIT" }, - "rollup": { + "node_modules/rollup": { "version": "4.18.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz", - "integrity": "sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==", "dev": true, - "requires": { + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.18.0", "@rollup/rollup-android-arm64": "4.18.0", "@rollup/rollup-darwin-arm64": "4.18.0", @@ -3284,156 +1123,218 @@ "@rollup/rollup-win32-arm64-msvc": "4.18.0", "@rollup/rollup-win32-ia32-msvc": "4.18.0", "@rollup/rollup-win32-x64-msvc": "4.18.0", - "@types/estree": "1.0.5", "fsevents": "~2.3.2" } }, - "shebang-command": { + "node_modules/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": { + "license": "MIT", + "dependencies": { "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" } }, - "shebang-regex": { + "node_modules/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 + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } }, - "siginfo": { + "node_modules/siginfo": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true + "dev": true, + "license": "ISC" }, - "source-map-js": { + "node_modules/source-map-js": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", - "dev": true + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } }, - "stackback": { + "node_modules/stackback": { "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true + "dev": true, + "license": "MIT" }, - "std-env": { + "node_modules/std-env": { "version": "3.7.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz", - "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", - "dev": true + "dev": true, + "license": "MIT" }, - "strict-event-emitter": { + "node_modules/strict-event-emitter": { "version": "0.5.1", - "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", - "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", - "dev": true + "dev": true, + "license": "MIT" }, - "strip-final-newline": { + "node_modules/strip-final-newline": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dev": true + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "strip-literal": { + "node_modules/strip-literal": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.0.tgz", - "integrity": "sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==", "dev": true, - "requires": { + "license": "MIT", + "dependencies": { "js-tokens": "^9.0.0" }, - "dependencies": { - "js-tokens": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.0.tgz", - "integrity": "sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ==", - "dev": true - } + "funding": { + "url": "https://github.com/sponsors/antfu" } }, - "test-exclude": { + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/test-exclude": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", "dev": true, - "requires": { + "license": "ISC", + "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" } }, - "tinybench": { + "node_modules/tinybench": { "version": "2.8.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.8.0.tgz", - "integrity": "sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==", - "dev": true + "dev": true, + "license": "MIT" }, - "tinypool": { + "node_modules/tinypool": { "version": "0.8.4", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", - "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", - "dev": true + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } }, - "tinyspy": { + "node_modules/tinyspy": { "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", - "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", - "dev": true + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } }, - "to-fast-properties": { + "node_modules/to-fast-properties": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } }, - "type-detect": { + "node_modules/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 + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } }, - "ufo": { + "node_modules/ufo": { "version": "1.5.3", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.3.tgz", - "integrity": "sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==", - "dev": true + "dev": true, + "license": "MIT" }, - "vite": { + "node_modules/vite": { "version": "5.2.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz", - "integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==", "dev": true, - "requires": { + "license": "MIT", + "dependencies": { "esbuild": "^0.20.1", - "fsevents": "~2.3.3", "postcss": "^8.4.38", "rollup": "^4.13.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } } }, - "vite-node": { + "node_modules/vite-node": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.0.tgz", - "integrity": "sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==", "dev": true, - "requires": { + "license": "MIT", + "dependencies": { "cac": "^6.7.14", "debug": "^4.3.4", "pathe": "^1.1.1", "picocolors": "^1.0.0", "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "vitest": { + "node_modules/vitest": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.0.tgz", - "integrity": "sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==", "dev": true, - "requires": { + "license": "MIT", + "dependencies": { "@vitest/expect": "1.6.0", "@vitest/runner": "1.6.0", "@vitest/snapshot": "1.6.0", @@ -3454,29 +1355,75 @@ "vite": "^5.0.0", "vite-node": "1.6.0", "why-is-node-running": "^2.2.2" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "1.6.0", + "@vitest/ui": "1.6.0", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } } }, - "why-is-node-running": { + "node_modules/why-is-node-running": { "version": "2.2.2", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", - "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==", "dev": true, - "requires": { + "license": "MIT", + "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" } }, - "wrappy": { + "node_modules/wrappy": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "dev": true, + "license": "ISC" }, - "yocto-queue": { + "node_modules/yocto-queue": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", - "dev": true + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } From ca44f611f837ae33657853cf7c6a07bb32898e8a Mon Sep 17 00:00:00 2001 From: tgd Date: Wed, 12 Nov 2025 10:52:48 -0800 Subject: [PATCH 14/15] install deps after merge --- package-lock.json | 174 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) diff --git a/package-lock.json b/package-lock.json index 1794322..203d021 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ }, "devDependencies": { "@vitest/coverage-v8": "^4.0.8", + "nock": "^14.0.10", "proxyquire": "^2.1.3", "vitest": "^4.0.8" }, @@ -550,6 +551,49 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mswjs/interceptors": { + "version": "0.39.8", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.39.8.tgz", + "integrity": "sha512-2+BzZbjRO7Ct61k8fMNHEtoKjeWI9pIlHFTqBwZ5icHpqszIgEZbjb1MW5Z0+bITTCTl3gk4PDBxs9tA/csXvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" + } + }, + "node_modules/@open-draft/until": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "dev": true, + "license": "MIT" + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.53.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.2.tgz", @@ -1275,6 +1319,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "dev": true, + "license": "MIT" + }, "node_modules/is-object": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz", @@ -1342,6 +1393,13 @@ "dev": true, "license": "MIT" }, + "node_modules/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, + "license": "ISC" + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -1417,6 +1475,28 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/nock": { + "version": "14.0.10", + "resolved": "https://registry.npmjs.org/nock/-/nock-14.0.10.tgz", + "integrity": "sha512-Q7HjkpyPeLa0ZVZC5qpxBt5EyLczFJ91MEewQiIi9taWuA0KB/MDJlUWtON+7dGouVdADTQsf9RA7TZk6D8VMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mswjs/interceptors": "^0.39.5", + "json-stringify-safe": "^5.0.1", + "propagate": "^2.0.0" + }, + "engines": { + "node": ">=18.20.0 <20 || >=20.12.1" + } + }, + "node_modules/outvariant": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", + "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", + "dev": true, + "license": "MIT" + }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -1480,6 +1560,16 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/proxyquire": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/proxyquire/-/proxyquire-2.1.3.tgz", @@ -1599,6 +1689,13 @@ "dev": true, "license": "MIT" }, + "node_modules/strict-event-emitter": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "dev": true, + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -2079,6 +2176,42 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "@mswjs/interceptors": { + "version": "0.39.8", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.39.8.tgz", + "integrity": "sha512-2+BzZbjRO7Ct61k8fMNHEtoKjeWI9pIlHFTqBwZ5icHpqszIgEZbjb1MW5Z0+bITTCTl3gk4PDBxs9tA/csXvA==", + "dev": true, + "requires": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "strict-event-emitter": "^0.5.1" + } + }, + "@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "dev": true + }, + "@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "dev": true, + "requires": { + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" + } + }, + "@open-draft/until": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "dev": true + }, "@rollup/rollup-android-arm-eabi": { "version": "4.53.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.2.tgz", @@ -2511,6 +2644,12 @@ "hasown": "^2.0.2" } }, + "is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "dev": true + }, "is-object": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz", @@ -2561,6 +2700,12 @@ "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", "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 + }, "magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -2613,6 +2758,23 @@ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "dev": true }, + "nock": { + "version": "14.0.10", + "resolved": "https://registry.npmjs.org/nock/-/nock-14.0.10.tgz", + "integrity": "sha512-Q7HjkpyPeLa0ZVZC5qpxBt5EyLczFJ91MEewQiIi9taWuA0KB/MDJlUWtON+7dGouVdADTQsf9RA7TZk6D8VMw==", + "dev": true, + "requires": { + "@mswjs/interceptors": "^0.39.5", + "json-stringify-safe": "^5.0.1", + "propagate": "^2.0.0" + } + }, + "outvariant": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", + "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", + "dev": true + }, "path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -2648,6 +2810,12 @@ "source-map-js": "^1.2.1" } }, + "propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "dev": true + }, "proxyquire": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/proxyquire/-/proxyquire-2.1.3.tgz", @@ -2732,6 +2900,12 @@ "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", "dev": true }, + "strict-event-emitter": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "dev": true + }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", From 27942b9dfde0f160f077415e17cc36d688037f2f Mon Sep 17 00:00:00 2001 From: Kwame Efah Date: Wed, 12 Nov 2025 11:08:32 -0800 Subject: [PATCH 15/15] updating deceiving comment --- lib/flags/local_flags.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/flags/local_flags.js b/lib/flags/local_flags.js index 39ae1d5..1b35788 100644 --- a/lib/flags/local_flags.js +++ b/lib/flags/local_flags.js @@ -264,7 +264,6 @@ class LocalFeatureFlagsProvider extends FeatureFlagsProvider { const salt = flagKey + storedSalt + "variant"; const variantHash = normalizedHash(String(contextValue), salt); - // Deep copy variants and apply splits const variants = flag.ruleset.variants.map(v => ({ ...v })); if (rollout.variant_splits) { for (const variant of variants) {