flagsmith-nodejs 7.0.3 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/pull_request.yaml +1 -1
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +21 -0
- package/build/cjs/flagsmith-engine/segments/evaluators.js +14 -8
- package/build/esm/flagsmith-engine/segments/evaluators.js +14 -8
- package/flagsmith-engine/segments/evaluators.ts +16 -10
- package/package.json +4 -4
- package/tests/engine/unit/segments/segment_evaluators.test.ts +71 -1
- package/.github/workflows/conventional-commit.yml +0 -29
|
@@ -1 +1 @@
|
|
|
1
|
-
{".":"
|
|
1
|
+
{".":"8.0.0"}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [8.0.0](https://github.com/Flagsmith/flagsmith-nodejs-client/compare/v7.0.3...v8.0.0) (2026-02-25)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### ⚠ BREAKING CHANGES
|
|
7
|
+
|
|
8
|
+
* remove node18 support and update pino ([#220](https://github.com/Flagsmith/flagsmith-nodejs-client/issues/220))
|
|
9
|
+
|
|
10
|
+
### Bug Fixes
|
|
11
|
+
|
|
12
|
+
* **CVE-2026-1615:** Replace jsonpath with jsonpath-plus to fix security vulnerability ([#247](https://github.com/Flagsmith/flagsmith-nodejs-client/issues/247)) ([56a285c](https://github.com/Flagsmith/flagsmith-nodejs-client/commit/56a285c610a51f1fe3f7d2cd561566d3aa1c6018))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Dependency Updates
|
|
16
|
+
|
|
17
|
+
* remove node18 support and update pino ([#220](https://github.com/Flagsmith/flagsmith-nodejs-client/issues/220)) ([a246c06](https://github.com/Flagsmith/flagsmith-nodejs-client/commit/a246c066b062d5897abead34c0f1b8ee1d687d20))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Other
|
|
21
|
+
|
|
22
|
+
* Remove amannn/action-semantic-pull-request workflow ([#243](https://github.com/Flagsmith/flagsmith-nodejs-client/issues/243)) ([980728a](https://github.com/Flagsmith/flagsmith-nodejs-client/commit/980728a380518e123e7ce8f6ede98842c915fcae))
|
|
23
|
+
|
|
3
24
|
## [7.0.3](https://github.com/Flagsmith/flagsmith-nodejs-client/compare/v7.0.2...v7.0.3) (2026-01-21)
|
|
4
25
|
|
|
5
26
|
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.getContextValue = exports.traitsMatchSegmentCondition = exports.getIdentitySegments = void 0;
|
|
4
|
-
const
|
|
4
|
+
const jsonpath_plus_1 = require("jsonpath-plus");
|
|
5
5
|
const index_js_1 = require("../utils/hashing/index.js");
|
|
6
6
|
const models_js_1 = require("./models.js");
|
|
7
7
|
const constants_js_1 = require("./constants.js");
|
|
8
|
-
// Handle ESM/CJS interop - jsonpath exports default in ESM
|
|
9
|
-
const jsonpath = jsonpathModule.default || jsonpathModule;
|
|
10
8
|
/**
|
|
11
9
|
* Returns all segments that the identity belongs to based on segment rules evaluation.
|
|
12
10
|
*
|
|
@@ -100,8 +98,20 @@ function evaluateRuleConditions(ruleType, conditionResults) {
|
|
|
100
98
|
return false;
|
|
101
99
|
}
|
|
102
100
|
}
|
|
101
|
+
const TRAITS_DOT_PATTERN = /^\$\.identity\.traits\.(.+)$/;
|
|
102
|
+
const TRAITS_BRACKET_PATTERN = /^\$\.identity\.traits\['(.+)'\]$/;
|
|
103
|
+
function extractTraitNameFromPath(property) {
|
|
104
|
+
return TRAITS_DOT_PATTERN.exec(property)?.[1] ?? TRAITS_BRACKET_PATTERN.exec(property)?.[1];
|
|
105
|
+
}
|
|
103
106
|
function getTraitValue(property, context) {
|
|
104
107
|
if (property.startsWith('$.')) {
|
|
108
|
+
// Look up $.identity.traits.X and $.identity.traits['X'] paths directly
|
|
109
|
+
// to avoid jsonpath-plus mis-parsing special characters (e.g. $, [, ]) in
|
|
110
|
+
// trait names that appear inside bracket-notation strings.
|
|
111
|
+
const traitName = extractTraitNameFromPath(property);
|
|
112
|
+
if (traitName !== undefined) {
|
|
113
|
+
return context?.identity?.traits?.[traitName];
|
|
114
|
+
}
|
|
105
115
|
const contextValue = getContextValue(property, context);
|
|
106
116
|
if (contextValue !== undefined && isPrimitive(contextValue)) {
|
|
107
117
|
return contextValue;
|
|
@@ -135,8 +145,7 @@ function getContextValue(jsonPath, context) {
|
|
|
135
145
|
if (!context || !jsonPath?.startsWith('$.'))
|
|
136
146
|
return undefined;
|
|
137
147
|
try {
|
|
138
|
-
const
|
|
139
|
-
const results = jsonpath.query(context, normalizedPath);
|
|
148
|
+
const results = (0, jsonpath_plus_1.JSONPath)({ path: jsonPath, json: context });
|
|
140
149
|
return results.length > 0 ? results[0] : undefined;
|
|
141
150
|
}
|
|
142
151
|
catch (error) {
|
|
@@ -144,6 +153,3 @@ function getContextValue(jsonPath, context) {
|
|
|
144
153
|
}
|
|
145
154
|
}
|
|
146
155
|
exports.getContextValue = getContextValue;
|
|
147
|
-
function normalizeJsonPath(jsonPath) {
|
|
148
|
-
return jsonPath.replace(/\.([^.\[\]]+)$/, "['$1']");
|
|
149
|
-
}
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { JSONPath } from 'jsonpath-plus';
|
|
2
2
|
import { getHashedPercentageForObjIds } from '../utils/hashing/index.js';
|
|
3
3
|
import { SegmentConditionModel } from './models.js';
|
|
4
4
|
import { IS_NOT_SET, IS_SET, PERCENTAGE_SPLIT } from './constants.js';
|
|
5
|
-
// Handle ESM/CJS interop - jsonpath exports default in ESM
|
|
6
|
-
const jsonpath = jsonpathModule.default || jsonpathModule;
|
|
7
5
|
/**
|
|
8
6
|
* Returns all segments that the identity belongs to based on segment rules evaluation.
|
|
9
7
|
*
|
|
@@ -95,8 +93,20 @@ function evaluateRuleConditions(ruleType, conditionResults) {
|
|
|
95
93
|
return false;
|
|
96
94
|
}
|
|
97
95
|
}
|
|
96
|
+
const TRAITS_DOT_PATTERN = /^\$\.identity\.traits\.(.+)$/;
|
|
97
|
+
const TRAITS_BRACKET_PATTERN = /^\$\.identity\.traits\['(.+)'\]$/;
|
|
98
|
+
function extractTraitNameFromPath(property) {
|
|
99
|
+
return TRAITS_DOT_PATTERN.exec(property)?.[1] ?? TRAITS_BRACKET_PATTERN.exec(property)?.[1];
|
|
100
|
+
}
|
|
98
101
|
function getTraitValue(property, context) {
|
|
99
102
|
if (property.startsWith('$.')) {
|
|
103
|
+
// Look up $.identity.traits.X and $.identity.traits['X'] paths directly
|
|
104
|
+
// to avoid jsonpath-plus mis-parsing special characters (e.g. $, [, ]) in
|
|
105
|
+
// trait names that appear inside bracket-notation strings.
|
|
106
|
+
const traitName = extractTraitNameFromPath(property);
|
|
107
|
+
if (traitName !== undefined) {
|
|
108
|
+
return context?.identity?.traits?.[traitName];
|
|
109
|
+
}
|
|
100
110
|
const contextValue = getContextValue(property, context);
|
|
101
111
|
if (contextValue !== undefined && isPrimitive(contextValue)) {
|
|
102
112
|
return contextValue;
|
|
@@ -130,14 +140,10 @@ export function getContextValue(jsonPath, context) {
|
|
|
130
140
|
if (!context || !jsonPath?.startsWith('$.'))
|
|
131
141
|
return undefined;
|
|
132
142
|
try {
|
|
133
|
-
const
|
|
134
|
-
const results = jsonpath.query(context, normalizedPath);
|
|
143
|
+
const results = JSONPath({ path: jsonPath, json: context });
|
|
135
144
|
return results.length > 0 ? results[0] : undefined;
|
|
136
145
|
}
|
|
137
146
|
catch (error) {
|
|
138
147
|
return undefined;
|
|
139
148
|
}
|
|
140
149
|
}
|
|
141
|
-
function normalizeJsonPath(jsonPath) {
|
|
142
|
-
return jsonPath.replace(/\.([^.\[\]]+)$/, "['$1']");
|
|
143
|
-
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { JSONPath } from 'jsonpath-plus';
|
|
2
2
|
import {
|
|
3
3
|
GenericEvaluationContext,
|
|
4
4
|
InSegmentCondition,
|
|
@@ -10,9 +10,6 @@ import { getHashedPercentageForObjIds } from '../utils/hashing/index.js';
|
|
|
10
10
|
import { SegmentConditionModel } from './models.js';
|
|
11
11
|
import { IS_NOT_SET, IS_SET, PERCENTAGE_SPLIT } from './constants.js';
|
|
12
12
|
|
|
13
|
-
// Handle ESM/CJS interop - jsonpath exports default in ESM
|
|
14
|
-
const jsonpath = (jsonpathModule as any).default || jsonpathModule;
|
|
15
|
-
|
|
16
13
|
/**
|
|
17
14
|
* Returns all segments that the identity belongs to based on segment rules evaluation.
|
|
18
15
|
*
|
|
@@ -140,8 +137,22 @@ function evaluateRuleConditions(ruleType: string, conditionResults: boolean[]):
|
|
|
140
137
|
}
|
|
141
138
|
}
|
|
142
139
|
|
|
140
|
+
const TRAITS_DOT_PATTERN = /^\$\.identity\.traits\.(.+)$/;
|
|
141
|
+
const TRAITS_BRACKET_PATTERN = /^\$\.identity\.traits\['(.+)'\]$/;
|
|
142
|
+
|
|
143
|
+
function extractTraitNameFromPath(property: string): string | undefined {
|
|
144
|
+
return TRAITS_DOT_PATTERN.exec(property)?.[1] ?? TRAITS_BRACKET_PATTERN.exec(property)?.[1];
|
|
145
|
+
}
|
|
146
|
+
|
|
143
147
|
function getTraitValue(property: string, context?: GenericEvaluationContext): any {
|
|
144
148
|
if (property.startsWith('$.')) {
|
|
149
|
+
// Look up $.identity.traits.X and $.identity.traits['X'] paths directly
|
|
150
|
+
// to avoid jsonpath-plus mis-parsing special characters (e.g. $, [, ]) in
|
|
151
|
+
// trait names that appear inside bracket-notation strings.
|
|
152
|
+
const traitName = extractTraitNameFromPath(property);
|
|
153
|
+
if (traitName !== undefined) {
|
|
154
|
+
return context?.identity?.traits?.[traitName];
|
|
155
|
+
}
|
|
145
156
|
const contextValue = getContextValue(property, context);
|
|
146
157
|
if (contextValue !== undefined && isPrimitive(contextValue)) {
|
|
147
158
|
return contextValue;
|
|
@@ -179,14 +190,9 @@ export function getContextValue(jsonPath: string, context?: GenericEvaluationCon
|
|
|
179
190
|
if (!context || !jsonPath?.startsWith('$.')) return undefined;
|
|
180
191
|
|
|
181
192
|
try {
|
|
182
|
-
const
|
|
183
|
-
const results = jsonpath.query(context, normalizedPath);
|
|
193
|
+
const results = JSONPath({ path: jsonPath, json: context });
|
|
184
194
|
return results.length > 0 ? results[0] : undefined;
|
|
185
195
|
} catch (error) {
|
|
186
196
|
return undefined;
|
|
187
197
|
}
|
|
188
198
|
}
|
|
189
|
-
|
|
190
|
-
function normalizeJsonPath(jsonPath: string): string {
|
|
191
|
-
return jsonPath.replace(/\.([^.\[\]]+)$/, "['$1']");
|
|
192
|
-
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flagsmith-nodejs",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.0.0",
|
|
4
4
|
"description": "Flagsmith lets you manage features flags and remote config across web, mobile and server side applications. Deliver true Continuous Integration. Get builds out faster. Control who has access to new features.",
|
|
5
5
|
"main": "./build/cjs/index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|
|
8
|
-
"node": ">=
|
|
8
|
+
"node": ">=20"
|
|
9
9
|
},
|
|
10
10
|
"exports": {
|
|
11
11
|
"import": "./build/esm/index.js",
|
|
@@ -64,8 +64,8 @@
|
|
|
64
64
|
"generate-engine-types": "npm run generate-evaluation-result-types && npm run generate-evaluation-context-types"
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
|
-
"jsonpath": "^
|
|
68
|
-
"pino": "^
|
|
67
|
+
"jsonpath-plus": "^10.4.0",
|
|
68
|
+
"pino": "^10",
|
|
69
69
|
"semver": "^7.3.7",
|
|
70
70
|
"undici-types": "^6.19.8"
|
|
71
71
|
},
|
|
@@ -345,6 +345,75 @@ describe('getIdentitySegments single segment evaluation', () => {
|
|
|
345
345
|
});
|
|
346
346
|
});
|
|
347
347
|
|
|
348
|
+
describe('traitsMatchSegmentCondition with $.identity.traits.* properties', () => {
|
|
349
|
+
const mockContext: EvaluationContext = {
|
|
350
|
+
environment: { key: 'env', name: 'test' },
|
|
351
|
+
identity: {
|
|
352
|
+
key: 'user',
|
|
353
|
+
identifier: 'user@example.com',
|
|
354
|
+
traits: {
|
|
355
|
+
age: 25,
|
|
356
|
+
tamaño: 'grande',
|
|
357
|
+
サイズ: 'medium',
|
|
358
|
+
'[$the.size$]': 'small',
|
|
359
|
+
'my.foo.bar': 'dotted'
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
segments: {},
|
|
363
|
+
features: {}
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
test.each([
|
|
367
|
+
// dot notation – normal trait name
|
|
368
|
+
[{ property: '$.identity.traits.age', operator: 'EQUAL', value: '25' }, true],
|
|
369
|
+
[{ property: '$.identity.traits.age', operator: 'EQUAL', value: '30' }, false],
|
|
370
|
+
// dot notation – unicode trait name
|
|
371
|
+
[{ property: '$.identity.traits.tamaño', operator: 'EQUAL', value: 'grande' }, true],
|
|
372
|
+
[{ property: '$.identity.traits.サイズ', operator: 'EQUAL', value: 'medium' }, true],
|
|
373
|
+
// dot notation – trait name that itself contains dots (everything after $.identity.traits. is the key)
|
|
374
|
+
[{ property: '$.identity.traits.my.foo.bar', operator: 'EQUAL', value: 'dotted' }, true],
|
|
375
|
+
[{ property: '$.identity.traits.my.foo.bar', operator: 'EQUAL', value: 'other' }, false],
|
|
376
|
+
// bracket notation – special characters in trait name that break jsonpath-plus
|
|
377
|
+
[
|
|
378
|
+
{ property: "$.identity.traits['[$the.size$]']", operator: 'EQUAL', value: 'small' },
|
|
379
|
+
true
|
|
380
|
+
],
|
|
381
|
+
[
|
|
382
|
+
{ property: "$.identity.traits['[$the.size$]']", operator: 'EQUAL', value: 'large' },
|
|
383
|
+
false
|
|
384
|
+
],
|
|
385
|
+
// non-existent trait
|
|
386
|
+
[{ property: '$.identity.traits.nonexistent', operator: 'EQUAL', value: 'any' }, false],
|
|
387
|
+
// IS_SET / IS_NOT_SET
|
|
388
|
+
[{ property: '$.identity.traits.age', operator: 'IS_SET', value: null }, true],
|
|
389
|
+
[{ property: '$.identity.traits.nonexistent', operator: 'IS_SET', value: null }, false],
|
|
390
|
+
[{ property: '$.identity.traits.nonexistent', operator: 'IS_NOT_SET', value: null }, true],
|
|
391
|
+
[{ property: '$.identity.traits.age', operator: 'IS_NOT_SET', value: null }, false],
|
|
392
|
+
// IN operator
|
|
393
|
+
[
|
|
394
|
+
{
|
|
395
|
+
property: '$.identity.traits.tamaño',
|
|
396
|
+
operator: CONDITION_OPERATORS.IN,
|
|
397
|
+
value: ['grande', 'pequeño']
|
|
398
|
+
},
|
|
399
|
+
true
|
|
400
|
+
],
|
|
401
|
+
[
|
|
402
|
+
{
|
|
403
|
+
property: '$.identity.traits.tamaño',
|
|
404
|
+
operator: CONDITION_OPERATORS.IN,
|
|
405
|
+
value: ['pequeño']
|
|
406
|
+
},
|
|
407
|
+
false
|
|
408
|
+
]
|
|
409
|
+
] as Array<[SegmentCondition | InSegmentCondition, boolean]>)(
|
|
410
|
+
'evaluates %j to %s',
|
|
411
|
+
(condition, expected) => {
|
|
412
|
+
expect(traitsMatchSegmentCondition(condition, 'seg', mockContext)).toBe(expected);
|
|
413
|
+
}
|
|
414
|
+
);
|
|
415
|
+
});
|
|
416
|
+
|
|
348
417
|
describe('getContextValue', () => {
|
|
349
418
|
const mockContext: EvaluationContext = {
|
|
350
419
|
environment: {
|
|
@@ -354,6 +423,7 @@ describe('getContextValue', () => {
|
|
|
354
423
|
identity: {
|
|
355
424
|
key: 'user-123',
|
|
356
425
|
identifier: 'user@example.com'
|
|
426
|
+
// intentionally no traits – tests below confirm paths that require traits return undefined
|
|
357
427
|
},
|
|
358
428
|
segments: {},
|
|
359
429
|
features: {}
|
|
@@ -371,7 +441,7 @@ describe('getContextValue', () => {
|
|
|
371
441
|
|
|
372
442
|
// Undefined or invalid cases
|
|
373
443
|
test.each([
|
|
374
|
-
['$.identity.traits.user_type', '
|
|
444
|
+
['$.identity.traits.user_type', 'no traits in context'],
|
|
375
445
|
['identity.identifier', 'missing $ prefix'],
|
|
376
446
|
['$.invalid.path', 'completely invalid path'],
|
|
377
447
|
['$.identity.nonexistent', 'valid structure but missing property'],
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
name: Conventional Commit
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
pull_request:
|
|
5
|
-
types:
|
|
6
|
-
- edited
|
|
7
|
-
- opened
|
|
8
|
-
|
|
9
|
-
jobs:
|
|
10
|
-
conventional-commit:
|
|
11
|
-
name: Conventional Commit
|
|
12
|
-
runs-on: ubuntu-latest
|
|
13
|
-
steps:
|
|
14
|
-
- name: Check PR Conventional Commit title
|
|
15
|
-
uses: amannn/action-semantic-pull-request@v5
|
|
16
|
-
env:
|
|
17
|
-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
18
|
-
with:
|
|
19
|
-
types: | # mirrors changelog-sections in the /release-please-config.json
|
|
20
|
-
feat
|
|
21
|
-
fix
|
|
22
|
-
infra
|
|
23
|
-
ci
|
|
24
|
-
docs
|
|
25
|
-
deps
|
|
26
|
-
perf
|
|
27
|
-
refactor
|
|
28
|
-
test
|
|
29
|
-
chore
|