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..6d04604 --- /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 Priority = 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 Priority1 = 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?: Metadata1; + [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?: Priority1; + metadata?: Metadata; + [k: string]: unknown; +} +/** + * Represents a multivariate value for a feature flag. + */ +export interface FeatureValue { + value: Value3; + weight: Weight; + priority: Priority; + [k: string]: unknown; +} +/** + * Additional metadata associated with the feature. + */ +export interface Metadata { + [k: string]: unknown; +} +/** + * Additional metadata associated with the segment. + */ +export interface Metadata1 { + [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..7afa4bc --- /dev/null +++ b/flagsmith-engine/evaluation/evaluationContext/mappers.ts @@ -0,0 +1,190 @@ +import { + Features, + Segments, + Traits, + EvaluationContext, + 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[] +): EvaluationContext { + const environmentContext = mapEnvironmentModelToEvaluationContext(environment); + const identityContext = identity + ? mapIdentityModelToIdentityContext(identity, overrideTraits) + : undefined; + + const context = { + ...environmentContext, + ...(identityContext && { identity: identityContext }) + }; + + return context; +} + +function mapEnvironmentModelToEvaluationContext(environment: EnvironmentModel): EvaluationContext { + const environmentContext: EnvironmentContext = { + key: environment.apiKey, + name: environment.project.name + }; + + const features: Features = {}; + 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 + })); + + 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..7ff71b4 --- /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?: Metadata; + [k: string]: unknown; +} +/** + * Additional metadata associated with the feature. + */ +export interface Metadata { + [k: string]: unknown; +} +export interface SegmentResult { + key: Key; + name: Name1; + metadata?: Metadata1; + [k: string]: unknown; +} +/** + * Additional metadata associated with the segment. + */ +export interface Metadata1 { + [k: string]: unknown; +} diff --git a/flagsmith-engine/evaluation/models.ts b/flagsmith-engine/evaluation/models.ts new file mode 100644 index 0000000..b165d3c --- /dev/null +++ b/flagsmith-engine/evaluation/models.ts @@ -0,0 +1,78 @@ +// 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 { + EnvironmentContext, + IdentityContext, + SegmentContext, + SegmentRule, + SegmentCondition, + InSegmentCondition, + FeatureContext, + FeatureValue as ContextFeatureValue, + Traits, + Features, + Segments +} from './evaluationContext/evaluationContext.types.js'; + +import type { + EvaluationResult as EvaluationContextResult, + FlagResult as EvaluationContextResultFlagResult, + Metadata +} from './evaluationResult/evaluationResult.types.js'; + +export type EnvironmentKey = EnvironmentContext['key']; +export type EnvironmentName = EnvironmentContext['name']; + +export type IdentityIdentifier = IdentityContext['identifier']; +export type IdentityKey = IdentityContext['key']; + +export type SegmentKey = SegmentContext['key']; +export type SegmentName = SegmentContext['name']; +export type SegmentRuleType = SegmentRule['type']; +export type ConditionOperator = SegmentCondition['operator'] | InSegmentCondition['operator']; +export type ConditionProperty = SegmentCondition['property'] | InSegmentCondition['property']; +export type ConditionValue = SegmentCondition['value'] | InSegmentCondition['value']; + +export type FeatureKey = FeatureContext['feature_key']; +export type FeatureName = FeatureContext['name']; +export type FeatureEnabled = FeatureContext['enabled']; +export type FeatureValue = FeatureContext['value']; +export type FeaturePriority = FeatureContext['priority']; +export type FeatureVariants = FeatureContext['variants']; + +export type VariantValue = ContextFeatureValue['value']; +export type VariantWeight = ContextFeatureValue['weight']; + +export type TraitMap = Traits; +export type FeatureMap = Features; +export type SegmentMap = Segments; + +export type SegmentConditionOperator = SegmentCondition['operator']; + +export type EvaluationReason = EvaluationContextResultFlagResult['reason']; + +export type EvaluationResultSegments = EvaluationContextResult['segments']; +import type { FlagResult } from './evaluationResult/evaluationResult.types.js'; + +export type FlagResultWithMetadata = FlagResult & { + metadata?: T; +}; + +export type EvaluationResultFlags = Record< + string, + FlagResultWithMetadata +>; + +export type EvaluationResult = { + flags: EvaluationResultFlags; + segments: EvaluationResultSegments; +}; + +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..333dcad 100644 --- a/flagsmith-engine/index.ts +++ b/flagsmith-engine/index.ts @@ -1,102 +1,226 @@ -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 { EvaluationContext, FeatureContext, SegmentSource } from './evaluation/models.js'; import { getIdentitySegments } from './segments/evaluators.js'; -import { SegmentModel } from './segments/models.js'; -import { FeatureStateNotFound } from './utils/errors.js'; - +import { EvaluationResult, 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: FeatureContext; + 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: EvaluationContext): EvaluationResult { + 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: EvaluationContext): { + segments: EvaluationResult['segments']; + segmentOverrides: Record; +} { + if (!context.identity || !context.segments) { + return { segments: [], segmentOverrides: {} }; } + 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: EvaluationContext, + 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 }) + }; } - 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: FeatureContext, + 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: FeatureContext, + 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..44ebcf0 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 { + EvaluationContext, + 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: EvaluationContext): 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?: EvaluationContext ): 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?: EvaluationContext +): boolean { + const matchesConditions = evaluateConditions(rule, segmentKey, context); + const matchesSubRules = evaluateSubRules(rule, segmentKey, context); + + return matchesConditions && matchesSubRules; +} + +function evaluateConditions( + rule: SegmentRule, + segmentKey: string, + context?: EvaluationContext ): 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?: EvaluationContext ): 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?: EvaluationContext): 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?: EvaluationContext): 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 ffdb63f..933acba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,17 +9,24 @@ "version": "6.1.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 aa632b6..ede37a6 100644 --- a/package.json +++ b/package.json @@ -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..f34f7b0 100644 --- a/sdk/models.ts +++ b/sdk/models.ts @@ -1,7 +1,8 @@ +import { EvaluationResult, FlagResultWithMetadata } 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 +50,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 +61,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( @@ -74,12 +81,23 @@ export class Flag extends BaseFlag { }); } + static fromFlagResult(flagResult: FlagResultWithMetadata<{ flagsmithId?: number }>): Flag { + return new Flag({ + enabled: flagResult.enabled, + value: flagResult.value ?? null, + featureId: flagResult.metadata?.flagsmithId || Number(flagResult.feature_key), + featureName: flagResult.name, + reason: flagResult.reason + }); + } + static fromAPIFlag(flagData: any): Flag { return new Flag({ 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 +117,28 @@ export class Flags { this.analyticsProcessor = data.analyticsProcessor; } + static fromEvaluationResult( + evaluationResult: EvaluationResult, + defaultFlagHandler?: (v: string) => DefaultFlag, + analyticsProcessor?: AnalyticsProcessor + ): Flags { + const flags: { [key: string]: Flag } = {}; + for (const flag of Object.values(evaluationResult.flags)) { + flags[flag.name] = new Flag({ + enabled: flag.enabled, + value: flag.value ?? null, + featureId: Number(flag.feature_key), + 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..e2e1b22 100644 --- a/tests/engine/e2e/engine.test.ts +++ b/tests/engine/e2e/engine.test.ts @@ -1,46 +1,46 @@ -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 { 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); + 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) {