diff --git a/.gitmodules b/.gitmodules index bba15b6..f9b9019 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "tests/engine/engine-tests/engine-test-data"] path = tests/engine/engine-tests/engine-test-data url = git@github.com:Flagsmith/engine-test-data.git - branch = v1.0.0 + branch = v2.5.0 diff --git a/.husky/pre-commit b/.husky/pre-commit index 938cbdb..c221482 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,6 +1,7 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" +npm run generate-engine-types npm run lint git add ./flagsmith-engine ./sdk ./tests ./index.ts ./.github npm run test \ No newline at end of file diff --git a/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts b/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts new file mode 100644 index 0000000..d1ec209 --- /dev/null +++ b/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts @@ -0,0 +1,252 @@ +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * An environment's unique identifier. + */ +export type Key = string; +/** + * An environment's human-readable name. + */ +export type Name = string; +/** + * A unique identifier for an identity, used for segment and multivariate feature flag targeting, and displayed in the Flagsmith UI. + */ +export type Identifier = string; +/** + * Key used when selecting a value for a multivariate feature, or for % split segmentation. Set to an internal identifier or a composite value based on the environment key and identifier, depending on Flagsmith implementation. + */ +export type Key1 = string; +/** + * Key used for % split segmentation. + */ +export type Key2 = string; +/** + * The name of the segment. + */ +export type Name1 = string; +/** + * Segment rule type. Represents a logical quantifier for the conditions and sub-rules. + */ +export type Type = 'ALL' | 'ANY' | 'NONE'; +export type SegmentCondition = SegmentCondition1 | InSegmentCondition; +/** + * A reference to the identity trait or value in the evaluation context. + */ +export type Property = string; +/** + * The operator to use for evaluating the condition. + */ +export type Operator = + | 'EQUAL' + | 'GREATER_THAN' + | 'LESS_THAN' + | 'LESS_THAN_INCLUSIVE' + | 'CONTAINS' + | 'GREATER_THAN_INCLUSIVE' + | 'NOT_CONTAINS' + | 'NOT_EQUAL' + | 'REGEX' + | 'PERCENTAGE_SPLIT' + | 'MODULO' + | 'IS_SET' + | 'IS_NOT_SET' + | 'IN'; +/** + * The value to compare against the trait or context value. + */ +export type Value = string; +/** + * A reference to the identity trait or value in the evaluation context. + */ +export type Property1 = string; +/** + * The operator to use for evaluating the condition. + */ +export type Operator1 = 'IN'; +/** + * The values to compare against the trait or context value. + */ +export type Value1 = string[]; +/** + * Conditions that must be met for the rule to apply. + */ +export type Conditions = SegmentCondition[]; +/** + * Sub-rules nested within the segment rule. + */ +export type SubRules = SegmentRule[]; +/** + * Rules that define the segment. + */ +export type Rules = SegmentRule[]; +/** + * Key used when selecting a value for a multivariate feature. Set to an internal identifier or a UUID, depending on Flagsmith implementation. + */ +export type Key3 = string; +/** + * Unique feature identifier. + */ +export type FeatureKey = string; +/** + * Feature name. + */ +export type Name2 = string; +/** + * Indicates whether the feature is enabled in the environment. + */ +export type Enabled = boolean; +/** + * A default environment value for the feature. If the feature is multivariate, this will be the control value. + */ +export type Value2 = string | number | boolean | null; +/** + * The value of the feature. + */ +export type Value3 = string | number | boolean | null; +/** + * The weight of the feature value variant, as a percentage number (i.e. 100.0). + */ +export type Weight = number; +/** + * Priority of the feature flag variant. Lower values indicate a higher priority when multiple variants apply to the same context key. + */ +export type VariantPriority = number; +/** + * An array of environment default values associated with the feature. Empty for standard features, or contains multiple values for multivariate features. + */ +export type Variants = FeatureValue[]; +/** + * Priority of the feature context. Lower values indicate a higher priority when multiple contexts apply to the same feature. + */ +export type FeaturePriority = number; +/** + * Feature overrides for the segment. + */ +export type Overrides = FeatureContext[]; + +/** + * A context object containing the necessary information to evaluate Flagsmith feature flags. + */ +export interface EvaluationContext { + environment: EnvironmentContext; + /** + * Identity context used for identity-based evaluation. + */ + identity?: IdentityContext | null; + segments?: Segments; + features?: Features; + [k: string]: unknown; +} +/** + * Environment context required for evaluation. + */ +export interface EnvironmentContext { + key: Key; + name: Name; + [k: string]: unknown; +} +/** + * Represents an identity context for feature flag evaluation. + */ +export interface IdentityContext { + identifier: Identifier; + key: Key1; + traits?: Traits; + [k: string]: unknown; +} +/** + * A map of traits associated with the identity, where the key is the trait name and the value is the trait value. + */ +export interface Traits { + [k: string]: string | number | boolean | null; +} +/** + * Segments applicable to the evaluation context. + */ +export interface Segments { + [k: string]: SegmentContext; +} +/** + * Represents a segment context for feature flag evaluation. + */ +export interface SegmentContext { + key: Key2; + name: Name1; + rules: Rules; + overrides?: Overrides; + metadata?: SegmentMetadata; + [k: string]: unknown; +} +/** + * Represents a rule within a segment for feature flag evaluation. + */ +export interface SegmentRule { + type: Type; + conditions?: Conditions; + rules?: SubRules; + [k: string]: unknown; +} +/** + * Represents a condition within a segment rule for feature flag evaluation. + */ +export interface SegmentCondition1 { + property: Property; + operator: Operator; + value: Value; + [k: string]: unknown; +} +/** + * Represents an IN condition within a segment rule for feature flag evaluation. + */ +export interface InSegmentCondition { + property: Property1; + operator: Operator1; + value: Value1; + [k: string]: unknown; +} +/** + * Represents a feature context for feature flag evaluation. + */ +export interface FeatureContext { + key: Key3; + feature_key: FeatureKey; + name: Name2; + enabled: Enabled; + value: Value2; + variants?: Variants; + priority?: FeaturePriority; + metadata?: FeatureMetadata; + [k: string]: unknown; +} +/** + * Represents a multivariate value for a feature flag. + */ +export interface FeatureValue { + value: Value3; + weight: Weight; + priority: VariantPriority; + [k: string]: unknown; +} +/** + * Additional metadata associated with the feature. + */ +export interface FeatureMetadata { + [k: string]: unknown; +} +/** + * Additional metadata associated with the segment. + */ +export interface SegmentMetadata { + [k: string]: unknown; +} +/** + * Features to be evaluated in the context. + */ +export interface Features { + [k: string]: FeatureContext; +} diff --git a/flagsmith-engine/evaluation/evaluationContext/mappers.ts b/flagsmith-engine/evaluation/evaluationContext/mappers.ts new file mode 100644 index 0000000..dede412 --- /dev/null +++ b/flagsmith-engine/evaluation/evaluationContext/mappers.ts @@ -0,0 +1,195 @@ +import { + FeaturesWithMetadata, + Segments, + Traits, + GenericEvaluationContext, + EnvironmentContext, + IdentityContext, + SegmentSource +} from '../models.js'; +import { EnvironmentModel } from '../../environments/models.js'; +import { IdentityModel } from '../../identities/models.js'; +import { TraitModel } from '../../identities/traits/models.js'; +import { IDENTITY_OVERRIDE_SEGMENT_NAME } from '../../segments/constants.js'; +import { createHash } from 'node:crypto'; +import { uuidToBigInt } from '../../features/util.js'; + +export function getEvaluationContext( + environment: EnvironmentModel, + identity?: IdentityModel, + overrideTraits?: TraitModel[] +): GenericEvaluationContext { + const environmentContext = mapEnvironmentModelToEvaluationContext(environment); + const identityContext = identity + ? mapIdentityModelToIdentityContext(identity, overrideTraits) + : undefined; + + const context = { + ...environmentContext, + ...(identityContext && { identity: identityContext }) + }; + + return context; +} + +function mapEnvironmentModelToEvaluationContext( + environment: EnvironmentModel +): GenericEvaluationContext { + const environmentContext: EnvironmentContext = { + key: environment.apiKey, + name: environment.project.name + }; + + const features: FeaturesWithMetadata = {}; + for (const fs of environment.featureStates) { + const variants = + fs.multivariateFeatureStateValues?.length > 0 + ? fs.multivariateFeatureStateValues.map(mv => ({ + value: mv.multivariateFeatureOption.value, + weight: mv.percentageAllocation, + priority: mv.id ?? uuidToBigInt(mv.mvFsValueUuid) + })) + : undefined; + + features[fs.feature.name] = { + key: fs.djangoID?.toString() || fs.featurestateUUID, + feature_key: fs.feature.id.toString(), + name: fs.feature.name, + enabled: fs.enabled, + value: fs.getValue(), + variants, + priority: fs.featureSegment?.priority, + metadata: { + flagsmithId: fs.feature.id + } + }; + } + + const segmentOverrides: Segments = {}; + for (const segment of environment.project.segments) { + segmentOverrides[segment.id.toString()] = { + key: segment.id.toString(), + name: segment.name, + rules: segment.rules.map(rule => mapSegmentRuleModelToRule(rule)), + overrides: + segment.featureStates.length > 0 + ? segment.featureStates.map(fs => ({ + key: fs.djangoID?.toString() || fs.featurestateUUID, + feature_key: fs.feature.id.toString(), + name: fs.feature.name, + enabled: fs.enabled, + value: fs.getValue(), + priority: fs.featureSegment?.priority + })) + : [], + metadata: { + source: SegmentSource.API, + flagsmith_id: segment.id + } + }; + } + + let identityOverrideSegments: Segments = {}; + if (environment.identityOverrides && environment.identityOverrides.length > 0) { + identityOverrideSegments = mapIdentityOverridesToSegments(environment.identityOverrides); + } + + return { + environment: environmentContext, + features, + segments: { + ...segmentOverrides, + ...identityOverrideSegments + } + }; +} + +function mapIdentityModelToIdentityContext( + identity: IdentityModel, + overrideTraits?: TraitModel[] +): IdentityContext { + const traits = overrideTraits || identity.identityTraits; + const traitsContext: Traits = {}; + + for (const trait of traits) { + traitsContext[trait.traitKey] = trait.traitValue; + } + + return { + identifier: identity.identifier, + key: identity.djangoID?.toString() || identity.compositeKey, + traits: traitsContext + }; +} + +function mapSegmentRuleModelToRule(rule: any): any { + return { + type: rule.type, + conditions: rule.conditions.map((condition: any) => ({ + property: condition.property, + operator: condition.operator, + value: condition.value + })), + rules: rule.rules.map((subRule: any) => mapSegmentRuleModelToRule(subRule)) + }; +} + +function mapIdentityOverridesToSegments(identityOverrides: IdentityModel[]): Segments { + const segments: Segments = {}; + const featuresToIdentifiers = new Map(); + + for (const identity of identityOverrides) { + if (!identity.identityFeatures || identity.identityFeatures.length === 0) { + continue; + } + + const sortedFeatures = [...identity.identityFeatures].sort((a, b) => + a.feature.name.localeCompare(b.feature.name) + ); + const overridesKey = sortedFeatures.map(fs => ({ + feature_key: fs.feature.id.toString(), + name: fs.feature.name, + enabled: fs.enabled, + value: fs.getValue(), + priority: -Infinity, + metadata: { + flagsmithId: fs.feature.id + } + })); + + const overridesHash = createHash('sha1').update(JSON.stringify(overridesKey)).digest('hex'); + + if (!featuresToIdentifiers.has(overridesHash)) { + featuresToIdentifiers.set(overridesHash, { identifiers: [], overrides: overridesKey }); + } + + featuresToIdentifiers.get(overridesHash)!.identifiers.push(identity.identifier); + } + + for (const [overrideHash, { identifiers, overrides }] of featuresToIdentifiers.entries()) { + const segmentKey = `identity_override_${overrideHash}`; + + segments[segmentKey] = { + key: segmentKey, + name: IDENTITY_OVERRIDE_SEGMENT_NAME, + rules: [ + { + type: 'ALL', + conditions: [ + { + property: '$.identity.identifier', + operator: 'IN', + value: identifiers.join(',') + } + ] + } + ], + metadata: { + source: SegmentSource.IDENTITY_OVERRIDE + }, + overrides: overrides + }; + } + + return segments; +} diff --git a/flagsmith-engine/evaluation/evaluationContext/types.ts b/flagsmith-engine/evaluation/evaluationContext/types.ts new file mode 100644 index 0000000..e671005 --- /dev/null +++ b/flagsmith-engine/evaluation/evaluationContext/types.ts @@ -0,0 +1,233 @@ +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * An environment's unique identifier. + */ +export type Key = string; +/** + * An environment's human-readable name. + */ +export type Name = string; +/** + * A unique identifier for an identity, used for segment and multivariate feature flag targeting, and displayed in the Flagsmith UI. + */ +export type Identifier = string; +/** + * Key used when selecting a value for a multivariate feature, or for % split segmentation. Set to an internal identifier or a composite value based on the environment key and identifier, depending on Flagsmith implementation. + */ +export type Key1 = string; +/** + * Key used for % split segmentation. + */ +export type Key2 = string; +/** + * The name of the segment. + */ +export type Name1 = string; +/** + * Segment rule type. Represents a logical quantifier for the conditions and sub-rules. + */ +export type Type = 'ALL' | 'ANY' | 'NONE'; +export type SegmentCondition = SegmentCondition1 | InSegmentCondition; +/** + * A reference to the identity trait or value in the evaluation context. + */ +export type Property = string; +/** + * The operator to use for evaluating the condition. + */ +export type Operator = + | 'EQUAL' + | 'GREATER_THAN' + | 'LESS_THAN' + | 'LESS_THAN_INCLUSIVE' + | 'CONTAINS' + | 'GREATER_THAN_INCLUSIVE' + | 'NOT_CONTAINS' + | 'NOT_EQUAL' + | 'REGEX' + | 'PERCENTAGE_SPLIT' + | 'MODULO' + | 'IS_SET' + | 'IS_NOT_SET' + | 'IN'; +/** + * The value to compare against the trait or context value. + */ +export type Value = string; +/** + * A reference to the identity trait or value in the evaluation context. + */ +export type Property1 = string; +/** + * The operator to use for evaluating the condition. + */ +export type Operator1 = 'IN'; +/** + * The values to compare against the trait or context value. + */ +export type Value1 = string[]; +/** + * Conditions that must be met for the rule to apply. + */ +export type Conditions = SegmentCondition[]; +/** + * Sub-rules nested within the segment rule. + */ +export type SubRules = SegmentRule[]; +/** + * Rules that define the segment. + */ +export type Rules = SegmentRule[]; +/** + * Key used when selecting a value for a multivariate feature. Set to an internal identifier or a UUID, depending on Flagsmith implementation. + */ +export type Key3 = string; +/** + * Unique feature identifier. + */ +export type FeatureKey = string; +/** + * Feature name. + */ +export type Name2 = string; +/** + * Indicates whether the feature is enabled in the environment. + */ +export type Enabled = boolean; +/** + * A default environment value for the feature. If the feature is multivariate, this will be the control value. + */ +export type Value2 = string; +/** + * The value of the feature. + */ +export type Value3 = string; +/** + * The weight of the feature value variant, as a percentage number (i.e. 100.0). + */ +export type Weight = number; +/** + * An array of environment default values associated with the feature. Contains a single value for standard features, or multiple values for multivariate features. + */ +export type Variants = FeatureValue[]; +/** + * Priority of the feature context. Lower values indicate a higher priority when multiple contexts apply to the same feature. + */ +export type Priority = number; +/** + * Feature overrides for the segment. + */ +export type Overrides = FeatureContext[]; + +/** + * A context object containing the necessary information to evaluate Flagsmith feature flags. + */ +export interface EvaluationContext { + environment: EnvironmentContext; + /** + * Identity context used for identity-based evaluation. + */ + identity?: IdentityContext | null; + segments?: Segments; + features?: Features; + [k: string]: unknown; +} +/** + * Environment context required for evaluation. + */ +export interface EnvironmentContext { + key: Key; + name: Name; + [k: string]: unknown; +} +/** + * Represents an identity context for feature flag evaluation. + */ +export interface IdentityContext { + identifier: Identifier; + key: Key1; + traits?: Traits; + [k: string]: unknown; +} +/** + * A map of traits associated with the identity, where the key is the trait name and the value is the trait value. + */ +export interface Traits { + [k: string]: string | number | boolean | null; +} +/** + * Segments applicable to the evaluation context. + */ +export interface Segments { + [k: string]: SegmentContext; +} +/** + * Represents a segment context for feature flag evaluation. + */ +export interface SegmentContext { + key: Key2; + name: Name1; + rules: Rules; + overrides?: Overrides; + [k: string]: unknown; +} +/** + * Represents a rule within a segment for feature flag evaluation. + */ +export interface SegmentRule { + type: Type; + conditions?: Conditions; + rules?: SubRules; + [k: string]: unknown; +} +/** + * Represents a condition within a segment rule for feature flag evaluation. + */ +export interface SegmentCondition1 { + property: Property; + operator: Operator; + value: Value; + [k: string]: unknown; +} +/** + * Represents an IN condition within a segment rule for feature flag evaluation. + */ +export interface InSegmentCondition { + property: Property1; + operator: Operator1; + value: Value1; + [k: string]: unknown; +} +/** + * Represents a feature context for feature flag evaluation. + */ +export interface FeatureContext { + key: Key3; + feature_key: FeatureKey; + name: Name2; + enabled: Enabled; + value: Value2; + variants?: Variants; + priority?: Priority; + [k: string]: unknown; +} +/** + * Represents a multivariate value for a feature flag. + */ +export interface FeatureValue { + value: Value3; + weight: Weight; + [k: string]: unknown; +} +/** + * Features to be evaluated in the context. + */ +export interface Features { + [k: string]: FeatureContext; +} diff --git a/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.ts b/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.ts new file mode 100644 index 0000000..390146a --- /dev/null +++ b/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.ts @@ -0,0 +1,81 @@ +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * Unique feature identifier. + */ +export type FeatureKey = string; +/** + * Feature name. + */ +export type Name = string; +/** + * Indicates if the feature flag is enabled. + */ +export type Enabled = boolean; +/** + * Feature flag value. + */ +export type Value = string | number | boolean | null; +/** + * Reason for the feature flag evaluation. + */ +export type Reason = string; +/** + * Unique segment identifier. + */ +export type Key = string; +/** + * Segment name. + */ +export type Name1 = string; +/** + * List of segments which the provided context belongs to. + */ +export type Segments = SegmentResult[]; + +/** + * Evaluation result object containing the used context, flag evaluation results, and segments used in the evaluation. + */ +export interface EvaluationResult { + flags: Flags; + segments: Segments; + [k: string]: unknown; +} +/** + * Feature flags evaluated for the context, mapped by feature names. + */ +export interface Flags { + [k: string]: FlagResult; +} +export interface FlagResult { + feature_key: FeatureKey; + name: Name; + enabled: Enabled; + value: Value; + reason: Reason; + metadata?: FeatureMetadata; + [k: string]: unknown; +} +/** + * Additional metadata associated with the feature. + */ +export interface FeatureMetadata { + [k: string]: unknown; +} +export interface SegmentResult { + key: Key; + name: Name1; + metadata?: SegmentMetadata; + [k: string]: unknown; +} +/** + * Additional metadata associated with the segment. + */ +export interface SegmentMetadata { + [k: string]: unknown; +} diff --git a/flagsmith-engine/evaluation/models.ts b/flagsmith-engine/evaluation/models.ts new file mode 100644 index 0000000..71ea56b --- /dev/null +++ b/flagsmith-engine/evaluation/models.ts @@ -0,0 +1,64 @@ +// This file is the entry point for the evaluation module types +// All types from evaluations should be at least imported here and re-exported +// Do not use types directly from generated files + +import type { + EvaluationResult as EvaluationContextResult, + FlagResult, + FeatureMetadata +} from './evaluationResult/evaluationResult.types.js'; + +import type { + FeatureContext, + EnvironmentContext, + IdentityContext, + Segments +} from './evaluationContext/evaluationContext.types.js'; + +export interface CustomFeatureMetadata extends FeatureMetadata { + flagsmithId?: number; +} + +export interface FeatureContextWithMetadata + extends FeatureContext { + metadata: T; + [k: string]: unknown; +} + +export type FeaturesWithMetadata = { + [k: string]: FeatureContextWithMetadata; +}; + +export interface GenericEvaluationContext { + environment: EnvironmentContext; + identity?: IdentityContext | null; + segments?: Segments; + features?: FeaturesWithMetadata; + [k: string]: unknown; +} + +export type FlagResultWithMetadata = FlagResult & { + metadata: T; +}; + +export type EvaluationResultFlags = Record< + string, + FlagResultWithMetadata +>; + +export type EvaluationResultSegments = EvaluationContextResult['segments']; + +export type EvaluationResult = { + flags: EvaluationResultFlags; + segments: EvaluationResultSegments; +}; + +export type EvaluationResultWithMetadata = EvaluationResult; +export type EvaluationContextWithMetadata = GenericEvaluationContext; + +export enum SegmentSource { + API = 'api', + IDENTITY_OVERRIDE = 'identity_override' +} + +export * from './evaluationContext/evaluationContext.types.js'; diff --git a/flagsmith-engine/features/models.ts b/flagsmith-engine/features/models.ts index 686fbed..1549e5d 100644 --- a/flagsmith-engine/features/models.ts +++ b/flagsmith-engine/features/models.ts @@ -1,5 +1,5 @@ import { randomUUID as uuidv4 } from 'node:crypto'; -import { getHashedPercentateForObjIds } from '../utils/hashing/index.js'; +import { getHashedPercentageForObjIds } from '../utils/hashing/index.js'; export class FeatureModel { id: number; @@ -103,6 +103,7 @@ export class FeatureStateModel { const sortedF = this.multivariateFeatureStateValues.sort((a, b) => { return a.id - b.id; }); + for (const myValue of sortedF) { switch (myValue.percentageAllocation) { case 0: @@ -111,7 +112,7 @@ export class FeatureStateModel { return myValue.multivariateFeatureOption.value; default: if (percentageValue === undefined) { - percentageValue = getHashedPercentateForObjIds([ + percentageValue = getHashedPercentageForObjIds([ this.djangoID || this.featurestateUUID, identityID ]); diff --git a/flagsmith-engine/features/types.ts b/flagsmith-engine/features/types.ts new file mode 100644 index 0000000..f792e2d --- /dev/null +++ b/flagsmith-engine/features/types.ts @@ -0,0 +1,5 @@ +export enum TARGETING_REASONS { + DEFAULT = 'DEFAULT', + TARGETING_MATCH = 'TARGETING_MATCH', + SPLIT = 'SPLIT' +} diff --git a/flagsmith-engine/features/util.ts b/flagsmith-engine/features/util.ts index 0a19589..ef7224d 100644 --- a/flagsmith-engine/features/util.ts +++ b/flagsmith-engine/features/util.ts @@ -46,3 +46,7 @@ export function buildFeatureStateModel(featuresStateModelJSON: any): FeatureStat export function buildFeatureSegment(featureSegmentJSON: any): FeatureSegment { return new FeatureSegment(featureSegmentJSON.priority); } + +export function uuidToBigInt(uuid: string): BigInt { + return BigInt('0x' + uuid.replace(/-/g, '')); +} diff --git a/flagsmith-engine/index.ts b/flagsmith-engine/index.ts index fb641ee..534b9e1 100644 --- a/flagsmith-engine/index.ts +++ b/flagsmith-engine/index.ts @@ -1,102 +1,238 @@ -import { EnvironmentModel } from './environments/models.js'; -import { FeatureStateModel } from './features/models.js'; -import { IdentityModel } from './identities/models.js'; -import { TraitModel } from './identities/traits/models.js'; +import { + EvaluationContextWithMetadata, + EvaluationResultSegments, + EvaluationResultWithMetadata, + FeatureContextWithMetadata, + CustomFeatureMetadata, + FlagResultWithMetadata +} from './evaluation/models.js'; import { getIdentitySegments } from './segments/evaluators.js'; -import { SegmentModel } from './segments/models.js'; -import { FeatureStateNotFound } from './utils/errors.js'; - +import { EvaluationResultFlags } from './evaluation/models.js'; +import { TARGETING_REASONS } from './features/types.js'; +import { getHashedPercentageForObjIds } from './utils/hashing/index.js'; export { EnvironmentModel } from './environments/models.js'; -export { FeatureModel, FeatureStateModel } from './features/models.js'; export { IdentityModel } from './identities/models.js'; export { TraitModel } from './identities/traits/models.js'; export { SegmentModel } from './segments/models.js'; +export { FeatureModel, FeatureStateModel } from './features/models.js'; export { OrganisationModel } from './organisations/models.js'; -function getIdentityFeatureStatesDict( - environment: EnvironmentModel, - identity: IdentityModel, - overrideTraits?: TraitModel[] -) { - // Get feature states from the environment - const featureStates: { [key: number]: FeatureStateModel } = {}; - for (const fs of environment.featureStates) { - featureStates[fs.feature.id] = fs; +type SegmentOverride = { + feature: FeatureContextWithMetadata; + segmentName: string; +}; + +export type SegmentOverrides = Record; + +/** + * Evaluates flags and segments for the given context. + * + * This is the main entry point for the evaluation engine. It processes segments, + * applies feature overrides based on segment priority, and returns the final flag states with + * evaluation reasons. + * + * @param context - EvaluationContext containing environment, identity, and segment data + * @returns EvaluationResult with flags, segments, and original context + */ +export function getEvaluationResult( + context: EvaluationContextWithMetadata +): EvaluationResultWithMetadata { + const { segments, segmentOverrides } = evaluateSegments(context); + const flags = evaluateFeatures(context, segmentOverrides); + + return { flags, segments }; +} + +/** + * Evaluates which segments the identity belongs to and collects feature overrides. + * + * @param context - EvaluationContext containing identity and segment definitions + * @returns Object containing segments the identity belongs to and any feature overrides + */ +export function evaluateSegments(context: EvaluationContextWithMetadata): { + segments: EvaluationResultSegments; + segmentOverrides: Record; +} { + if (!context.identity || !context.segments) { + return { + segments: [], + segmentOverrides: {} as Record + }; } + const identitySegments = getIdentitySegments(context); - // Override with any feature states defined by matching segments - const identitySegments: SegmentModel[] = getIdentitySegments( - environment, - identity, - overrideTraits - ); - for (const matchingSegment of identitySegments) { - for (const featureState of matchingSegment.featureStates) { - if (featureStates[featureState.feature.id]) { - if (featureStates[featureState.feature.id].isHigherSegmentPriority(featureState)) { - continue; - } + const segments = identitySegments.map(segment => ({ + key: segment.key, + name: segment.name, + ...(segment.metadata + ? { + metadata: { + ...segment.metadata + } + } + : {}) + })); + const segmentOverrides = processSegmentOverrides(identitySegments); + + return { segments, segmentOverrides }; +} + +/** + * Processes feature overrides from segments, applying priority rules. + * + * When multiple segments override the same feature, the segment with + * higher priority (lower numeric value) takes precedence. + * + * @param identitySegments - Segments that the identity belongs to + * @returns Map of feature keys to their highest-priority segment overrides + */ +export function processSegmentOverrides(identitySegments: any[]): Record { + const segmentOverrides: Record = {}; + + for (const segment of identitySegments) { + if (!segment.overrides) continue; + + const overridesList = Array.isArray(segment.overrides) ? segment.overrides : []; + + for (const override of overridesList) { + if (shouldApplyOverride(override, segmentOverrides)) { + segmentOverrides[override.feature_key] = { + feature: override, + segmentName: segment.name + }; } - featureStates[featureState.feature.id] = featureState; } } - // Override with any feature states defined directly the identity - for (const fs of identity.identityFeatures) { - if (featureStates[fs.feature.id]) { - featureStates[fs.feature.id] = fs; - } - } - return featureStates; + return segmentOverrides; } -export function getIdentityFeatureState( - environment: EnvironmentModel, - identity: IdentityModel, - featureName: string, - overrideTraits?: TraitModel[] -): FeatureStateModel { - const featureStates = getIdentityFeatureStatesDict(environment, identity, overrideTraits); +/** + * Evaluates all features in the context, applying segment overrides where applicable. + * For each feature: + * - Checks if a segment override exists + * - Uses override values if present, otherwise evaluates the base feature + * - Determines appropriate evaluation reason + * - Handles multivariate evaluation for features without overrides + * + * @param context - EvaluationContext containing features and identity + * @param segmentOverrides - Map of feature keys to their segment overrides + * @returns EvaluationResultFlags containing evaluated flag results + */ +export function evaluateFeatures( + context: EvaluationContextWithMetadata, + segmentOverrides: Record +): EvaluationResultFlags { + const flags: EvaluationResultFlags = {}; - const matchingFeature = Object.values(featureStates).filter( - f => f.feature.name === featureName - ); + for (const feature of Object.values(context.features || {})) { + const segmentOverride = segmentOverrides[feature.feature_key]; + const finalFeature = segmentOverride ? segmentOverride.feature : feature; + const hasOverride = !!segmentOverride; + + const { value: evaluatedValue, reason: evaluatedReason } = hasOverride + ? { value: finalFeature.value, reason: undefined } + : evaluateFeatureValue(finalFeature, context.identity?.key); - if (matchingFeature.length === 0) { - throw new FeatureStateNotFound('Feature State Not Found'); + flags[finalFeature.name] = { + feature_key: finalFeature.feature_key, + name: finalFeature.name, + enabled: finalFeature.enabled, + value: evaluatedValue, + ...(finalFeature.metadata ? { metadata: finalFeature.metadata } : {}), + reason: + evaluatedReason ?? + getTargetingMatchReason({ type: 'SEGMENT', override: segmentOverride }) + } as FlagResultWithMetadata; } - return matchingFeature[0]; + return flags; } -export function getIdentityFeatureStates( - environment: EnvironmentModel, - identity: IdentityModel, - overrideTraits?: TraitModel[] -): FeatureStateModel[] { - const featureStates = Object.values( - getIdentityFeatureStatesDict(environment, identity, overrideTraits) - ); - - if (environment.project.hideDisabledFlags) { - return featureStates.filter(fs => !!fs.enabled); +function evaluateFeatureValue( + feature: FeatureContextWithMetadata, + identityKey?: string +): { value: any; reason?: string } { + if (!!feature.variants && feature.variants.length > 0 && !!identityKey) { + return getMultivariateFeatureValue(feature, identityKey); } - return featureStates; + + return { value: feature.value, reason: undefined }; } -export function getEnvironmentFeatureState(environment: EnvironmentModel, featureName: string) { - const featuresStates = environment.featureStates.filter(f => f.feature.name === featureName); +/** + * Evaluates a multivariate feature flag to determine which variant value to return for a given identity. + * + * Uses deterministic hashing to ensure the same identity always receives the same variant, + * while distributing variants according to their configured weight percentages. + * + * @param feature - The feature context containing variants and their weights + * @param identityKey - The identity key used for deterministic variant selection + * @returns The variant value if the identity falls within a variant's range, otherwise the default feature value + */ +function getMultivariateFeatureValue( + feature: FeatureContextWithMetadata, + identityKey?: string +): { value: any; reason?: string } { + const percentageValue = getHashedPercentageForObjIds([feature.key, identityKey]); + const sortedVariants = [...(feature?.variants || [])].sort((a, b) => { + return (a.priority ?? Infinity) - (b.priority ?? Infinity); + }); - if (featuresStates.length === 0) { - throw new FeatureStateNotFound('Feature State Not Found'); + let startPercentage = 0; + for (const variant of sortedVariants) { + const limit = startPercentage + variant.weight; + if (startPercentage <= percentageValue && percentageValue < limit) { + return { + value: variant.value, + reason: getTargetingMatchReason({ type: 'SPLIT', weight: variant.weight }) + }; + } + startPercentage = limit; } - return featuresStates[0]; + return { value: feature.value, reason: undefined }; } -export function getEnvironmentFeatureStates(environment: EnvironmentModel): FeatureStateModel[] { - if (environment.project.hideDisabledFlags) { - return environment.featureStates.filter(fs => !!fs.enabled); - } - return environment.featureStates; +export function shouldApplyOverride( + override: any, + existingOverrides: Record +): boolean { + const currentOverride = existingOverrides[override.feature_key]; + return ( + !currentOverride || isHigherPriority(override.priority, currentOverride.feature.priority) + ); } + +export function isHigherPriority( + priorityA: number | undefined, + priorityB: number | undefined +): boolean { + return (priorityA ?? Infinity) < (priorityB ?? Infinity); +} + +export type TargetingMatchReason = + | { + type: 'SEGMENT'; + override: SegmentOverride; + } + | { + type: 'SPLIT'; + weight: number; + }; + +const getTargetingMatchReason = (matchObject: TargetingMatchReason) => { + const { type } = matchObject; + + if (type === 'SEGMENT') { + return matchObject.override + ? `${TARGETING_REASONS.TARGETING_MATCH}; segment=${matchObject.override.segmentName}` + : TARGETING_REASONS.DEFAULT; + } + + if (type === 'SPLIT') { + return `${TARGETING_REASONS.SPLIT}; weight=${matchObject.weight}`; + } + + return TARGETING_REASONS.DEFAULT; +}; diff --git a/flagsmith-engine/segments/constants.ts b/flagsmith-engine/segments/constants.ts index d2a3e9b..fad1660 100644 --- a/flagsmith-engine/segments/constants.ts +++ b/flagsmith-engine/segments/constants.ts @@ -4,6 +4,7 @@ export const ANY_RULE = 'ANY'; export const NONE_RULE = 'NONE'; export const RULE_TYPES = [ALL_RULE, ANY_RULE, NONE_RULE]; +export const IDENTITY_OVERRIDE_SEGMENT_NAME = 'identity_overrides'; // Segment Condition Operators export const EQUAL = 'EQUAL'; diff --git a/flagsmith-engine/segments/evaluators.ts b/flagsmith-engine/segments/evaluators.ts index f5d0081..0b08526 100644 --- a/flagsmith-engine/segments/evaluators.ts +++ b/flagsmith-engine/segments/evaluators.ts @@ -1,76 +1,175 @@ -import { EnvironmentModel } from '../environments/models.js'; -import { IdentityModel } from '../identities/models.js'; -import { TraitModel } from '../identities/traits/models.js'; -import { getHashedPercentateForObjIds } from '../utils/hashing/index.js'; -import { PERCENTAGE_SPLIT, IS_SET, IS_NOT_SET } from './constants.js'; -import { SegmentConditionModel, SegmentModel, SegmentRuleModel } from './models.js'; - -export function getIdentitySegments( - environment: EnvironmentModel, - identity: IdentityModel, - overrideTraits?: TraitModel[] -): SegmentModel[] { - return environment.project.segments.filter(segment => - evaluateIdentityInSegment(identity, segment, overrideTraits) - ); +import * as jsonpath from 'jsonpath'; +import { + GenericEvaluationContext, + InSegmentCondition, + SegmentCondition, + SegmentContext, + SegmentRule +} from '../evaluation/models.js'; +import { getHashedPercentageForObjIds } from '../utils/hashing/index.js'; +import { SegmentConditionModel } from './models.js'; +import { IS_NOT_SET, IS_SET, PERCENTAGE_SPLIT } from './constants.js'; + +/** + * Returns all segments that the identity belongs to based on segment rules evaluation. + * + * An identity belongs to a segment if it matches ALL of the segment's rules. + * If the context has no identity or segments, returns an empty array. + * + * @param context - Evaluation context containing identity and segment definitions + * @returns Array of segments that the identity matches + */ +export function getIdentitySegments(context: GenericEvaluationContext): SegmentContext[] { + if (!context.identity || !context.segments) return []; + + return Object.values(context.segments).filter(segment => { + if (segment.rules.length === 0) return false; + return segment.rules.every(rule => traitsMatchSegmentRule(rule, segment.key, context)); + }); } -export function evaluateIdentityInSegment( - identity: IdentityModel, - segment: SegmentModel, - overrideTraits?: TraitModel[] +/** + * Evaluates whether a segment condition matches the identity's traits or context values. + * + * Handles different types of conditions: + * - PERCENTAGE_SPLIT: Deterministic percentage-based bucketing using identity key + * - IS_SET/IS_NOT_SET: Checks for trait existence + * - Standard operators: EQUAL, NOT_EQUAL, etc. via SegmentConditionModel + * - JSONPath expressions: $.identity.identifier, $.environment.name, etc. + * + * @param condition - The condition to evaluate (property, operator, value) + * @param segmentKey - Key of the segment (used for percentage split hashing) + * @param context - Evaluation context containing identity, traits, and environment + * @returns true if the condition matches + */ +export function traitsMatchSegmentCondition( + condition: SegmentCondition | InSegmentCondition, + segmentKey: string, + context?: GenericEvaluationContext ): boolean { - return ( - segment.rules.length > 0 && - segment.rules.filter(rule => - traitsMatchSegmentRule( - overrideTraits || identity.identityTraits, - rule, - segment.id, - identity.djangoID || identity.compositeKey - ) - ).length === segment.rules.length - ); + if (condition.operator === PERCENTAGE_SPLIT) { + const contextValueKey = + getContextValue(condition.property, context) || context?.identity?.key; + const hashedPercentage = getHashedPercentageForObjIds([segmentKey, contextValueKey]); + return hashedPercentage <= parseFloat(String(condition.value)); + } + if (!condition.property) { + return false; + } + + const traitValue = getTraitValue(condition.property, context); + + if (condition.operator === IS_SET) { + return traitValue !== undefined && traitValue !== null; + } + if (condition.operator === IS_NOT_SET) { + return traitValue === undefined || traitValue === null; + } + + if (traitValue !== undefined && traitValue !== null) { + const segmentCondition = new SegmentConditionModel( + condition.operator, + condition.value as string, + condition.property + ); + return segmentCondition.matchesTraitValue(traitValue); + } + + return false; } function traitsMatchSegmentRule( - identityTraits: TraitModel[], - rule: SegmentRuleModel, - segmentId: number | string, - identityId: number | string + rule: SegmentRule, + segmentKey: string, + context?: GenericEvaluationContext +): boolean { + const matchesConditions = evaluateConditions(rule, segmentKey, context); + const matchesSubRules = evaluateSubRules(rule, segmentKey, context); + + return matchesConditions && matchesSubRules; +} + +function evaluateConditions( + rule: SegmentRule, + segmentKey: string, + context?: GenericEvaluationContext ): boolean { - const matchesConditions = - rule.conditions.length > 0 - ? rule.matchingFunction()( - rule.conditions.map(condition => - traitsMatchSegmentCondition(identityTraits, condition, segmentId, identityId) - ) - ) - : true; - return ( - matchesConditions && - rule.rules.filter(rule => - traitsMatchSegmentRule(identityTraits, rule, segmentId, identityId) - ).length === rule.rules.length + if (!rule.conditions || rule.conditions.length === 0) return true; + + const conditionResults = rule.conditions.map((condition: SegmentCondition) => + traitsMatchSegmentCondition(condition, segmentKey, context) ); + + return evaluateRuleConditions(rule.type, conditionResults); } -export function traitsMatchSegmentCondition( - identityTraits: TraitModel[], - condition: SegmentConditionModel, - segmentId: number | string, - identityId: number | string +function evaluateSubRules( + rule: SegmentRule, + segmentKey: string, + context?: GenericEvaluationContext ): boolean { - if (condition.operator == PERCENTAGE_SPLIT) { - var hashedPercentage = getHashedPercentateForObjIds([segmentId, identityId]); - return hashedPercentage <= parseFloat(String(condition.value)); + if (!rule.rules || rule.rules.length === 0) return true; + + return rule.rules.every((subRule: SegmentRule) => + traitsMatchSegmentRule(subRule, segmentKey, context) + ); +} + +function evaluateRuleConditions(ruleType: string, conditionResults: boolean[]): boolean { + switch (ruleType) { + case 'ALL': + return conditionResults.length === 0 || conditionResults.every(result => result); + case 'ANY': + return conditionResults.length > 0 && conditionResults.some(result => result); + case 'NONE': + return conditionResults.length === 0 || conditionResults.every(result => !result); + default: + return false; } - const traits = identityTraits.filter(t => t.traitKey === condition.property_); - const trait = traits.length > 0 ? traits[0] : undefined; - if (condition.operator === IS_SET) { - return !!trait; - } else if (condition.operator === IS_NOT_SET) { - return trait == undefined; +} + +function getTraitValue(property: string, context?: GenericEvaluationContext): any { + if (property.startsWith('$.')) { + const contextValue = getContextValue(property, context); + if (contextValue && !isNonPrimitive(contextValue)) { + return contextValue; + } + } + + const traits = context?.identity?.traits || {}; + return traits[property]; +} + +function isNonPrimitive(value: any): boolean { + if (value === null || value === undefined) { + return false; + } + + // Objects and arrays are non-primitive + return typeof value === 'object'; +} + +/** + * Evaluates JSONPath expressions against the evaluation context. + * + * Supports accessing nested context values using JSONPath syntax. + * Commonly used paths: + * - $.identity.identifier - User's unique identifier + * - $.identity.key - User's internal key + * - $.environment.name - Environment name + * - $.environment.key - Environment key + * + * @param jsonPath - JSONPath expression starting with '$.' + * @param context - Evaluation context to query against + * @returns The resolved value, or undefined if path doesn't exist or is invalid + */ +export function getContextValue(jsonPath: string, context?: GenericEvaluationContext): any { + if (!context || !jsonPath?.startsWith('$.')) return undefined; + + try { + const results = jsonpath.query(context, jsonPath); + return results.length > 0 ? results[0] : undefined; + } catch (error) { + return undefined; } - return trait ? condition.matchesTraitValue(trait.traitValue) : false; } diff --git a/flagsmith-engine/segments/models.ts b/flagsmith-engine/segments/models.ts index 67aca0d..b912867 100644 --- a/flagsmith-engine/segments/models.ts +++ b/flagsmith-engine/segments/models.ts @@ -1,6 +1,11 @@ import * as semver from 'semver'; -import { FeatureStateModel } from '../features/models.js'; +import { + FeatureModel, + FeatureStateModel, + MultivariateFeatureOptionModel, + MultivariateFeatureStateValueModel +} from '../features/models.js'; import { getCastingFunction as getCastingFunction } from '../utils/index.js'; import { ALL_RULE, @@ -13,6 +18,12 @@ import { CONDITION_OPERATORS } from './constants.js'; import { isSemver } from './util.js'; +import { + EvaluationContext, + Overrides +} from '../evaluation/evaluationContext/evaluationContext.types.js'; +import { CONSTANTS } from '../features/constants.js'; +import { EvaluationResultSegments, SegmentSource } from '../evaluation/models.js'; export const all = (iterable: Array) => iterable.filter(e => !!e).length === iterable.length; export const any = (iterable: Array) => iterable.filter(e => !!e).length > 0; @@ -26,22 +37,45 @@ export const matchingFunctions = { [CONDITION_OPERATORS.LESS_THAN_INCLUSIVE]: (thisValue: any, otherValue: any) => thisValue >= otherValue, [CONDITION_OPERATORS.NOT_EQUAL]: (thisValue: any, otherValue: any) => thisValue != otherValue, - [CONDITION_OPERATORS.CONTAINS]: (thisValue: any, otherValue: any) => - !!otherValue && otherValue.includes(thisValue) + [CONDITION_OPERATORS.CONTAINS]: (thisValue: any, otherValue: any) => { + try { + return !!otherValue && otherValue.includes(thisValue); + } catch { + return false; + } + } +}; + +// Semver library throws an error if the version is invalid, in this case, we want to catch and return false +const safeSemverCompare = ( + semverMatchingFunction: (conditionValue: any, traitValue: any) => boolean +) => { + return (conditionValue: any, traitValue: any) => { + try { + return semverMatchingFunction(conditionValue, traitValue); + } catch { + return false; + } + }; }; export const semverMatchingFunction = { ...matchingFunctions, - [CONDITION_OPERATORS.EQUAL]: (thisValue: any, otherValue: any) => - semver.eq(thisValue, otherValue), - [CONDITION_OPERATORS.GREATER_THAN]: (thisValue: any, otherValue: any) => - semver.gt(otherValue, thisValue), - [CONDITION_OPERATORS.GREATER_THAN_INCLUSIVE]: (thisValue: any, otherValue: any) => - semver.gte(otherValue, thisValue), - [CONDITION_OPERATORS.LESS_THAN]: (thisValue: any, otherValue: any) => - semver.gt(thisValue, otherValue), - [CONDITION_OPERATORS.LESS_THAN_INCLUSIVE]: (thisValue: any, otherValue: any) => - semver.gte(thisValue, otherValue) + [CONDITION_OPERATORS.EQUAL]: safeSemverCompare((conditionValue, traitValue) => + semver.eq(traitValue, conditionValue) + ), + [CONDITION_OPERATORS.GREATER_THAN]: safeSemverCompare((conditionValue, traitValue) => + semver.gt(traitValue, conditionValue) + ), + [CONDITION_OPERATORS.GREATER_THAN_INCLUSIVE]: safeSemverCompare((conditionValue, traitValue) => + semver.gte(traitValue, conditionValue) + ), + [CONDITION_OPERATORS.LESS_THAN]: safeSemverCompare((conditionValue, traitValue) => + semver.lt(traitValue, conditionValue) + ), + [CONDITION_OPERATORS.LESS_THAN_INCLUSIVE]: safeSemverCompare((conditionValue, traitValue) => + semver.lte(traitValue, conditionValue) + ) }; export const getMatchingFunctions = (semver: boolean) => @@ -56,17 +90,17 @@ export class SegmentConditionModel { }; operator: string; - value: string | null | undefined; - property_: string | null | undefined; + value: string | null | undefined | string[]; + property: string | null | undefined; constructor( operator: string, - value?: string | null | undefined, + value?: string | null | undefined | string[], property?: string | null | undefined ) { this.operator = operator; this.value = value; - this.property_ = property; + this.property = property; } matchesTraitValue(traitValue: any) { @@ -79,17 +113,49 @@ export class SegmentConditionModel { ); }, evaluateRegex: (traitValue: any) => { - return !!this.value && !!traitValue?.toString().match(new RegExp(this.value)); + try { + if (!this.value) { + return false; + } + const regex = new RegExp(this.value?.toString()); + return !!traitValue?.toString().match(regex); + } catch { + return false; + } }, evaluateModulo: (traitValue: any) => { - if (isNaN(parseFloat(traitValue)) || !this.value) { + const parsedTraitValue = parseFloat(traitValue); + if (isNaN(parsedTraitValue) || !this.value) { + return false; + } + + const parts = this.value.toString().split('|'); + if (parts.length !== 2) { return false; } - const parts = this.value.split('|'); - const [divisor, reminder] = [parseFloat(parts[0]), parseFloat(parts[1])]; - return traitValue % divisor === reminder; + + const divisor = parseFloat(parts[0]); + const remainder = parseFloat(parts[1]); + + if (isNaN(divisor) || isNaN(remainder) || divisor === 0) { + return false; + } + + return parsedTraitValue % divisor === remainder; }, - evaluateIn: (traitValue: any) => { + evaluateIn: (traitValue: string[] | string) => { + if (Array.isArray(this.value)) { + return this.value.includes(traitValue.toString()); + } + + if (typeof this.value === 'string') { + try { + const parsed = JSON.parse(this.value); + if (Array.isArray(parsed)) { + return parsed.includes(traitValue.toString()); + } + } catch {} + } return this.value?.split(',').includes(traitValue.toString()); } }; @@ -144,4 +210,73 @@ export class SegmentModel { this.id = id; this.name = name; } + + static fromSegmentResult( + segmentResults: EvaluationResultSegments, + evaluationContext: EvaluationContext + ): SegmentModel[] { + const segmentModels: SegmentModel[] = []; + if (!evaluationContext.segments) { + return []; + } + + for (const segmentResult of segmentResults) { + if (segmentResult.metadata?.source === SegmentSource.IDENTITY_OVERRIDE) { + continue; + } + const segmentContext = evaluationContext.segments[segmentResult.key]; + if (segmentContext) { + const segment = new SegmentModel(parseInt(segmentContext.key), segmentContext.name); + segment.rules = segmentContext.rules.map(rule => new SegmentRuleModel(rule.type)); + segment.featureStates = SegmentModel.createFeatureStatesFromOverrides( + segmentContext.overrides || [] + ); + segmentModels.push(segment); + } + } + + return segmentModels; + } + + private static createFeatureStatesFromOverrides(overrides: Overrides): FeatureStateModel[] { + if (!overrides) return []; + return overrides.map(override => { + const feature = new FeatureModel( + parseInt(override.feature_key), + override.name, + override.variants?.length && override.variants.length > 0 + ? CONSTANTS.MULTIVARIATE + : CONSTANTS.STANDARD + ); + + const featureState = new FeatureStateModel( + feature, + override.enabled, + override.priority || 0 + ); + + if (override.value !== undefined) { + featureState.setValue(override.value); + } + + if (override.variants && override.variants.length > 0) { + featureState.multivariateFeatureStateValues = this.createMultivariateValues( + override.variants + ); + } + + return featureState; + }); + } + + private static createMultivariateValues(variants: any[]): MultivariateFeatureStateValueModel[] { + return variants.map( + variant => + new MultivariateFeatureStateValueModel( + new MultivariateFeatureOptionModel(variant.value, variant.id as number), + variant.weight as number, + variant.id as number + ) + ); + } } diff --git a/flagsmith-engine/utils/hashing/index.ts b/flagsmith-engine/utils/hashing/index.ts index 72f3f46..1390d13 100644 --- a/flagsmith-engine/utils/hashing/index.ts +++ b/flagsmith-engine/utils/hashing/index.ts @@ -14,7 +14,7 @@ const makeRepeated = (arr: Array, repeats: number) => * @param {} iterations=1 num times to include each id in the generated string to hash * @returns number number between 0 (inclusive) and 100 (exclusive) */ -export function getHashedPercentateForObjIds(objectIds: Array, iterations = 1): number { +export function getHashedPercentageForObjIds(objectIds: Array, iterations = 1): number { let toHash = makeRepeated(objectIds, iterations).join(','); const hashedValue = md5(toHash); const hashedInt = BigInt('0x' + hashedValue); @@ -24,7 +24,7 @@ export function getHashedPercentateForObjIds(objectIds: Array, iterations = /* istanbul ignore next */ if (value === 100) { /* istanbul ignore next */ - return getHashedPercentateForObjIds(objectIds, iterations + 1); + return getHashedPercentageForObjIds(objectIds, iterations + 1); } return value; diff --git a/package-lock.json b/package-lock.json index 4e22ebc..c630255 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,17 +9,24 @@ "version": "6.2.0", "license": "MIT", "dependencies": { + "jsonpath": "^1.1.1", "pino": "^8.8.0", "semver": "^7.3.7", "undici-types": "^6.19.8" }, "devDependencies": { + "@types/jest": "^30.0.0", + "@types/jsonpath": "^0.2.4", "@types/node": "^20.16.10", "@types/semver": "^7.3.9", "@types/uuid": "^8.3.4", "@vitest/coverage-v8": "^2.1.2", "esbuild": "^0.25.0", "husky": "^7.0.4", + "install": "^0.13.0", + "json-schema-to-typescript": "^15.0.4", + "jsonc-parser": "^3.3.1", + "npm": "^11.6.1", "prettier": "^2.2.1", "typescript": "^4.9.5", "undici": "^6.19.8", @@ -42,6 +49,39 @@ "node": ">=6.0.0" } }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "11.9.3", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.9.3.tgz", + "integrity": "sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.15", + "js-yaml": "^4.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/philsturgeon" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.25.7", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz", @@ -52,10 +92,11 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz", - "integrity": "sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -625,6 +666,85 @@ "node": ">=8" } }, + "node_modules/@jest/diff-sequences": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", + "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.1.2.tgz", + "integrity": "sha512-HXy1qT/bfdjCv7iC336ExbqqYtZvljrV8odNdso7dWK9bSeHtLlvwWWC3YSybSPL03Gg5rug6WLCZAZFH72m0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", @@ -673,6 +793,13 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "dev": true, + "license": "MIT" + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -949,6 +1076,13 @@ "win32" ] }, + "node_modules/@sinclair/typebox": { + "version": "0.34.41", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", + "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -956,6 +1090,65 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^30.0.0", + "pretty-format": "^30.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsonpath": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@types/jsonpath/-/jsonpath-0.2.4.tgz", + "integrity": "sha512-K3hxB8Blw0qgW6ExKgMbXQv2UPZBoE2GqLpVY+yr7nMD2Pq86lsuIzyAaiQ7eMqFL5B6di6pxSkogLJEyEHoGA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.16.11", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.11.tgz", @@ -971,12 +1164,36 @@ "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", "dev": true }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/uuid": { "version": "8.3.4", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", "dev": true }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@vitest/coverage-v8": { "version": "2.1.9", "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.9.tgz", @@ -1158,6 +1375,13 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -1180,7 +1404,8 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/base64-js": { "version": "1.5.1", @@ -1201,6 +1426,29 @@ } ] }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", @@ -1251,6 +1499,23 @@ "node": ">=12" } }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/check-error": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", @@ -1261,6 +1526,22 @@ "node": ">= 16" } }, + "node_modules/ci-info": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz", + "integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1320,6 +1601,12 @@ "node": ">=6" } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "license": "MIT" + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -1754,6 +2041,72 @@ "node": ">=18" } }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=4.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esprima": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.2.2.tgz", + "integrity": "sha512-+JpPZam9w5DuJ3Q67SqsMGtiHKENSMRVoxvArfJZK01/BfLEObtZ6orJa/MtoGNR/rfMgp5837T41PAmTwAv/A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -1764,6 +2117,15 @@ "@types/estree": "^1.0.0" } }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", @@ -1780,6 +2142,24 @@ "node": ">=0.8.x" } }, + "node_modules/expect": { + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.1.2.tgz", + "integrity": "sha512-xvHszRavo28ejws8FpemjhwswGj4w/BetHIL8cU49u4sGyXDw2+p3YbeDbj6xzlxi6kWTjIRSTJ+9sNXPnF0Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.1.2", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.1.2", + "jest-message-util": "30.1.0", + "jest-mock": "30.0.5", + "jest-util": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/expect-type": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.1.0.tgz", @@ -1790,6 +2170,12 @@ "node": ">=12.0.0" } }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "license": "MIT" + }, "node_modules/fast-redact": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", @@ -1798,6 +2184,37 @@ "node": ">=6" } }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", @@ -1841,6 +2258,34 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -1890,6 +2335,26 @@ } ] }, + "node_modules/install": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/install/-/install-0.13.0.tgz", + "integrity": "sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -1899,14 +2364,37 @@ "node": ">=8" } }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "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, @@ -1925,128 +2413,2999 @@ "supports-color": "^7.1.0" }, "engines": { - "node": ">=10" + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "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-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/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest-diff": { + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.1.2.tgz", + "integrity": "sha512-4+prq+9J61mOVXCa4Qp8ZjavdxzrWQXrI80GNxP8f4tkI2syPuPrJgdRPZRrfUTRvIoUwcmNLbqEJy9W800+NQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.0.1", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.1.2.tgz", + "integrity": "sha512-7ai16hy4rSbDjvPTuUhuV8nyPBd6EX34HkBsBcBX2lENCuAQ0qKCPb/+lt8OSWUa9WWmGYLy41PrEzkwRwoGZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.1.2", + "pretty-format": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.1.0.tgz", + "integrity": "sha512-HizKDGG98cYkWmaLUHChq4iN+oCENohQLb7Z5guBPumYs+/etonmNFlg1Ps6yN9LTPyZn+M+b/9BbnHx3WTMDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.0.5", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "pretty-format": "30.0.5", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.0.5.tgz", + "integrity": "sha512-Od7TyasAAQX/6S+QCbN6vZoWOMwlTtzzGuxJku1GhGanAjz9y+QsQkpScDmETvdc9aSXyJ/Op4rhpMYBWW91wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.5", + "@types/node": "*", + "jest-util": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.5.tgz", + "integrity": "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.5", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-to-typescript": { + "version": "15.0.4", + "resolved": "https://registry.npmjs.org/json-schema-to-typescript/-/json-schema-to-typescript-15.0.4.tgz", + "integrity": "sha512-Su9oK8DR4xCmDsLlyvadkXzX6+GGXJpbhwoLtOGArAG61dvbW4YQmSEno2y66ahpIdmLMg6YUf/QHLgiwvkrHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^11.5.5", + "@types/json-schema": "^7.0.15", + "@types/lodash": "^4.17.7", + "is-glob": "^4.0.3", + "js-yaml": "^4.1.0", + "lodash": "^4.17.21", + "minimist": "^1.2.8", + "prettier": "^3.2.5", + "tinyglobby": "^0.2.9" + }, + "bin": { + "json2ts": "dist/src/cli.js" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/json-schema-to-typescript/node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonpath": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/jsonpath/-/jsonpath-1.1.1.tgz", + "integrity": "sha512-l6Cg7jRpixfbgoWgkrl77dgEj8RPvND0wMH6TwQmi9Qs4TFfS9u5cUFnbeKTwj5ga5Y3BTGGNI28k117LJ009w==", + "license": "MIT", + "dependencies": { + "esprima": "1.2.2", + "static-eval": "2.0.2", + "underscore": "1.12.1" + } + }, + "node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "license": "MIT", + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", + "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "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/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "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" + } + }, + "node_modules/npm": { + "version": "11.6.1", + "resolved": "https://registry.npmjs.org/npm/-/npm-11.6.1.tgz", + "integrity": "sha512-7iDSHDoup6uMQJ37yWrhfqcbMhF0UEfGRap6Nv+aKQcrIJXlCi2cKbj75WBmiHlcwsQCy/U0zEwDZdAx6H/Vaw==", + "bundleDependencies": [ + "@isaacs/string-locale-compare", + "@npmcli/arborist", + "@npmcli/config", + "@npmcli/fs", + "@npmcli/map-workspaces", + "@npmcli/package-json", + "@npmcli/promise-spawn", + "@npmcli/redact", + "@npmcli/run-script", + "@sigstore/tuf", + "abbrev", + "archy", + "cacache", + "chalk", + "ci-info", + "cli-columns", + "fastest-levenshtein", + "fs-minipass", + "glob", + "graceful-fs", + "hosted-git-info", + "ini", + "init-package-json", + "is-cidr", + "json-parse-even-better-errors", + "libnpmaccess", + "libnpmdiff", + "libnpmexec", + "libnpmfund", + "libnpmorg", + "libnpmpack", + "libnpmpublish", + "libnpmsearch", + "libnpmteam", + "libnpmversion", + "make-fetch-happen", + "minimatch", + "minipass", + "minipass-pipeline", + "ms", + "node-gyp", + "nopt", + "normalize-package-data", + "npm-audit-report", + "npm-install-checks", + "npm-package-arg", + "npm-pick-manifest", + "npm-profile", + "npm-registry-fetch", + "npm-user-validate", + "p-map", + "pacote", + "parse-conflict-json", + "proc-log", + "qrcode-terminal", + "read", + "semver", + "spdx-expression-parse", + "ssri", + "supports-color", + "tar", + "text-table", + "tiny-relative-date", + "treeverse", + "validate-npm-package-name", + "which" + ], + "dev": true, + "license": "Artistic-2.0", + "workspaces": [ + "docs", + "smoke-tests", + "mock-globals", + "mock-registry", + "workspaces/*" + ], + "dependencies": { + "@isaacs/string-locale-compare": "^1.1.0", + "@npmcli/arborist": "^9.1.5", + "@npmcli/config": "^10.4.1", + "@npmcli/fs": "^4.0.0", + "@npmcli/map-workspaces": "^5.0.0", + "@npmcli/package-json": "^7.0.1", + "@npmcli/promise-spawn": "^8.0.3", + "@npmcli/redact": "^3.2.2", + "@npmcli/run-script": "^10.0.0", + "@sigstore/tuf": "^4.0.0", + "abbrev": "^3.0.1", + "archy": "~1.0.0", + "cacache": "^20.0.1", + "chalk": "^5.6.2", + "ci-info": "^4.3.0", + "cli-columns": "^4.0.0", + "fastest-levenshtein": "^1.0.16", + "fs-minipass": "^3.0.3", + "glob": "^11.0.3", + "graceful-fs": "^4.2.11", + "hosted-git-info": "^9.0.0", + "ini": "^5.0.0", + "init-package-json": "^8.2.2", + "is-cidr": "^6.0.0", + "json-parse-even-better-errors": "^4.0.0", + "libnpmaccess": "^10.0.2", + "libnpmdiff": "^8.0.8", + "libnpmexec": "^10.1.7", + "libnpmfund": "^7.0.8", + "libnpmorg": "^8.0.1", + "libnpmpack": "^9.0.8", + "libnpmpublish": "^11.1.1", + "libnpmsearch": "^9.0.1", + "libnpmteam": "^8.0.2", + "libnpmversion": "^8.0.2", + "make-fetch-happen": "^15.0.2", + "minimatch": "^10.0.3", + "minipass": "^7.1.1", + "minipass-pipeline": "^1.2.4", + "ms": "^2.1.2", + "node-gyp": "^11.4.2", + "nopt": "^8.1.0", + "normalize-package-data": "^8.0.0", + "npm-audit-report": "^6.0.0", + "npm-install-checks": "^7.1.2", + "npm-package-arg": "^13.0.0", + "npm-pick-manifest": "^11.0.1", + "npm-profile": "^12.0.0", + "npm-registry-fetch": "^19.0.0", + "npm-user-validate": "^3.0.0", + "p-map": "^7.0.3", + "pacote": "^21.0.3", + "parse-conflict-json": "^4.0.0", + "proc-log": "^5.0.0", + "qrcode-terminal": "^0.12.0", + "read": "^4.1.0", + "semver": "^7.7.2", + "spdx-expression-parse": "^4.0.0", + "ssri": "^12.0.0", + "supports-color": "^10.2.2", + "tar": "^7.5.1", + "text-table": "~0.2.0", + "tiny-relative-date": "^2.0.2", + "treeverse": "^3.0.0", + "validate-npm-package-name": "^6.0.2", + "which": "^5.0.0" + }, + "bin": { + "npm": "bin/npm-cli.js", + "npx": "bin/npx-cli.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/npm/node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui": { + "version": "8.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/npm/node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/npm/node_modules/@isaacs/string-locale-compare": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/@npmcli/agent": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^11.2.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@npmcli/arborist": { + "version": "9.1.5", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@isaacs/string-locale-compare": "^1.1.0", + "@npmcli/fs": "^4.0.0", + "@npmcli/installed-package-contents": "^3.0.0", + "@npmcli/map-workspaces": "^5.0.0", + "@npmcli/metavuln-calculator": "^9.0.2", + "@npmcli/name-from-folder": "^3.0.0", + "@npmcli/node-gyp": "^4.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/query": "^4.0.0", + "@npmcli/redact": "^3.0.0", + "@npmcli/run-script": "^10.0.0", + "bin-links": "^5.0.0", + "cacache": "^20.0.1", + "common-ancestor-path": "^1.0.1", + "hosted-git-info": "^9.0.0", + "json-stringify-nice": "^1.1.4", + "lru-cache": "^11.2.1", + "minimatch": "^10.0.3", + "nopt": "^8.0.0", + "npm-install-checks": "^7.1.0", + "npm-package-arg": "^13.0.0", + "npm-pick-manifest": "^11.0.1", + "npm-registry-fetch": "^19.0.0", + "pacote": "^21.0.2", + "parse-conflict-json": "^4.0.0", + "proc-log": "^5.0.0", + "proggy": "^3.0.0", + "promise-all-reject-late": "^1.0.0", + "promise-call-limit": "^3.0.1", + "semver": "^7.3.7", + "ssri": "^12.0.0", + "treeverse": "^3.0.0", + "walk-up-path": "^4.0.0" + }, + "bin": { + "arborist": "bin/index.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@npmcli/config": { + "version": "10.4.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/map-workspaces": "^5.0.0", + "@npmcli/package-json": "^7.0.0", + "ci-info": "^4.0.0", + "ini": "^5.0.0", + "nopt": "^8.1.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "walk-up-path": "^4.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@npmcli/fs": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/git": { + "version": "7.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/promise-spawn": "^8.0.0", + "ini": "^5.0.0", + "lru-cache": "^11.2.1", + "npm-pick-manifest": "^11.0.1", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^5.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@npmcli/installed-package-contents": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-bundled": "^4.0.0", + "npm-normalize-package-bin": "^4.0.0" + }, + "bin": { + "installed-package-contents": "bin/index.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/map-workspaces": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/name-from-folder": "^3.0.0", + "@npmcli/package-json": "^7.0.0", + "glob": "^11.0.3", + "minimatch": "^10.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@npmcli/metavuln-calculator": { + "version": "9.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "cacache": "^20.0.0", + "json-parse-even-better-errors": "^4.0.0", + "pacote": "^21.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@npmcli/name-from-folder": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/node-gyp": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/package-json": { + "version": "7.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^7.0.0", + "glob": "^11.0.3", + "hosted-git-info": "^9.0.0", + "json-parse-even-better-errors": "^4.0.0", + "proc-log": "^5.0.0", + "semver": "^7.5.3", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@npmcli/promise-spawn": { + "version": "8.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "which": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/query": { + "version": "4.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/redact": { + "version": "3.2.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/run-script": { + "version": "10.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/node-gyp": "^4.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/promise-spawn": "^8.0.0", + "node-gyp": "^11.0.0", + "proc-log": "^5.0.0", + "which": "^5.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/npm/node_modules/@sigstore/bundle": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.5.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@sigstore/core": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@sigstore/protobuf-specs": { + "version": "0.5.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@sigstore/sign": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.0.0", + "@sigstore/protobuf-specs": "^0.5.0", + "make-fetch-happen": "^15.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@sigstore/tuf": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.5.0", + "tuf-js": "^4.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@sigstore/verify": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.0.0", + "@sigstore/protobuf-specs": "^0.5.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@tufjs/canonical-json": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@tufjs/models": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^9.0.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@tufjs/models/node_modules/minimatch": { + "version": "9.0.5", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/abbrev": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/agent-base": { + "version": "7.1.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/npm/node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/ansi-styles": { + "version": "6.2.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/npm/node_modules/aproba": { + "version": "2.1.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/archy": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/bin-links": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "cmd-shim": "^7.0.0", + "npm-normalize-package-bin": "^4.0.0", + "proc-log": "^5.0.0", + "read-cmd-shim": "^5.0.0", + "write-file-atomic": "^6.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/binary-extensions": { + "version": "3.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/brace-expansion": { + "version": "2.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/npm/node_modules/cacache": { + "version": "20.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^4.0.0", + "fs-minipass": "^3.0.0", + "glob": "^11.0.3", + "lru-cache": "^11.1.0", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^12.0.0", + "unique-filename": "^4.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/chalk": { + "version": "5.6.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/npm/node_modules/chownr": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/npm/node_modules/ci-info": { + "version": "4.3.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/cidr-regex": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "ip-regex": "^5.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/npm/node_modules/cli-columns": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/npm/node_modules/cmd-shim": { + "version": "7.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/npm/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/common-ancestor-path": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/cross-spawn": { + "version": "7.0.6", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/cross-spawn/node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/cssesc": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/debug": { + "version": "4.4.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/npm/node_modules/diff": { + "version": "8.0.2", + "dev": true, + "inBundle": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/npm/node_modules/eastasianwidth": { + "version": "0.2.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/encoding": { + "version": "0.1.13", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/npm/node_modules/env-paths": { + "version": "2.2.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/err-code": { + "version": "2.0.3", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/exponential-backoff": { + "version": "3.1.2", + "dev": true, + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/npm/node_modules/fastest-levenshtein": { + "version": "1.0.16", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/npm/node_modules/foreground-child": { + "version": "3.3.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/fs-minipass": { + "version": "3.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/glob": { + "version": "11.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.0.3", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/graceful-fs": { + "version": "4.2.11", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/hosted-git-info": { + "version": "9.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/http-cache-semantics": { + "version": "4.2.0", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause" + }, + "node_modules/npm/node_modules/http-proxy-agent": { + "version": "7.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/npm/node_modules/https-proxy-agent": { + "version": "7.0.6", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/npm/node_modules/iconv-lite": { + "version": "0.6.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/ignore-walk": { + "version": "8.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minimatch": "^10.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/imurmurhash": { + "version": "0.1.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/npm/node_modules/ini": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/init-package-json": { + "version": "8.2.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/package-json": "^7.0.0", + "npm-package-arg": "^13.0.0", + "promzard": "^2.0.0", + "read": "^4.0.0", + "semver": "^7.7.2", + "validate-npm-package-license": "^3.0.4", + "validate-npm-package-name": "^6.0.2" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/ip-address": { + "version": "10.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/npm/node_modules/ip-regex": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/is-cidr": { + "version": "6.0.0", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "cidr-regex": "^5.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/npm/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/isexe": { + "version": "3.1.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/npm/node_modules/jackspeak": { + "version": "4.1.1", + "dev": true, + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/json-parse-even-better-errors": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/json-stringify-nice": { + "version": "1.1.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/jsonparse": { + "version": "1.3.1", + "dev": true, + "engines": [ + "node >= 0.2.0" + ], + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/just-diff": { + "version": "6.0.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/just-diff-apply": { + "version": "5.5.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/libnpmaccess": { + "version": "10.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-package-arg": "^13.0.0", + "npm-registry-fetch": "^19.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmdiff": { + "version": "8.0.8", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^9.1.5", + "@npmcli/installed-package-contents": "^3.0.0", + "binary-extensions": "^3.0.0", + "diff": "^8.0.2", + "minimatch": "^10.0.3", + "npm-package-arg": "^13.0.0", + "pacote": "^21.0.2", + "tar": "^7.5.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmexec": { + "version": "10.1.7", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^9.1.5", + "@npmcli/package-json": "^7.0.0", + "@npmcli/run-script": "^10.0.0", + "ci-info": "^4.0.0", + "npm-package-arg": "^13.0.0", + "pacote": "^21.0.2", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "read": "^4.0.0", + "semver": "^7.3.7", + "signal-exit": "^4.1.0", + "walk-up-path": "^4.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmfund": { + "version": "7.0.8", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^9.1.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmorg": { + "version": "8.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^19.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmpack": { + "version": "9.0.8", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^9.1.5", + "@npmcli/run-script": "^10.0.0", + "npm-package-arg": "^13.0.0", + "pacote": "^21.0.2" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmpublish": { + "version": "11.1.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/package-json": "^7.0.0", + "ci-info": "^4.0.0", + "npm-package-arg": "^13.0.0", + "npm-registry-fetch": "^19.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.7", + "sigstore": "^4.0.0", + "ssri": "^12.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmsearch": { + "version": "9.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-registry-fetch": "^19.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmteam": { + "version": "8.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^19.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmversion": { + "version": "8.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^7.0.0", + "@npmcli/run-script": "^10.0.0", + "json-parse-even-better-errors": "^4.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.7" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/lru-cache": { + "version": "11.2.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/npm/node_modules/make-fetch-happen": { + "version": "15.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/agent": "^4.0.0", + "cacache": "^20.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "ssri": "^12.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/minimatch": { + "version": "10.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/minipass": { + "version": "7.1.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/npm/node_modules/minipass-collect": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/npm/node_modules/minipass-fetch": { + "version": "4.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/npm/node_modules/minipass-flush": { + "version": "1.0.5", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-pipeline": { + "version": "1.2.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-sized": { + "version": "1.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minizlib": { + "version": "3.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/npm/node_modules/ms": { + "version": "2.1.3", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/mute-stream": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/negotiator": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/npm/node_modules/node-gyp": { + "version": "11.4.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^14.0.3", + "nopt": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "tar": "^7.4.3", + "tinyglobby": "^0.2.12", + "which": "^5.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/@npmcli/agent": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/cacache": { + "version": "19.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^4.0.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^12.0.0", + "tar": "^7.4.3", + "unique-filename": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/glob": { + "version": "10.4.5", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/jackspeak": { + "version": "3.4.3", + "dev": true, + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/lru-cache": { + "version": "10.4.3", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/node-gyp/node_modules/make-fetch-happen": { + "version": "14.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/agent": "^3.0.0", + "cacache": "^19.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "ssri": "^12.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/minimatch": { + "version": "9.0.5", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/path-scurry": { + "version": "1.11.1", + "dev": true, + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/nopt": { + "version": "8.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "abbrev": "^3.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/normalize-package-data": { + "version": "8.0.0", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^9.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/npm-audit-report": { + "version": "6.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-bundled": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-install-checks": { + "version": "7.1.2", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-normalize-package-bin": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-package-arg": { + "version": "13.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^9.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/npm-packlist": { + "version": "10.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "ignore-walk": "^8.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/npm-pick-manifest": { + "version": "11.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^7.1.0", + "npm-normalize-package-bin": "^4.0.0", + "npm-package-arg": "^13.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/npm-profile": { + "version": "12.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-registry-fetch": "^19.0.0", + "proc-log": "^5.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/npm-registry-fetch": { + "version": "19.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/redact": "^3.0.0", + "jsonparse": "^1.3.1", + "make-fetch-happen": "^15.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minizlib": "^3.0.1", + "npm-package-arg": "^13.0.0", + "proc-log": "^5.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/npm-user-validate": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/p-map": { + "version": "7.0.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/package-json-from-dist": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/npm/node_modules/pacote": { + "version": "21.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^7.0.0", + "@npmcli/installed-package-contents": "^3.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/promise-spawn": "^8.0.0", + "@npmcli/run-script": "^10.0.0", + "cacache": "^20.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^13.0.0", + "npm-packlist": "^10.0.1", + "npm-pick-manifest": "^11.0.1", + "npm-registry-fetch": "^19.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "sigstore": "^4.0.0", + "ssri": "^12.0.0", + "tar": "^7.4.3" + }, + "bin": { + "pacote": "bin/index.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/parse-conflict-json": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^4.0.0", + "just-diff": "^6.0.0", + "just-diff-apply": "^5.2.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/path-scurry": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/postcss-selector-parser": { + "version": "7.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/proc-log": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/proggy": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/promise-all-reject-late": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/promise-call-limit": { + "version": "3.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/promise-retry": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/promzard": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "read": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/qrcode-terminal": { + "version": "0.12.0", + "dev": true, + "inBundle": true, + "bin": { + "qrcode-terminal": "bin/qrcode-terminal.js" + } + }, + "node_modules/npm/node_modules/read": { + "version": "4.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "mute-stream": "^2.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/read-cmd-shim": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/retry": { + "version": "0.12.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/npm/node_modules/safer-buffer": { + "version": "2.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true + }, + "node_modules/npm/node_modules/semver": { + "version": "7.7.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/signal-exit": { + "version": "4.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/sigstore": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.0.0", + "@sigstore/protobuf-specs": "^0.5.0", + "@sigstore/sign": "^4.0.0", + "@sigstore/tuf": "^4.0.0", + "@sigstore/verify": "^3.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/smart-buffer": { + "version": "4.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/npm/node_modules/socks": { + "version": "2.8.7", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/npm/node_modules/socks-proxy-agent": { + "version": "8.0.5", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/npm/node_modules/spdx-correct": { + "version": "3.2.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/spdx-correct/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/spdx-exceptions": { + "version": "2.5.0", + "dev": true, + "inBundle": true, + "license": "CC-BY-3.0" + }, + "node_modules/npm/node_modules/spdx-expression-parse": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/spdx-license-ids": { + "version": "3.0.22", + "dev": true, + "inBundle": true, + "license": "CC0-1.0" + }, + "node_modules/npm/node_modules/ssri": { + "version": "12.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/string-width": { + "version": "4.2.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/supports-color": { + "version": "10.2.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/npm/node_modules/tar": { + "version": "7.5.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/npm/node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/npm/node_modules/text-table": { + "version": "0.2.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/tiny-relative-date": { + "version": "2.0.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/tinyglobby": { + "version": "0.2.15", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/npm/node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/npm/node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/npm/node_modules/treeverse": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/tuf-js": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "@tufjs/models": "4.0.0", + "debug": "^4.4.1", + "make-fetch-happen": "^15.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/unique-filename": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/unique-slug": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/util-deprecate": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/validate-npm-package-license": { + "version": "3.0.4", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/npm/node_modules/validate-npm-package-license/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/validate-npm-package-name": { + "version": "6.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/walk-up-path": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/npm/node_modules/which": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/wrap-ansi": { + "version": "8.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/istanbul-lib-source-maps": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", - "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "node_modules/npm/node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", "dev": true, + "inBundle": true, + "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.23", - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "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==", + "node_modules/npm/node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", "dev": true, + "inBundle": true, + "license": "MIT", "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" + "color-convert": "^2.0.1" }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "node_modules/npm/node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.2.2", "dev": true, - "dependencies": { - "@isaacs/cliui": "^8.0.2" + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/loupe": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", - "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", + "node_modules/npm/node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "9.2.2", "dev": true, + "inBundle": true, "license": "MIT" }, - "node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "node_modules/npm/node_modules/wrap-ansi/node_modules/string-width": { + "version": "5.1.2", "dev": true, + "inBundle": true, "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" - } - }, - "node_modules/magicast": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", - "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.25.4", - "@babel/types": "^7.25.4", - "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" + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" }, "engines": { - "node": ">=10" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/minipass": { + "node_modules/npm/node_modules/wrap-ansi/node_modules/strip-ansi": { "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, - "node_modules/nanoid": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", - "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "node_modules/npm/node_modules/write-file-atomic": { + "version": "6.0.0", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" + "inBundle": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/npm/node_modules/yallist": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, "node_modules/on-exit-leak-free": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", @@ -2055,6 +5414,23 @@ "node": ">=14.0.0" } }, + "node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "license": "MIT", + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -2116,6 +5492,19 @@ "dev": true, "license": "ISC" }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/pino": { "version": "8.21.0", "resolved": "https://registry.npmjs.org/pino/-/pino-8.21.0.tgz", @@ -2180,6 +5569,14 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/prettier": { "version": "2.8.8", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", @@ -2195,6 +5592,34 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/pretty-format": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", + "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -2213,6 +5638,13 @@ "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" }, + "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, + "license": "MIT" + }, "node_modules/readable-stream": { "version": "4.5.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", @@ -2341,6 +5773,16 @@ "dev": true, "license": "ISC" }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/sonic-boom": { "version": "3.8.1", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.8.1.tgz", @@ -2349,6 +5791,16 @@ "atomic-sleep": "^1.0.0" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2366,6 +5818,19 @@ "node": ">= 10.x" } }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -2373,6 +5838,15 @@ "dev": true, "license": "MIT" }, + "node_modules/static-eval": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.tgz", + "integrity": "sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg==", + "license": "MIT", + "dependencies": { + "escodegen": "^1.8.1" + } + }, "node_modules/std-env": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.0.tgz", @@ -2468,50 +5942,6 @@ "node": ">=18" } }, - "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/test-exclude/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/thread-stream": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.7.0.tgz", @@ -2534,6 +5964,23 @@ "dev": true, "license": "MIT" }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, "node_modules/tinypool": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", @@ -2573,6 +6020,31 @@ "node": ">=4" } }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "license": "MIT", + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/typescript": { "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", @@ -2586,6 +6058,12 @@ "node": ">=4.2.0" } }, + "node_modules/underscore": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", + "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==", + "license": "MIT" + }, "node_modules/undici": { "version": "6.21.2", "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.2.tgz", @@ -2838,6 +6316,15 @@ "node": ">=8" } }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrap-ansi-cjs": { "name": "wrap-ansi", "version": "7.0.0", diff --git a/package.json b/package.json index 8f8296e..dff70e4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "flagsmith-nodejs", - "version": "6.2.0", + "version": "7.0.0", "description": "Flagsmith lets you manage features flags and remote config across web, mobile and server side applications. Deliver true Continuous Integration. Get builds out faster. Control who has access to new features.", "main": "./build/cjs/index.js", "type": "module", @@ -57,20 +57,30 @@ "build": "tsc -b tsconfig.cjs.json tsconfig.esm.json && echo '{\"type\": \"commonjs\"}'> build/cjs/package.json", "deploy": "npm i && npm run build && npm publish", "deploy:beta": "npm i && npm run build && npm publish --tag beta", - "prepare": "husky install" + "prepare": "husky install", + "generate-evaluation-result-types": "curl -o evaluation-result.json https://raw.githubusercontent.com/Flagsmith/flagsmith/main/sdk/evaluation-result.json && npx json2ts -i evaluation-result.json -o flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.ts && rm evaluation-result.json", + "generate-evaluation-context-types": "curl -o evaluation-context.json https://raw.githubusercontent.com/Flagsmith/flagsmith/main/sdk/evaluation-context.json && npx json2ts -i evaluation-context.json -o flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts && rm evaluation-context.json", + "generate-engine-types": "npm run generate-evaluation-result-types && npm run generate-evaluation-context-types" }, "dependencies": { + "jsonpath": "^1.1.1", "pino": "^8.8.0", "semver": "^7.3.7", "undici-types": "^6.19.8" }, "devDependencies": { + "@types/jest": "^30.0.0", + "@types/jsonpath": "^0.2.4", "@types/node": "^20.16.10", "@types/semver": "^7.3.9", "@types/uuid": "^8.3.4", "@vitest/coverage-v8": "^2.1.2", "esbuild": "^0.25.0", "husky": "^7.0.4", + "install": "^0.13.0", + "json-schema-to-typescript": "^15.0.4", + "jsonc-parser": "^3.3.1", + "npm": "^11.6.1", "prettier": "^2.2.1", "typescript": "^4.9.5", "undici": "^6.19.8", diff --git a/sdk/index.ts b/sdk/index.ts index 7438b99..81cbde7 100644 --- a/sdk/index.ts +++ b/sdk/index.ts @@ -1,22 +1,21 @@ import { Dispatcher } from 'undici-types'; -import { - getEnvironmentFeatureStates, - getIdentityFeatureStates -} from '../flagsmith-engine/index.js'; -import { EnvironmentModel } from '../flagsmith-engine/index.js'; + import { buildEnvironmentModel } from '../flagsmith-engine/environments/util.js'; -import { IdentityModel } from '../flagsmith-engine/index.js'; -import { TraitModel } from '../flagsmith-engine/index.js'; import { ANALYTICS_ENDPOINT, AnalyticsProcessor } from './analytics.js'; import { BaseOfflineHandler } from './offline_handlers.js'; -import { FlagsmithAPIError } from './errors.js'; +import { FlagsmithAPIError, FlagsmithClientError } from './errors.js'; import { DefaultFlag, Flags } from './models.js'; import { EnvironmentDataPollingManager } from './polling_manager.js'; import { Deferred, generateIdentitiesData, getUserAgent, retryFetch } from './utils.js'; -import { SegmentModel } from '../flagsmith-engine/index.js'; -import { getIdentitySegments } from '../flagsmith-engine/segments/evaluators.js'; +import { + SegmentModel, + EnvironmentModel, + IdentityModel, + TraitModel, + getEvaluationResult +} from '../flagsmith-engine/index.js'; import { Fetch, FlagsmithCache, @@ -25,6 +24,7 @@ import { TraitConfig } from './types.js'; import { pino, Logger } from 'pino'; +import { getEvaluationContext } from '../flagsmith-engine/evaluation/evaluationContext/mappers.js'; export { AnalyticsProcessor, AnalyticsProcessorOptions } from './analytics.js'; export { FlagsmithAPIError, FlagsmithClientError } from './errors.js'; @@ -278,7 +278,13 @@ export class Flagsmith { })) ); - return getIdentitySegments(environment, identityModel); + const context = getEvaluationContext(environment, identityModel); + if (!context) { + throw new FlagsmithClientError('Local evaluation required to obtain identity segments'); + } + const evaluationResult = getEvaluationResult(context); + + return SegmentModel.fromSegmentResult(evaluationResult.segments, context); } private async fetchEnvironment(): Promise { @@ -443,14 +449,17 @@ export class Flagsmith { private async getEnvironmentFlagsFromDocument(): Promise { const environment = await this.getEnvironment(); - const flags = Flags.fromFeatureStateModels({ - featureStates: getEnvironmentFeatureStates(environment), - analyticsProcessor: this.analyticsProcessor, - defaultFlagHandler: this.defaultFlagHandler - }); + const context = getEvaluationContext(environment); + if (!context) { + throw new FlagsmithClientError('Unable to get flags. No environment present.'); + } + const evaluationResult = getEvaluationResult(context); + const flags = Flags.fromEvaluationResult(evaluationResult); + if (!!this.cache) { await this.cache.set('flags', flags); } + return flags; } @@ -468,14 +477,17 @@ export class Flagsmith { })) ); - const featureStates = getIdentityFeatureStates(environment, identityModel); + const context = getEvaluationContext(environment, identityModel); + if (!context) { + throw new FlagsmithClientError('Unable to get flags. No environment present.'); + } + const evaluationResult = getEvaluationResult(context); - const flags = Flags.fromFeatureStateModels({ - featureStates: featureStates, - analyticsProcessor: this.analyticsProcessor, - defaultFlagHandler: this.defaultFlagHandler, - identityID: identityModel.djangoID || identityModel.compositeKey - }); + const flags = Flags.fromEvaluationResult( + evaluationResult, + this.defaultFlagHandler, + this.analyticsProcessor + ); if (!!this.cache) { await this.cache.set(`flags-${identifier}`, flags); diff --git a/sdk/models.ts b/sdk/models.ts index 90cffae..f6c48a1 100644 --- a/sdk/models.ts +++ b/sdk/models.ts @@ -1,7 +1,12 @@ +import { + CustomFeatureMetadata, + FlagResultWithMetadata, + EvaluationResultWithMetadata +} from '../flagsmith-engine/evaluation/models.js'; import { FeatureStateModel } from '../flagsmith-engine/features/models.js'; import { AnalyticsProcessor } from './analytics.js'; -type FlagValue = string | number | boolean | undefined; +type FlagValue = string | number | boolean | undefined | null; /** * A Flagsmith feature. It has an enabled/disabled state, and an optional {@link FlagValue}. @@ -49,6 +54,10 @@ export class Flag extends BaseFlag { * The programmatic name for this feature, unique per Flagsmith project. */ featureName: string; + /** + * The reason for this feature, unique per Flagsmith project. + */ + reason?: string; constructor(params: { value: FlagValue; @@ -56,10 +65,12 @@ export class Flag extends BaseFlag { isDefault?: boolean; featureId: number; featureName: string; + reason?: string; }) { super(params.value, params.enabled, !!params.isDefault); this.featureId = params.featureId; this.featureName = params.featureName; + this.reason = params.reason; } static fromFeatureStateModel( @@ -79,7 +90,8 @@ export class Flag extends BaseFlag { enabled: flagData['enabled'], value: flagData['feature_state_value'] ?? flagData['value'], featureId: flagData['feature']['id'], - featureName: flagData['feature']['name'] + featureName: flagData['feature']['name'], + reason: flagData['feature']['reason'] }); } } @@ -99,6 +111,33 @@ export class Flags { this.analyticsProcessor = data.analyticsProcessor; } + static fromEvaluationResult( + evaluationResult: EvaluationResultWithMetadata, + defaultFlagHandler?: (v: string) => DefaultFlag, + analyticsProcessor?: AnalyticsProcessor + ): Flags { + const flags: { [key: string]: Flag } = {}; + for (const flag of Object.values(evaluationResult.flags)) { + const flagsmithId = flag.metadata?.flagsmithId; + if (!flagsmithId) { + continue; + } + + flags[flag.name] = new Flag({ + enabled: flag.enabled, + value: flag.value ?? null, + featureId: flagsmithId, + featureName: flag.name, + reason: flag.reason + }); + } + return new Flags({ + flags: flags, + defaultFlagHandler: defaultFlagHandler, + analyticsProcessor: analyticsProcessor + }); + } + static fromFeatureStateModels(data: { featureStates: FeatureStateModel[]; analyticsProcessor?: AnalyticsProcessor; diff --git a/tests/engine/e2e/engine.test.ts b/tests/engine/e2e/engine.test.ts index 87d045f..a2a00b1 100644 --- a/tests/engine/e2e/engine.test.ts +++ b/tests/engine/e2e/engine.test.ts @@ -1,46 +1,51 @@ -import { getIdentityFeatureStates } from '../../../flagsmith-engine/index.js'; -import { EnvironmentModel } from '../../../flagsmith-engine/environments/models.js'; -import { buildEnvironmentModel } from '../../../flagsmith-engine/environments/util.js'; -import { IdentityModel } from '../../../flagsmith-engine/identities/models.js'; -import { buildIdentityModel } from '../../../flagsmith-engine/identities/util.js'; -import * as testData from '../engine-tests/engine-test-data/data/environment_n9fbf9h3v4fFgH3U3ngWhb.json'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { getEvaluationResult } from '../../../flagsmith-engine/index.js'; +import { Flags } from '../../../sdk/models.js'; +import { EvaluationContext } from '../../../flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.js'; +import { parse as parseJsonc } from 'jsonc-parser'; +import { + EvaluationContextWithMetadata, + EvaluationResult +} from '../../../flagsmith-engine/evaluation/models.js'; -function extractTestCases(data: any): { - environment: EnvironmentModel; - identity: IdentityModel; - response: any; -}[] { - const environmentModel = buildEnvironmentModel(data['environment']); - const test_data = data['identities_and_responses'].map((test_case: any) => { - const identity = buildIdentityModel(test_case['identity']); +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const TEST_DATA_DIR = path.join(__dirname, '../engine-tests/engine-test-data/test_cases'); +interface TestCase { + context: EvaluationContext; + result: EvaluationResult; +} - return { - environment: environmentModel, - identity: identity, - response: test_case['response'] - }; - }); - return test_data; +function getTestFiles(): string[] { + const files = fs.readdirSync(TEST_DATA_DIR); + return files + .filter(f => f.endsWith('.json') || f.endsWith('.jsonc')) + .map(f => path.join(TEST_DATA_DIR, f)); } -test('Test Engine', () => { - const testCases = extractTestCases(testData); - for (const testCase of testCases) { - const engine_response = getIdentityFeatureStates(testCase.environment, testCase.identity); - const sortedEngineFlags = engine_response.sort((a, b) => - a.feature.name > b.feature.name ? 1 : -1 - ); - const sortedAPIFlags = testCase.response['flags'].sort((a: any, b: any) => - a.feature.name > b.feature.name ? 1 : -1 - ); +function loadTestFile(filePath: string): TestCase { + const content = fs.readFileSync(filePath, 'utf-8'); + return parseJsonc(content); +} - expect(sortedEngineFlags.length).toBe(sortedAPIFlags.length); +describe('Engine Integration Tests', () => { + const testFiles = getTestFiles(); - for (let i = 0; i < sortedEngineFlags.length; i++) { - expect(sortedEngineFlags[i].getValue(testCase.identity.djangoID)).toBe( - sortedAPIFlags[i]['feature_state_value'] - ); - expect(sortedEngineFlags[i].enabled).toBe(sortedAPIFlags[i]['enabled']); - } + if (testFiles.length === 0) { + throw new Error(`No test files found in ${TEST_DATA_DIR}`); } + + testFiles.forEach(filePath => { + const testName = path.basename(filePath, path.extname(filePath)); + + test(testName, () => { + const testCase = loadTestFile(filePath); + const engine_response = getEvaluationResult( + testCase.context as EvaluationContextWithMetadata + ); + expect(engine_response).toStrictEqual(testCase.result); + }); + }); }); diff --git a/tests/engine/engine-tests/engine-test-data b/tests/engine/engine-tests/engine-test-data index 95a077f..41c2021 160000 --- a/tests/engine/engine-tests/engine-test-data +++ b/tests/engine/engine-tests/engine-test-data @@ -1 +1 @@ -Subproject commit 95a077f8c260b730b20d084b9a67d426f2ecade5 +Subproject commit 41c202145e375c712600e318c439456de5b221d7 diff --git a/tests/engine/unit/engine.test.ts b/tests/engine/unit/engine.test.ts index 9dea3a9..8eb7d5d 100644 --- a/tests/engine/unit/engine.test.ts +++ b/tests/engine/unit/engine.test.ts @@ -1,8 +1,10 @@ import { - getEnvironmentFeatureState, - getEnvironmentFeatureStates, - getIdentityFeatureState, - getIdentityFeatureStates + evaluateFeatures, + evaluateSegments, + getEvaluationResult, + isHigherPriority, + SegmentOverrides, + shouldApplyOverride } from '../../../flagsmith-engine/index.js'; import { CONSTANTS } from '../../../flagsmith-engine/features/constants.js'; import { FeatureModel, FeatureStateModel } from '../../../flagsmith-engine/features/models.js'; @@ -11,101 +13,355 @@ import { environment, environmentWithSegmentOverride, feature1, - getEnvironmentFeatureStateForFeature, identity, identityInSegment, segmentConditionProperty, segmentConditionStringValue } from './utils.js'; +import { getEvaluationContext } from '../../../flagsmith-engine/evaluation/evaluationContext/mappers.js'; +import { TARGETING_REASONS } from '../../../flagsmith-engine/features/types.js'; +import { EvaluationContext } from '../../../flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.js'; +import { IDENTITY_OVERRIDE_SEGMENT_NAME } from '../../../flagsmith-engine/segments/constants.js'; -test('test_identity_get_feature_state_without_any_override', () => { - const feature_state = getIdentityFeatureState(environment(), identity(), feature1().name); +test('test_get_evaluation_result_without_any_override', () => { + const context = getEvaluationContext(environment(), identity()); + const result = getEvaluationResult(context); - expect(feature_state.feature).toStrictEqual(feature1()); + const flag = Object.values(result.flags).find(f => f.name === feature1().name); + expect(flag).toBeDefined(); + expect(flag?.name).toBe(feature1().name); + expect(flag?.feature_key).toBe(feature1().id.toString()); + expect(flag?.reason).toBe(TARGETING_REASONS.DEFAULT); }); -test('test_identity_get_feature_state_without_any_override_no_fs', () => { - expect(() => { - getIdentityFeatureState(environment(), identity(), 'nonExistentName'); - }).toThrowError('Feature State Not Found'); -}); - -test('test_identity_get_all_feature_states_no_segments', () => { +test('test_get_evaluation_result_with_identity_override_and_no_segment_override', () => { const env = environment(); const ident = identity(); const overridden_feature = new FeatureModel(3, 'overridden_feature', CONSTANTS.STANDARD); env.featureStates.push(new FeatureStateModel(overridden_feature, false, 3)); - ident.identityFeatures = [new FeatureStateModel(overridden_feature, true, 4)]; + env.identityOverrides = [ident]; + + const context = getEvaluationContext(env, ident); + const result = getEvaluationResult(context); - const featureStates = getIdentityFeatureStates(env, ident); + expect(Object.keys(result.flags).length).toBe(3); - expect(featureStates.length).toBe(3); - for (const featuresState of featureStates) { - const environmentFeatureState = getEnvironmentFeatureStateForFeature( - env, - featuresState.feature + for (const flag of Object.values(result.flags)) { + const environmentFeature = Object.values(context.features || {}).find( + f => f.name === flag.name + ); + + const expected = flag.name === 'overridden_feature' ? true : environmentFeature?.enabled; + + expect(flag.enabled).toBe(expected); + expect(flag.reason).toBe( + flag.name === 'overridden_feature' + ? `${TARGETING_REASONS.TARGETING_MATCH}; segment=${IDENTITY_OVERRIDE_SEGMENT_NAME}` + : TARGETING_REASONS.DEFAULT ); - const expected = - environmentFeatureState?.feature == overridden_feature - ? true - : environmentFeatureState?.enabled; - expect(featuresState.enabled).toBe(expected); } }); test('test_identity_get_all_feature_states_with_traits', () => { const trait_models = new TraitModel(segmentConditionProperty, segmentConditionStringValue); - const featureStates = getIdentityFeatureStates( - environmentWithSegmentOverride(), - identityInSegment(), - [trait_models] + const context = getEvaluationContext(environmentWithSegmentOverride(), identityInSegment(), [ + trait_models + ]); + + const result = getEvaluationResult(context); + + const overriddenFlag = Object.values(result.flags).find(f => f.value === 'segment_override'); + expect(overriddenFlag).toBeDefined(); + expect(overriddenFlag?.value).toBe('segment_override'); + expect(overriddenFlag?.reason).toEqual( + `${TARGETING_REASONS.TARGETING_MATCH}; segment=test name` ); - expect(featureStates[0].getValue()).toBe('segment_override'); }); -test('test_identity_get_all_feature_states_with_traits_hideDisabledFlags', () => { - const trait_models = new TraitModel(segmentConditionProperty, segmentConditionStringValue); +test('test_environment_get_all_feature_states', () => { + const env = environment(); + const context = getEvaluationContext(env); + const result = getEvaluationResult(context); - const env = environmentWithSegmentOverride(); - env.project.hideDisabledFlags = true; + expect(Object.keys(result.flags).length).toBe(Object.keys(context.features || {}).length); - const featureStates = getIdentityFeatureStates(env, identityInSegment(), [trait_models]); - expect(featureStates.length).toBe(0); + Object.values(result.flags).forEach(flag => { + expect(flag.reason).toBe(TARGETING_REASONS.DEFAULT); + }); + + for (const flag of Object.values(result.flags)) { + const envFeature = Object.values(context.features || {}).find(f => f.name === flag.name); + expect(flag.enabled).toBe(envFeature?.enabled); + expect(flag.value).toBe(envFeature?.value); + } }); -test('test_environment_get_all_feature_states', () => { - const env = environment(); - const featureStates = getEnvironmentFeatureStates(env); +test('isHigherPriority should handle undefined priorities correctly', () => { + expect(isHigherPriority(1, 2)).toBe(true); + expect(isHigherPriority(2, 1)).toBe(false); + expect(isHigherPriority(undefined, 5)).toBe(false); + expect(isHigherPriority(5, undefined)).toBe(true); + expect(isHigherPriority(undefined, undefined)).toBe(false); +}); + +test('shouldApplyOverride with priority conflicts', () => { + const existingOverrides: SegmentOverrides = { + feature1: { + feature: { + key: 'key', + feature_key: 'feature1', + name: 'name', + enabled: true, + value: 'value', + priority: 5 + }, + segmentName: 'segment1' + } + }; - expect(featureStates).toBe(env.featureStates); + expect(shouldApplyOverride({ feature_key: 'feature1', priority: 2 }, existingOverrides)).toBe( + true + ); + expect(shouldApplyOverride({ feature_key: 'feature1', priority: 10 }, existingOverrides)).toBe( + false + ); }); -test('test_environment_get_feature_states_hides_disabled_flags_if_enabled', () => { - const env = environment(); +test('evaluateSegments handles segments with identity identifier matching', () => { + const context: EvaluationContext = { + environment: { + key: 'test-env', + name: 'Test Environment' + }, + identity: { + key: 'test-user', + identifier: 'test-user' + }, + segments: { + '1': { + key: '1', + name: 'segment_with_no_overrides', + rules: [ + { + type: 'ALL', + conditions: [ + { + property: '$.identity.identifier', + operator: 'EQUAL', + value: 'test-user' + } + ] + } + ], + overrides: [] + }, + '2': { + key: '2', + name: 'segment_with_overrides', + rules: [ + { + type: 'ALL', + conditions: [ + { + property: '$.identity.identifier', + operator: 'EQUAL', + value: 'test-user' + } + ] + } + ], + overrides: [ + { + key: 'override1', + feature_key: 'feature1', + name: 'feature1', + enabled: true, + value: 'overridden_value', + priority: 1 + } + ] + } + }, + features: { + feature1: { + key: 'fs1', + feature_key: 'feature1', + name: 'feature1', + enabled: false, + value: 'default_value' + } + } + }; - env.project.hideDisabledFlags = true; + const result = evaluateSegments(context); - const featureStates = getEnvironmentFeatureStates(env); + expect(result.segments).toHaveLength(2); + expect(result.segments).toEqual( + expect.arrayContaining([ + { key: '1', name: 'segment_with_no_overrides' }, + { key: '2', name: 'segment_with_overrides' } + ]) + ); - expect(featureStates).not.toBe(env.featureStates); - for (const fs of featureStates) { - expect(fs.enabled).toBe(true); - } + expect(Object.keys(result.segmentOverrides)).toEqual(['feature1']); + expect(result.segmentOverrides.feature1.segmentName).toBe('segment_with_overrides'); }); -test('test_environment_get_feature_state', () => { - const env = environment(); - const feature = feature1(); - const featureState = getEnvironmentFeatureState(env, feature.name); +test('evaluateSegments handles priority conflicts correctly', () => { + const context: EvaluationContext = { + environment: { + key: 'test-env', + name: 'Test Environment' + }, + identity: { + key: 'test-user', + identifier: 'test-user' + }, + segments: { + '1': { + key: '1', + name: 'low_priority_segment', + rules: [ + { + type: 'ALL', + conditions: [ + { + property: '$.identity.identifier', + operator: 'EQUAL', + value: 'test-user' + } + ] + } + ], + overrides: [ + { + key: 'override1', + feature_key: 'feature1', + name: 'feature1', + enabled: true, + value: 'low_priority_value', + priority: 10 + } + ] + }, + '2': { + key: '2', + name: 'high_priority_segment', + rules: [ + { + type: 'ALL', + conditions: [ + { + property: '$.identity.identifier', + operator: 'EQUAL', + value: 'test-user' + } + ] + } + ], + overrides: [ + { + key: 'override2', + feature_key: 'feature1', + name: 'feature1', + enabled: false, + value: 'high_priority_value', + priority: 1 + } + ] + } + }, + features: { + feature1: { + key: 'fs1', + feature_key: 'feature1', + name: 'feature1', + enabled: false, + value: 'default_value' + } + } + }; - expect(featureState.feature).toStrictEqual(feature); + const result = evaluateSegments(context); + + expect(result.segments).toHaveLength(2); + + expect(result.segmentOverrides.feature1.segmentName).toBe('high_priority_segment'); + expect(result.segmentOverrides.feature1.feature.value).toBe('high_priority_value'); + expect(result.segmentOverrides.feature1.feature.priority).toBe(1); }); -test('test_environment_get_feature_state_raises_feature_state_not_found', () => { - expect(() => { - getEnvironmentFeatureState(environment(), 'not_a_feature_name'); - }).toThrowError('Feature State Not Found'); +test('evaluateSegments with non-matching identity returns empty', () => { + const context: EvaluationContext = { + environment: { + key: 'test-env', + name: 'Test Environment' + }, + identity: { + key: 'test-user', + identifier: 'test-user' + }, + segments: { + '1': { + key: '1', + name: 'segment_for_specific_user', + rules: [ + { + type: 'ALL', + conditions: [ + { + property: '$.identity.identifier', + operator: 'EQUAL', + value: 'test-user-123' + } + ] + } + ], + overrides: [ + { + key: 'override1', + feature_key: 'feature1', + name: 'feature1', + enabled: true, + value: 'overridden_value' + } + ] + } + }, + features: {} + }; + + const result = evaluateSegments(context); + + expect(result.segments).toEqual([]); + expect(result.segmentOverrides).toEqual({}); +}); + +test('evaluateFeatures with multivariate evaluation', () => { + const context = { + features: { + mv_feature: { + key: 'mv', + feature_key: 'mv_feature', + name: 'Multivariate Feature', + enabled: true, + value: 'default', + variants: [ + { value: 'variant_a', weight: 0 }, + { value: 'variant_b', weight: 100 } + ] + } + }, + identity: { key: 'test_user', identifier: 'test_user' }, + environment: { + key: 'test_env', + name: 'Test Environment' + } + }; + + const flags = evaluateFeatures(context, {}); + expect(flags['Multivariate Feature'].value).toBe('variant_b'); }); diff --git a/tests/engine/unit/segments/segment_evaluators.test.ts b/tests/engine/unit/segments/segment_evaluators.test.ts index 1a73eec..6d260c7 100644 --- a/tests/engine/unit/segments/segment_evaluators.test.ts +++ b/tests/engine/unit/segments/segment_evaluators.test.ts @@ -3,19 +3,27 @@ import { CONDITION_OPERATORS, PERCENTAGE_SPLIT } from '../../../../flagsmith-engine/segments/constants.js'; -import { SegmentConditionModel } from '../../../../flagsmith-engine/segments/models.js'; + import { traitsMatchSegmentCondition, - evaluateIdentityInSegment + getContextValue, + getIdentitySegments } from '../../../../flagsmith-engine/segments/evaluators.js'; import { TraitModel, IdentityModel } from '../../../../flagsmith-engine/index.js'; import { environment } from '../utils.js'; import { buildSegmentModel } from '../../../../flagsmith-engine/segments/util.js'; -import { getHashedPercentateForObjIds } from '../../../../flagsmith-engine/utils/hashing/index.js'; +import { getHashedPercentageForObjIds } from '../../../../flagsmith-engine/utils/hashing/index.js'; +import { getEvaluationContext } from '../../../../flagsmith-engine/evaluation/evaluationContext/mappers.js'; +import { + EvaluationContext, + InSegmentCondition, + SegmentCondition, + SegmentCondition1 +} from '../../../../flagsmith-engine/evaluation/models.js'; // todo: work out how to implement this in a test function or before hook vi.mock('../../../../flagsmith-engine/utils/hashing', () => ({ - getHashedPercentateForObjIds: vi.fn(() => 1) + getHashedPercentageForObjIds: vi.fn(() => 1) })); let traitExistenceTestCases: [ @@ -48,14 +56,33 @@ let traitExistenceTestCases: [ test('test_traits_match_segment_condition_for_trait_existence_operators', () => { for (const testCase of traitExistenceTestCases) { const [operator, conditionProperty, conditionValue, traits, expectedResult] = testCase; - let segmentModel = new SegmentConditionModel(operator, conditionValue, conditionProperty); - expect(traitsMatchSegmentCondition(traits, segmentModel, 'any', 'any')).toBe( - expectedResult - ); + let segmentConditionModel = { + operator, + value: conditionValue, + property: conditionProperty + }; + const traitsMap = traits.reduce((acc, trait) => { + acc[trait.traitKey] = trait.traitValue; + return acc; + }, {}); + const context: EvaluationContext = { + environment: { + key: 'any', + name: 'any' + }, + identity: { + traits: traitsMap, + key: 'any', + identifier: 'any' + } + }; + expect( + traitsMatchSegmentCondition(segmentConditionModel as SegmentCondition, 'any', context) + ).toBe(expectedResult); } }); -test('evaluateIdentityInSegment uses django ID for hashed percentage when present', () => { +test('getIdentitySegments uses django ID for hashed percentage when present', () => { var identityModel = new IdentityModel( Date.now().toString(), [], @@ -84,13 +111,376 @@ test('evaluateIdentityInSegment uses django ID for hashed percentage when presen feature_states: [] }; const segmentModel = buildSegmentModel(segmentDefinition); + const environmentModel = environment(); + environmentModel.project.segments = [segmentModel]; + const context = getEvaluationContext(environmentModel, identityModel); - var result = evaluateIdentityInSegment(identityModel, segmentModel); + var result = getIdentitySegments(context); - expect(result).toBe(true); - expect(getHashedPercentateForObjIds).toHaveBeenCalledTimes(1); - expect(getHashedPercentateForObjIds).toHaveBeenCalledWith([ - segmentModel.id, - identityModel.djangoID + expect(result).toHaveLength(1); + expect(getHashedPercentageForObjIds).toHaveBeenCalledTimes(1); + expect(getHashedPercentageForObjIds).toHaveBeenCalledWith([ + result[0].key, + context.identity!.key ]); }); + +describe('getIdentitySegments integration', () => { + test('returns only matching segments', () => { + const context: EvaluationContext = { + environment: { key: 'env', name: 'test' }, + identity: { + key: 'user', + identifier: 'premium@example.com', + traits: { subscription: 'premium' } + }, + segments: { + '1': { + key: '1', + name: 'premium_users', + rules: [ + { + type: 'ALL', + conditions: [ + { property: 'subscription', operator: 'EQUAL', value: 'premium' } + ] + } + ], + overrides: [] + }, + '2': { + key: '2', + name: 'basic_users', + rules: [ + { + type: 'ALL', + conditions: [ + { property: 'subscription', operator: 'EQUAL', value: 'basic' } + ] + } + ], + overrides: [] + } + }, + features: {} + }; + + const result = getIdentitySegments(context); + + expect(result).toHaveLength(1); + expect(result[0].name).toBe('premium_users'); + }); + + test('returns empty array when no segments match', () => { + const context: EvaluationContext = { + environment: { key: 'env', name: 'test' }, + identity: { + key: 'user', + identifier: 'test@example.com', + traits: { subscription: 'free' } + }, + segments: { + '1': { + key: '1', + name: 'premium_users', + rules: [ + { + type: 'ALL', + conditions: [ + { property: 'subscription', operator: 'EQUAL', value: 'premium' } + ] + } + ], + overrides: [] + } + }, + features: {} + }; + + const result = getIdentitySegments(context); + expect(result).toEqual([]); + }); +}); + +describe('IN operator', () => { + const mockContext: EvaluationContext = { + environment: { key: 'env', name: 'test' }, + identity: { + key: 'test-user', + identifier: 'test', + traits: { name: 'test' } + }, + segments: {}, + features: {} + }; + + test.each([ + // Array of strings + [ + { + property: '$.identity.identifier', + operator: CONDITION_OPERATORS.IN, + value: ['test', 'john-doe'] + }, + true + ], + [ + { + property: '$.identity.identifier', + operator: CONDITION_OPERATORS.IN, + value: ['john-doe'] + }, + false + ], + + // JSON encoded + [ + { + property: '$.identity.identifier', + operator: CONDITION_OPERATORS.IN, + value: '["test", "john-doe"]' + }, + true + ], + [ + { + property: '$.identity.identifier', + operator: CONDITION_OPERATORS.IN, + value: '["john-doe"]' + }, + false + ], + + // Legacy value string to split + [ + { + property: '$.identity.identifier', + operator: CONDITION_OPERATORS.IN, + value: 'test,john-doe' + }, + true + ], + [ + { + property: '$.identity.identifier', + operator: CONDITION_OPERATORS.IN, + value: 'john-doe' + }, + false + ], + // Fails because the value is split in middle + [ + { + property: '$.identity.identifier', + operator: CONDITION_OPERATORS.IN, + value: 'te,st,john-doe' + }, + false + ], + + // Edge cases + [{ property: '$.identity.identifier', operator: CONDITION_OPERATORS.IN, value: '' }, false], + [{ property: '$.identity.identifier', operator: CONDITION_OPERATORS.IN, value: [] }, false], + [ + { property: '$.identity.identifier', operator: CONDITION_OPERATORS.IN, value: '[]' }, + false + ] + ] as Array<[SegmentCondition | InSegmentCondition, boolean]>)( + 'evaluates IN condition %j to %s', + (condition: SegmentCondition | InSegmentCondition, expected: boolean) => { + const result = traitsMatchSegmentCondition(condition, 'segment', mockContext); + expect(result).toBe(expected); + } + ); +}); + +describe('getIdentitySegments single segment evaluation', () => { + const baseContext: EvaluationContext = { + environment: { key: 'env', name: 'test' }, + identity: { key: 'user', identifier: 'test@example.com', traits: { age: 25 } }, + segments: {}, + features: {} + }; + + test('returns empty array for segment with no rules', () => { + const context = { + ...baseContext, + segments: { + '1': { + key: '1', + name: 'empty_segment', + rules: [], + overrides: [] + } + } + }; + + expect(getIdentitySegments(context)).toEqual([]); + }); + + test('returns segment when all rules match', () => { + const context: EvaluationContext = { + ...baseContext, + segments: { + '1': { + key: '1', + name: 'matching_segment', + rules: [ + { + type: ALL_RULE, + conditions: [ + { + property: '$.identity.identifier', + operator: 'EQUAL', + value: 'test@example.com' + } + ], + rules: [] + }, + { + type: ALL_RULE, + conditions: [ + { + property: '$.identity.identifier', + operator: 'CONTAINS', + value: 'test@example.com' + } + ], + rules: [] + } + ], + overrides: [] + } + } + }; + + const result = getIdentitySegments(context); + expect(result).toHaveLength(1); + expect(result[0].name).toBe('matching_segment'); + }); + + test('returns empty array when any rule fails', () => { + const context: EvaluationContext = { + ...baseContext, + segments: { + '1': { + key: '1', + name: 'failing_segment', + rules: [ + { + type: ALL_RULE, + conditions: [ + { + property: '$.identity.identifier', + operator: 'EQUAL', + value: 'test@example.com' + } + ], + rules: [] + }, + { + type: ALL_RULE, + conditions: [{ property: 'age', operator: 'EQUAL', value: '30' }], + rules: [] + } + ], + overrides: [] + } + } + }; + + expect(getIdentitySegments(context)).toEqual([]); + }); +}); + +describe('getContextValue', () => { + const mockContext: EvaluationContext = { + environment: { + key: 'test-env-key', + name: 'Test Environment' + }, + identity: { + key: 'user-123', + identifier: 'user@example.com' + }, + segments: {}, + features: {} + }; + + // Success cases + test.each([ + ['$.identity.identifier', 'user@example.com'], + ['$.environment.name', 'Test Environment'], + ['$.environment.key', 'test-env-key'] + ])('returns correct value for path %s', (jsonPath, expected) => { + const result = getContextValue(jsonPath, mockContext); + expect(result).toBe(expected); + }); + + // Undefined or invalid cases + test.each([ + ['$.identity.traits.user_type', 'unsupported nested path'], + ['identity.identifier', 'missing $ prefix'], + ['$.invalid.path', 'completely invalid path'], + ['$.identity.nonexistent', 'valid structure but missing property'], + ['', 'empty string'], + ['$', 'just $ symbol'] + ])('returns undefined for %s (%s)', jsonPath => { + const result = getContextValue(jsonPath, mockContext); + expect(result).toBeUndefined(); + }); + + // Context error cases + test.each([ + [undefined, '$.identity.identifier', 'undefined context'], + [{ segments: {}, features: {} }, '$.identity.identifier', 'missing identity'], + [ + { identity: { key: 'test', identifier: 'test' }, segments: {}, features: {} }, + '$.environment.name', + 'missing environment' + ] + ])('returns undefined when %s', (context, jsonPath, _) => { + const result = getContextValue(jsonPath, context as EvaluationContext); + expect(result).toBeUndefined(); + }); +}); + +describe('percentage split operator', () => { + const mockContext: EvaluationContext = { + environment: { key: 'env', name: 'Test Env' }, + identity: { + key: 'user-123', + identifier: 'test@example.com', + traits: { + age: 25, + subscription: 'premium', + active: true + } + }, + segments: {}, + features: {} + }; + beforeEach(() => { + vi.clearAllMocks(); + }); + + test.each([ + [25.5, 30, true], + [25.5, 20, false], + [25.5, 25.5, true], + [0, 0, true], + [100, 99.9, false] + ])('percentage %d with threshold %d returns %s', (hashedValue, threshold, expected) => { + const mockHashFn = getHashedPercentageForObjIds; + mockHashFn.mockReturnValue(hashedValue); + const condition = { + property: 'any', + operator: 'PERCENTAGE_SPLIT', + value: threshold.toString() + } as SegmentCondition1 | InSegmentCondition; + const result = traitsMatchSegmentCondition(condition, 'seg1', mockContext); + + expect(result).toBe(expected); + expect(getHashedPercentageForObjIds).toHaveBeenCalledWith(['seg1', 'user-123']); + }); +}); diff --git a/tests/engine/unit/segments/segments_model.test.ts b/tests/engine/unit/segments/segments_model.test.ts index 17d9166..5607f03 100644 --- a/tests/engine/unit/segments/segments_model.test.ts +++ b/tests/engine/unit/segments/segments_model.test.ts @@ -1,3 +1,5 @@ +import { EvaluationContext } from '../../../../flagsmith-engine/evaluationContext/evaluationContext.types'; +import { CONSTANTS } from '../../../../flagsmith-engine/features/constants'; import { ALL_RULE, ANY_RULE, @@ -8,6 +10,7 @@ import { all, any, SegmentConditionModel, + SegmentModel, SegmentRuleModel } from '../../../../flagsmith-engine/segments/models'; @@ -135,3 +138,78 @@ test('test_segment_rule_matching_function', () => { expect(new SegmentRuleModel(testCase[0]).matchingFunction()).toBe(testCase[1]); } }); + +test('test_fromSegmentResult_with_multiple_variants', () => { + const segmentResults = [{ key: '1', name: 'test_segment' }]; + + const evaluationContext: EvaluationContext = { + identity: { + key: 'not_exist', + identifier: 'not_exist' + }, + environment: { + key: 'test', + name: 'test' + }, + features: {}, + segments: { + '1': { + key: '1', + name: 'test_segment', + rules: [ + { + type: 'ALL', + conditions: [ + { + property: '$.identity.identifier', + operator: 'EQUAL', + value: 'test-user' + } + ] + } + ], + overrides: [ + { + key: 'override', + feature_key: '1', + name: 'multivariate_feature', + enabled: true, + value: 'default_value', + priority: 1, + variants: [ + { id: 1, value: 'variant_a', weight: 30 }, + { id: 2, value: 'variant_b', weight: 70 } + ] + } + ] + } + } + }; + + const result = SegmentModel.fromSegmentResult(segmentResults, evaluationContext); + + expect(result).toHaveLength(1); + + const segment = result[0]; + expect(segment.name).toBe('test_segment'); + expect(segment.featureStates).toHaveLength(1); + + const featureState = segment.featureStates[0]; + expect(featureState.feature.name).toBe('multivariate_feature'); + expect(featureState.feature.type).toBe(CONSTANTS.MULTIVARIATE); + expect(featureState.enabled).toBe(true); + expect(featureState.getValue()).toBe('default_value'); + + // Test multivariate variants + expect(featureState.multivariateFeatureStateValues).toHaveLength(2); + + const variant1 = featureState.multivariateFeatureStateValues[0]; + expect(variant1.multivariateFeatureOption.value).toBe('variant_a'); + expect(variant1.percentageAllocation).toBe(30); + expect(variant1.id).toBe(1); + + const variant2 = featureState.multivariateFeatureStateValues[1]; + expect(variant2.multivariateFeatureOption.value).toBe('variant_b'); + expect(variant2.percentageAllocation).toBe(70); + expect(variant2.id).toBe(2); +}); diff --git a/tests/engine/unit/utils.ts b/tests/engine/unit/utils.ts index cdb73b2..4e89fca 100644 --- a/tests/engine/unit/utils.ts +++ b/tests/engine/unit/utils.ts @@ -20,7 +20,7 @@ export function segmentCondition() { } export function traitMatchingSegment() { - return new TraitModel(segmentCondition().property_ as string, segmentCondition().value); + return new TraitModel(segmentCondition().property as string, segmentCondition().value); } export function organisation() { diff --git a/tests/engine/unit/utils/utils.test.ts b/tests/engine/unit/utils/utils.test.ts index 041adfc..15a1a30 100644 --- a/tests/engine/unit/utils/utils.test.ts +++ b/tests/engine/unit/utils/utils.test.ts @@ -1,11 +1,11 @@ import { randomUUID as uuidv4 } from 'node:crypto'; -import { getHashedPercentateForObjIds } from '../../../../flagsmith-engine/utils/hashing/index.js'; +import { getHashedPercentageForObjIds } from '../../../../flagsmith-engine/utils/hashing/index.js'; describe('getHashedPercentageForObjIds', () => { it.each([[[12, 93]], [[uuidv4(), 99]], [[99, uuidv4()]], [[uuidv4(), uuidv4()]]])( 'returns x where 0 <= x < 100', (objIds: (string | number)[]) => { - let result = getHashedPercentateForObjIds(objIds); + let result = getHashedPercentageForObjIds(objIds); expect(result).toBeLessThan(100); expect(result).toBeGreaterThanOrEqual(0); } @@ -14,15 +14,15 @@ describe('getHashedPercentageForObjIds', () => { it.each([[[12, 93]], [[uuidv4(), 99]], [[99, uuidv4()]], [[uuidv4(), uuidv4()]]])( 'returns the same value each time', (objIds: (string | number)[]) => { - let resultOne = getHashedPercentateForObjIds(objIds); - let resultTwo = getHashedPercentateForObjIds(objIds); + let resultOne = getHashedPercentageForObjIds(objIds); + let resultTwo = getHashedPercentageForObjIds(objIds); expect(resultOne).toEqual(resultTwo); } ); it('is unique for different object ids', () => { - let resultOne = getHashedPercentateForObjIds([14, 106]); - let resultTwo = getHashedPercentateForObjIds([53, 200]); + let resultOne = getHashedPercentageForObjIds([14, 106]); + let resultTwo = getHashedPercentageForObjIds([53, 200]); expect(resultOne).not.toEqual(resultTwo); }); @@ -40,7 +40,7 @@ describe('getHashedPercentageForObjIds', () => { ); // When - let values = objectIdPairs.map(objIds => getHashedPercentateForObjIds(objIds)); + let values = objectIdPairs.map(objIds => getHashedPercentageForObjIds(objIds)); // Then for (let i = 0; i++; i < numTestBuckets) {