@depup/launchdarkly-node-server-sdk 7.0.4-depup.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/.babelrc +16 -0
- package/.circleci/config.yml +89 -0
- package/.eslintignore +5 -0
- package/.eslintrc.yaml +114 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +37 -0
- package/.github/ISSUE_TEMPLATE/config.yml +5 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- package/.github/pull_request_template.md +21 -0
- package/.github/workflows/stale.yml +8 -0
- package/.hound.yml +33 -0
- package/.ldrelease/config.yml +28 -0
- package/.prettierrc +6 -0
- package/CHANGELOG.md +603 -0
- package/CODEOWNERS +2 -0
- package/CONTRIBUTING.md +55 -0
- package/LICENSE.txt +13 -0
- package/README.md +36 -0
- package/SECURITY.md +5 -0
- package/attribute_reference.js +217 -0
- package/big_segments.js +117 -0
- package/caching_store_wrapper.js +240 -0
- package/changes.json +30 -0
- package/configuration.js +235 -0
- package/context.js +98 -0
- package/context_filter.js +137 -0
- package/contract-tests/README.md +7 -0
- package/contract-tests/index.js +109 -0
- package/contract-tests/log.js +23 -0
- package/contract-tests/package.json +15 -0
- package/contract-tests/sdkClientEntity.js +110 -0
- package/contract-tests/testharness-suppressions.txt +2 -0
- package/diagnostic_events.js +151 -0
- package/docs/typedoc.js +10 -0
- package/errors.js +26 -0
- package/evaluator.js +822 -0
- package/event_factory.js +121 -0
- package/event_processor.js +320 -0
- package/event_summarizer.js +101 -0
- package/feature_store.js +120 -0
- package/feature_store_event_wrapper.js +258 -0
- package/file_data_source.js +192 -0
- package/flags_state.js +46 -0
- package/index.d.ts +2426 -0
- package/index.js +452 -0
- package/integrations.js +7 -0
- package/interfaces.js +2 -0
- package/loggers.js +125 -0
- package/messages.js +31 -0
- package/operators.js +106 -0
- package/package.json +105 -0
- package/polling.js +70 -0
- package/requestor.js +62 -0
- package/scripts/better-audit.sh +76 -0
- package/sharedtest/big_segment_store_tests.js +86 -0
- package/sharedtest/feature_store_tests.js +177 -0
- package/sharedtest/persistent_feature_store_tests.js +183 -0
- package/sharedtest/store_tests.js +7 -0
- package/streaming.js +179 -0
- package/test/LDClient-big-segments-test.js +92 -0
- package/test/LDClient-end-to-end-test.js +218 -0
- package/test/LDClient-evaluation-all-flags-test.js +226 -0
- package/test/LDClient-evaluation-test.js +204 -0
- package/test/LDClient-events-test.js +502 -0
- package/test/LDClient-listeners-test.js +180 -0
- package/test/LDClient-test.js +96 -0
- package/test/LDClient-tls-test.js +110 -0
- package/test/attribute_reference-test.js +494 -0
- package/test/big_segments-test.js +182 -0
- package/test/caching_store_wrapper-test.js +434 -0
- package/test/configuration-test.js +249 -0
- package/test/context-test.js +93 -0
- package/test/context_filter-test.js +424 -0
- package/test/diagnostic_events-test.js +152 -0
- package/test/evaluator-big-segments-test.js +301 -0
- package/test/evaluator-bucketing-test.js +333 -0
- package/test/evaluator-clause-test.js +277 -0
- package/test/evaluator-flag-test.js +452 -0
- package/test/evaluator-pre-conditions-test.js +105 -0
- package/test/evaluator-rule-test.js +131 -0
- package/test/evaluator-segment-match-test.js +310 -0
- package/test/evaluator_helpers.js +106 -0
- package/test/event_processor-test.js +680 -0
- package/test/event_summarizer-test.js +146 -0
- package/test/feature_store-test.js +42 -0
- package/test/feature_store_event_wrapper-test.js +182 -0
- package/test/feature_store_test_base.js +60 -0
- package/test/file_data_source-test.js +255 -0
- package/test/loggers-test.js +126 -0
- package/test/operators-test.js +102 -0
- package/test/polling-test.js +158 -0
- package/test/requestor-test.js +60 -0
- package/test/store_tests_big_segments-test.js +61 -0
- package/test/streaming-test.js +323 -0
- package/test/stubs.js +107 -0
- package/test/test_data-test.js +341 -0
- package/test/update_queue-test.js +61 -0
- package/test-types.ts +210 -0
- package/test_data.js +323 -0
- package/tsconfig.json +14 -0
- package/update_queue.js +28 -0
- package/utils/__tests__/httpUtils-test.js +39 -0
- package/utils/__tests__/wrapPromiseCallback-test.js +33 -0
- package/utils/asyncUtils.js +32 -0
- package/utils/httpUtils.js +105 -0
- package/utils/stringifyAttrs.js +14 -0
- package/utils/wrapPromiseCallback.js +36 -0
- package/versioned_data_kind.js +34 -0
package/operators.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
const semver = require('semver');
|
|
2
|
+
|
|
3
|
+
// Our reference SDK, Go, parses date/time strings with the time.RFC3339Nano format. This regex should match
|
|
4
|
+
// strings that are valid in that format, and no others.
|
|
5
|
+
// Acceptable: 2019-10-31T23:59:59Z, 2019-10-31T23:59:59.100Z, 2019-10-31T23:59:59-07, 2019-10-31T23:59:59-07:00, etc.
|
|
6
|
+
// Unacceptable: no "T", no time zone designation
|
|
7
|
+
const dateRegex = new RegExp('^\\d\\d\\d\\d-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d(\\.\\d\\d*)?(Z|[-+]\\d\\d(:\\d\\d)?)');
|
|
8
|
+
|
|
9
|
+
function stringOperator(f) {
|
|
10
|
+
return (userValue, clauseValue) =>
|
|
11
|
+
typeof userValue === 'string' && typeof clauseValue === 'string' && f(userValue, clauseValue);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function numericOperator(f) {
|
|
15
|
+
return (userValue, clauseValue) =>
|
|
16
|
+
typeof userValue === 'number' && typeof clauseValue === 'number' && f(userValue, clauseValue);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function dateOperator(f) {
|
|
20
|
+
return (userValue, clauseValue) => {
|
|
21
|
+
const userValueNum = parseDate(userValue);
|
|
22
|
+
const clauseValueNum = parseDate(clauseValue);
|
|
23
|
+
return userValueNum !== null && clauseValueNum !== null && f(userValueNum, clauseValueNum);
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseDate(input) {
|
|
28
|
+
switch (typeof input) {
|
|
29
|
+
case 'number':
|
|
30
|
+
return input;
|
|
31
|
+
case 'string':
|
|
32
|
+
return dateRegex.test(input) ? Date.parse(input) : null;
|
|
33
|
+
default:
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function semVerOperator(fn) {
|
|
39
|
+
return (a, b) => {
|
|
40
|
+
const av = parseSemVer(a),
|
|
41
|
+
bv = parseSemVer(b);
|
|
42
|
+
return av && bv ? fn(av, bv) : false;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseSemVer(input) {
|
|
47
|
+
if (typeof input !== 'string') {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
if (input.startsWith('v')) {
|
|
51
|
+
// the semver library tolerates a leading "v", but the standard does not.
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
let ret = semver.parse(input);
|
|
55
|
+
if (!ret) {
|
|
56
|
+
const versionNumericComponents = new RegExp('^\\d+(\\.\\d+)?(\\.\\d+)?').exec(input);
|
|
57
|
+
if (versionNumericComponents) {
|
|
58
|
+
let transformed = versionNumericComponents[0];
|
|
59
|
+
for (let i = 1; i < versionNumericComponents.length; i++) {
|
|
60
|
+
if (versionNumericComponents[i] === undefined) {
|
|
61
|
+
transformed = transformed + '.0';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
transformed = transformed + input.substring(versionNumericComponents[0].length);
|
|
65
|
+
ret = semver.parse(transformed);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return ret;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function safeRegexMatch(pattern, value) {
|
|
72
|
+
try {
|
|
73
|
+
return new RegExp(pattern).test(value);
|
|
74
|
+
} catch (e) {
|
|
75
|
+
// do not propagate this exception, just treat a bad regex as a non-match for consistency with other SDKs
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const operators = {
|
|
81
|
+
in: (a, b) => a === b,
|
|
82
|
+
endsWith: stringOperator((a, b) => a.endsWith(b)),
|
|
83
|
+
startsWith: stringOperator((a, b) => a.startsWith(b)),
|
|
84
|
+
matches: stringOperator((a, b) => safeRegexMatch(b, a)),
|
|
85
|
+
contains: stringOperator((a, b) => a.indexOf(b) > -1),
|
|
86
|
+
lessThan: numericOperator((a, b) => a < b),
|
|
87
|
+
lessThanOrEqual: numericOperator((a, b) => a <= b),
|
|
88
|
+
greaterThan: numericOperator((a, b) => a > b),
|
|
89
|
+
greaterThanOrEqual: numericOperator((a, b) => a >= b),
|
|
90
|
+
before: dateOperator((a, b) => a < b),
|
|
91
|
+
after: dateOperator((a, b) => a > b),
|
|
92
|
+
semVerEqual: semVerOperator((a, b) => a.compare(b) === 0),
|
|
93
|
+
semVerLessThan: semVerOperator((a, b) => a.compare(b) < 0),
|
|
94
|
+
semVerGreaterThan: semVerOperator((a, b) => a.compare(b) > 0),
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const operatorNone = () => false;
|
|
98
|
+
|
|
99
|
+
function fn(op) {
|
|
100
|
+
return operators[op] || operatorNone;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = {
|
|
104
|
+
operators: operators,
|
|
105
|
+
fn: fn,
|
|
106
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@depup/launchdarkly-node-server-sdk",
|
|
3
|
+
"version": "7.0.4-depup.0",
|
|
4
|
+
"description": "[DepUp] LaunchDarkly Server-Side SDK for Node.js",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "jest --ci --coverage --runInBand",
|
|
8
|
+
"check-typescript": "node_modules/typescript/bin/tsc",
|
|
9
|
+
"lint": "eslint --format 'node_modules/eslint-formatter-pretty' --ignore-path .eslintignore .",
|
|
10
|
+
"lint-fix": "eslint --fix --format 'node_modules/eslint-formatter-pretty' --ignore-path .eslintignore .",
|
|
11
|
+
"contract-test-service": "npm --prefix contract-tests install && npm --prefix contract-tests start",
|
|
12
|
+
"contract-test-harness": "curl -s https://raw.githubusercontent.com/launchdarkly/sdk-test-harness/master/downloader/run.sh \\ | VERSION=v2 PARAMS=\"-url http://localhost:8000 -debug -stop-service-at-end $TEST_HARNESS_PARAMS\" sh",
|
|
13
|
+
"contract-tests": "npm run contract-test-service & npm run contract-test-harness"
|
|
14
|
+
},
|
|
15
|
+
"types": "./index.d.ts",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/launchdarkly/node-server-sdk.git"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"depup",
|
|
22
|
+
"dependency-bumped",
|
|
23
|
+
"updated-deps",
|
|
24
|
+
"launchdarkly-node-server-sdk",
|
|
25
|
+
"launchdarkly",
|
|
26
|
+
"analytics",
|
|
27
|
+
"client"
|
|
28
|
+
],
|
|
29
|
+
"license": "Apache-2.0",
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/launchdarkly/node-server-sdk/issues"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://github.com/launchdarkly/node-server-sdk",
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"async": "^3.2.6",
|
|
36
|
+
"launchdarkly-eventsource": "^2.2.0",
|
|
37
|
+
"lru-cache": "^11.2.7",
|
|
38
|
+
"node-cache": "^5.1.2",
|
|
39
|
+
"semver": "^7.7.4",
|
|
40
|
+
"tunnel": "0.0.6",
|
|
41
|
+
"uuid": "^13.0.0"
|
|
42
|
+
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">= 12.0.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@babel/core": "^7.14.6",
|
|
48
|
+
"@babel/preset-env": "^7.14.5",
|
|
49
|
+
"@types/jest": "^27.4.0",
|
|
50
|
+
"@types/node": "^15.12.2",
|
|
51
|
+
"babel-jest": "^27.0.2",
|
|
52
|
+
"eslint": "^7.28.0",
|
|
53
|
+
"eslint-config-prettier": "^8.3.0",
|
|
54
|
+
"eslint-formatter-pretty": "^4.1.0",
|
|
55
|
+
"eslint-plugin-prettier": "^3.4.0",
|
|
56
|
+
"jest": "^27.0.4",
|
|
57
|
+
"jest-junit": "^12.2.0",
|
|
58
|
+
"launchdarkly-js-test-helpers": "^2.2.0",
|
|
59
|
+
"prettier": "^2.3.1",
|
|
60
|
+
"tmp": "^0.2.1",
|
|
61
|
+
"typescript": "~4.4.4",
|
|
62
|
+
"yaml": "^1.10.2"
|
|
63
|
+
},
|
|
64
|
+
"jest": {
|
|
65
|
+
"rootDir": ".",
|
|
66
|
+
"testEnvironment": "node",
|
|
67
|
+
"testMatch": [
|
|
68
|
+
"**/*-test.js"
|
|
69
|
+
],
|
|
70
|
+
"testResultsProcessor": "jest-junit"
|
|
71
|
+
},
|
|
72
|
+
"depup": {
|
|
73
|
+
"changes": {
|
|
74
|
+
"async": {
|
|
75
|
+
"from": "^3.2.4",
|
|
76
|
+
"to": "^3.2.6"
|
|
77
|
+
},
|
|
78
|
+
"launchdarkly-eventsource": {
|
|
79
|
+
"from": "1.4.4",
|
|
80
|
+
"to": "^2.2.0"
|
|
81
|
+
},
|
|
82
|
+
"lru-cache": {
|
|
83
|
+
"from": "^6.0.0",
|
|
84
|
+
"to": "^11.2.7"
|
|
85
|
+
},
|
|
86
|
+
"node-cache": {
|
|
87
|
+
"from": "^5.1.0",
|
|
88
|
+
"to": "^5.1.2"
|
|
89
|
+
},
|
|
90
|
+
"semver": {
|
|
91
|
+
"from": "^7.5.4",
|
|
92
|
+
"to": "^7.7.4"
|
|
93
|
+
},
|
|
94
|
+
"uuid": {
|
|
95
|
+
"from": "^8.3.2",
|
|
96
|
+
"to": "^13.0.0"
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
"depsUpdated": 6,
|
|
100
|
+
"originalPackage": "launchdarkly-node-server-sdk",
|
|
101
|
+
"originalVersion": "7.0.4",
|
|
102
|
+
"processedAt": "2026-03-17T22:56:56.202Z",
|
|
103
|
+
"smokeTest": "passed"
|
|
104
|
+
}
|
|
105
|
+
}
|
package/polling.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const errors = require('./errors');
|
|
2
|
+
const messages = require('./messages');
|
|
3
|
+
const dataKind = require('./versioned_data_kind');
|
|
4
|
+
|
|
5
|
+
function PollingProcessor(config, requestor) {
|
|
6
|
+
const processor = {},
|
|
7
|
+
featureStore = config.featureStore,
|
|
8
|
+
intervalMs = config.pollInterval * 1000;
|
|
9
|
+
|
|
10
|
+
let stopped = false;
|
|
11
|
+
|
|
12
|
+
let pollTask;
|
|
13
|
+
|
|
14
|
+
function poll(maybeCallback) {
|
|
15
|
+
const cb = maybeCallback || function () {};
|
|
16
|
+
|
|
17
|
+
if (stopped) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
config.logger.debug('Polling LaunchDarkly for feature flag updates');
|
|
22
|
+
|
|
23
|
+
requestor.requestAllData((err, respBody) => {
|
|
24
|
+
if (err) {
|
|
25
|
+
if (err.status && !errors.isHttpErrorRecoverable(err.status)) {
|
|
26
|
+
const message = messages.httpErrorMessage(err, 'polling request');
|
|
27
|
+
config.logger.error(message);
|
|
28
|
+
cb(new errors.LDPollingError(message));
|
|
29
|
+
processor.stop();
|
|
30
|
+
} else {
|
|
31
|
+
config.logger.warn(messages.httpErrorMessage(err, 'polling request', 'will retry'));
|
|
32
|
+
}
|
|
33
|
+
} else {
|
|
34
|
+
if (respBody) {
|
|
35
|
+
const allData = JSON.parse(respBody);
|
|
36
|
+
const initData = {};
|
|
37
|
+
initData[dataKind.features.namespace] = allData.flags;
|
|
38
|
+
initData[dataKind.segments.namespace] = allData.segments;
|
|
39
|
+
featureStore.init(initData, () => {
|
|
40
|
+
cb();
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
// There wasn't an error but there wasn't any new data either, so just keep polling
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
processor.start = cb => {
|
|
49
|
+
if (!pollTask && !stopped) {
|
|
50
|
+
pollTask = setInterval(() => poll(cb), intervalMs);
|
|
51
|
+
// setInterval always waits for the delay before firing the first time, but we want to do an initial poll right away
|
|
52
|
+
poll(cb);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
processor.stop = () => {
|
|
57
|
+
stopped = true;
|
|
58
|
+
if (pollTask) {
|
|
59
|
+
clearInterval(pollTask);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
processor.close = () => {
|
|
64
|
+
processor.stop();
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return processor;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = PollingProcessor;
|
package/requestor.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const httpUtils = require('./utils/httpUtils');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates a new Requestor object, which handles remote requests to fetch feature flags or segments for LaunchDarkly.
|
|
5
|
+
* This is never called synchronously when requesting a feature flag for a user (e.g. via the variation method).
|
|
6
|
+
*
|
|
7
|
+
* It will be called at the configured polling interval in polling mode. Older versions of the SDK also
|
|
8
|
+
* could use the Requestor to make a polling request even in streaming mode, for very large data sets,
|
|
9
|
+
* but the LD infrastructure no longer uses that behavior.
|
|
10
|
+
*
|
|
11
|
+
* @param {String} sdkKey the SDK key
|
|
12
|
+
* @param {Object} config the LaunchDarkly client configuration object
|
|
13
|
+
**/
|
|
14
|
+
function Requestor(sdkKey, config) {
|
|
15
|
+
const requestor = {};
|
|
16
|
+
|
|
17
|
+
const headers = httpUtils.getDefaultHeaders(sdkKey, config);
|
|
18
|
+
const requestWithETagCaching = httpUtils.httpWithETagCache();
|
|
19
|
+
|
|
20
|
+
function makeRequest(resource) {
|
|
21
|
+
const url = config.baseUri + resource;
|
|
22
|
+
const requestParams = { method: 'GET', headers };
|
|
23
|
+
return (cb, errCb) => {
|
|
24
|
+
requestWithETagCaching(url, requestParams, null, config, (err, resp, body) => {
|
|
25
|
+
if (err) {
|
|
26
|
+
errCb(err);
|
|
27
|
+
} else {
|
|
28
|
+
cb(resp, body);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function processResponse(cb) {
|
|
35
|
+
return (response, body) => {
|
|
36
|
+
if (response.statusCode !== 200 && response.statusCode !== 304) {
|
|
37
|
+
const err = new Error('Unexpected status code: ' + response.statusCode);
|
|
38
|
+
err.status = response.statusCode;
|
|
39
|
+
cb(err, null);
|
|
40
|
+
} else {
|
|
41
|
+
cb(null, response.statusCode === 304 ? null : body);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function processErrorResponse(cb) {
|
|
47
|
+
return err => {
|
|
48
|
+
cb(err, null);
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Note that requestAllData will pass (null, null) rather than (null, body) if it gets a 304 response;
|
|
53
|
+
// this is deliberate so that we don't keep updating the data store unnecessarily if there are no changes.
|
|
54
|
+
requestor.requestAllData = cb => {
|
|
55
|
+
const req = makeRequest('/sdk/latest-all');
|
|
56
|
+
req(processResponse(cb), processErrorResponse(cb));
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return requestor;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = Requestor;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# This script processes the output of "npm audit" to make it more useful, as follows:
|
|
4
|
+
# - For each flagged vulnerability, it looks at the "path" field and extracts both the flagged
|
|
5
|
+
# package (the last element in the path) and the topmost dependency that led to it (the first
|
|
6
|
+
# element in the path).
|
|
7
|
+
# - It sorts these and eliminates duplicates.
|
|
8
|
+
# - It then compares each of the topmost dependencies to package.json to see if it is from
|
|
9
|
+
# "dependencies", "peerDependencies", or "devDependencies". If it is either of the first two
|
|
10
|
+
# then this is a real runtime vulnerability, and must be fixed by updating the topmost
|
|
11
|
+
# dependency. If it is from devDependencies, then it can be safely fixed with "npm audit fix".
|
|
12
|
+
|
|
13
|
+
set -e
|
|
14
|
+
|
|
15
|
+
function readPackages() {
|
|
16
|
+
inCategory=$1
|
|
17
|
+
jq -r ".${inCategory} | keys | .[]" package.json 2>/dev/null || true
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isInList() {
|
|
21
|
+
item=$1
|
|
22
|
+
shift
|
|
23
|
+
for x in $@; do
|
|
24
|
+
if [ "$item" == "$x" ]; then
|
|
25
|
+
true
|
|
26
|
+
return
|
|
27
|
+
fi
|
|
28
|
+
done
|
|
29
|
+
false
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
dependencies=$(readPackages dependencies)
|
|
33
|
+
devDependencies=$(readPackages devDependencies)
|
|
34
|
+
peerDependencies=$(readPackages peerDependencies)
|
|
35
|
+
|
|
36
|
+
function processItems() {
|
|
37
|
+
flaggedRuntime=0
|
|
38
|
+
flaggedDev=0
|
|
39
|
+
while read -r badPackage topLevelDep; do
|
|
40
|
+
echo -n "flagged package \"$badPackage\", referenced via \"$topLevelDep\" "
|
|
41
|
+
for category in dependencies peerDependencies devDependencies; do
|
|
42
|
+
if isInList $topLevelDep ${!category}; then
|
|
43
|
+
if [ "$category" == "devDependencies" ]; then
|
|
44
|
+
echo "-- from \"$category\""
|
|
45
|
+
flaggedDev=1
|
|
46
|
+
else
|
|
47
|
+
echo "-- from \"$category\" (RUNTIME) ***"
|
|
48
|
+
flaggedRuntime=1
|
|
49
|
+
fi
|
|
50
|
+
break
|
|
51
|
+
fi
|
|
52
|
+
done
|
|
53
|
+
done
|
|
54
|
+
echo
|
|
55
|
+
if [ "$flaggedRuntime" == "1" ]; then
|
|
56
|
+
echo "*** At least one runtime dependency was flagged. These must be fixed by updating package.json."
|
|
57
|
+
echo "Do not use 'npm audit fix'."
|
|
58
|
+
exit 1 # return an error, causing the build to fail
|
|
59
|
+
elif [ "$flaggedDev" == "1" ]; then
|
|
60
|
+
echo "Only development dependencies were flagged. You may safely run 'npm audit fix', which will"
|
|
61
|
+
echo "fix these by adding overrides to package-lock.json."
|
|
62
|
+
else
|
|
63
|
+
echo "Congratulations! No dependencies were flagged by 'npm audit'."
|
|
64
|
+
fi
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
echo "Running npm audit..."
|
|
68
|
+
echo
|
|
69
|
+
|
|
70
|
+
npm audit --json \
|
|
71
|
+
| grep '"path":' \
|
|
72
|
+
| sort | uniq \
|
|
73
|
+
| sed -n -e 's#.*"path": "\([^"]*\)".*#\1#p' \
|
|
74
|
+
| awk -F '>' '{ print $NF,$1 }' \
|
|
75
|
+
| sort | uniq \
|
|
76
|
+
| processItems
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const { nullLogger } = require('../loggers');
|
|
2
|
+
|
|
3
|
+
const { withCloseable } = require('launchdarkly-js-test-helpers');
|
|
4
|
+
|
|
5
|
+
// See index.d.ts for interface documentation
|
|
6
|
+
|
|
7
|
+
const fakeUserHash = 'userhash';
|
|
8
|
+
|
|
9
|
+
function runBigSegmentStoreTests(storeFactory, clearExistingData, setMetadata, setSegments) {
|
|
10
|
+
function doAllTestsWithPrefix(prefix) {
|
|
11
|
+
async function withStoreAndEmptyData(action) {
|
|
12
|
+
await clearExistingData(prefix);
|
|
13
|
+
await withCloseable(storeFactory(prefix, nullLogger()), action);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('getMetadata', () => {
|
|
17
|
+
it('valid value', async () => {
|
|
18
|
+
const expected = { lastUpToDate: 1234567890 };
|
|
19
|
+
await withStoreAndEmptyData(async store => {
|
|
20
|
+
await setMetadata(prefix, expected);
|
|
21
|
+
|
|
22
|
+
const meta = await store.getMetadata();
|
|
23
|
+
expect(meta).toEqual(expected);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('no value', async () => {
|
|
28
|
+
await withStoreAndEmptyData(async store => {
|
|
29
|
+
const meta = await store.getMetadata();
|
|
30
|
+
expect(meta).toEqual({ lastUpToDate: undefined });
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('getUserMembership', () => {
|
|
36
|
+
it('not found', async () => {
|
|
37
|
+
await withStoreAndEmptyData(async store => {
|
|
38
|
+
const membership = await store.getUserMembership(fakeUserHash);
|
|
39
|
+
if (membership) {
|
|
40
|
+
// either null/undefined or an empty membership would be acceptable
|
|
41
|
+
expect(membership).toEqual({});
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('includes only', async () => {
|
|
47
|
+
await withStoreAndEmptyData(async store => {
|
|
48
|
+
await setSegments(prefix, fakeUserHash, ['key1', 'key2'], []);
|
|
49
|
+
|
|
50
|
+
const membership = await store.getUserMembership(fakeUserHash);
|
|
51
|
+
expect(membership).toEqual({ key1: true, key2: true });
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('excludes only', async () => {
|
|
56
|
+
await withStoreAndEmptyData(async store => {
|
|
57
|
+
await setSegments(prefix, fakeUserHash, [], ['key1', 'key2']);
|
|
58
|
+
|
|
59
|
+
const membership = await store.getUserMembership(fakeUserHash);
|
|
60
|
+
expect(membership).toEqual({ key1: false, key2: false });
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('includes and excludes', async () => {
|
|
65
|
+
await withStoreAndEmptyData(async store => {
|
|
66
|
+
await setSegments(prefix, fakeUserHash, ['key1', 'key2'], ['key2', 'key3']);
|
|
67
|
+
|
|
68
|
+
const membership = await store.getUserMembership(fakeUserHash);
|
|
69
|
+
expect(membership).toEqual({ key1: true, key2: true, key3: false }); // include of key2 overrides exclude
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
describe('with non-empty prefix', () => {
|
|
76
|
+
doAllTestsWithPrefix('testprefix');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('with empty prefix', () => {
|
|
80
|
+
doAllTestsWithPrefix(undefined);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = {
|
|
85
|
+
runBigSegmentStoreTests,
|
|
86
|
+
};
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
const dataKind = require('../versioned_data_kind');
|
|
2
|
+
|
|
3
|
+
const { promisifySingle, withCloseable, AsyncQueue } = require('launchdarkly-js-test-helpers');
|
|
4
|
+
|
|
5
|
+
function runFeatureStoreTests(createStore, clearExistingData) {
|
|
6
|
+
const feature1 = { key: 'foo', version: 10 };
|
|
7
|
+
const feature2 = { key: 'bar', version: 10 };
|
|
8
|
+
|
|
9
|
+
async function withInitedStore(asyncAction) {
|
|
10
|
+
if (clearExistingData) {
|
|
11
|
+
await clearExistingData();
|
|
12
|
+
}
|
|
13
|
+
const store = createStore();
|
|
14
|
+
await withCloseable(store, async () => {
|
|
15
|
+
const initData = {
|
|
16
|
+
[dataKind.features.namespace]: {
|
|
17
|
+
foo: feature1,
|
|
18
|
+
bar: feature2,
|
|
19
|
+
},
|
|
20
|
+
[dataKind.segments.namespace]: {},
|
|
21
|
+
};
|
|
22
|
+
await promisifySingle(store.init)(initData);
|
|
23
|
+
await asyncAction(store);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
it('is initialized after calling init()', async () => {
|
|
28
|
+
await withInitedStore(async store => {
|
|
29
|
+
const result = await promisifySingle(store.initialized)();
|
|
30
|
+
expect(result).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('init() completely replaces previous data', async () => {
|
|
35
|
+
await withInitedStore(async store => {
|
|
36
|
+
const flags = {
|
|
37
|
+
first: { key: 'first', version: 1 },
|
|
38
|
+
second: { key: 'second', version: 1 },
|
|
39
|
+
};
|
|
40
|
+
const segments = { first: { key: 'first', version: 2 } };
|
|
41
|
+
const initData1 = {
|
|
42
|
+
[dataKind.features.namespace]: flags,
|
|
43
|
+
[dataKind.segments.namespace]: segments,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
await promisifySingle(store.init)(initData1);
|
|
47
|
+
const items1 = await promisifySingle(store.all)(dataKind.features);
|
|
48
|
+
expect(items1).toEqual(flags);
|
|
49
|
+
const items2 = await promisifySingle(store.all)(dataKind.segments);
|
|
50
|
+
expect(items2).toEqual(segments);
|
|
51
|
+
|
|
52
|
+
const newFlags = { first: { key: 'first', version: 3 } };
|
|
53
|
+
const newSegments = { first: { key: 'first', version: 4 } };
|
|
54
|
+
const initData2 = {
|
|
55
|
+
[dataKind.features.namespace]: newFlags,
|
|
56
|
+
[dataKind.segments.namespace]: newSegments,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
await promisifySingle(store.init)(initData2);
|
|
60
|
+
const items3 = await promisifySingle(store.all)(dataKind.features);
|
|
61
|
+
expect(items3).toEqual(newFlags);
|
|
62
|
+
const items4 = await promisifySingle(store.all)(dataKind.segments);
|
|
63
|
+
expect(items4).toEqual(newSegments);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('gets existing feature', async () => {
|
|
68
|
+
await withInitedStore(async store => {
|
|
69
|
+
const result = await promisifySingle(store.get)(dataKind.features, feature1.key);
|
|
70
|
+
expect(result).toEqual(feature1);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('does not get nonexisting feature', async () => {
|
|
75
|
+
await withInitedStore(async store => {
|
|
76
|
+
const result = await promisifySingle(store.get)(dataKind.features, 'biz');
|
|
77
|
+
expect(result).toBe(null);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('gets all features', async () => {
|
|
82
|
+
await withInitedStore(async store => {
|
|
83
|
+
const result = await promisifySingle(store.all)(dataKind.features);
|
|
84
|
+
expect(result).toEqual({
|
|
85
|
+
foo: feature1,
|
|
86
|
+
bar: feature2,
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('upserts with newer version', async () => {
|
|
92
|
+
const newVer = { key: feature1.key, version: feature1.version + 1 };
|
|
93
|
+
await withInitedStore(async store => {
|
|
94
|
+
await promisifySingle(store.upsert)(dataKind.features, newVer);
|
|
95
|
+
const result = await promisifySingle(store.get)(dataKind.features, feature1.key);
|
|
96
|
+
expect(result).toEqual(newVer);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('does not upsert with older version', async () => {
|
|
101
|
+
const oldVer = { key: feature1.key, version: feature1.version - 1 };
|
|
102
|
+
await withInitedStore(async store => {
|
|
103
|
+
await promisifySingle(store.upsert)(dataKind.features, oldVer);
|
|
104
|
+
const result = await promisifySingle(store.get)(dataKind.features, feature1.key);
|
|
105
|
+
expect(result).toEqual(feature1);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('upserts new feature', async () => {
|
|
110
|
+
const newFeature = { key: 'biz', version: 99 };
|
|
111
|
+
await withInitedStore(async store => {
|
|
112
|
+
await promisifySingle(store.upsert)(dataKind.features, newFeature);
|
|
113
|
+
const result = await promisifySingle(store.get)(dataKind.features, newFeature.key);
|
|
114
|
+
expect(result).toEqual(newFeature);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('handles upsert race condition within same client correctly', async () => {
|
|
119
|
+
// Not sure if there is a way to do this one with async/await
|
|
120
|
+
const ver1 = { key: feature1.key, version: feature1.version + 1 };
|
|
121
|
+
const ver2 = { key: feature1.key, version: feature1.version + 2 };
|
|
122
|
+
const calls = new AsyncQueue();
|
|
123
|
+
await withInitedStore(async store => {
|
|
124
|
+
const callback = () => {
|
|
125
|
+
calls.add(null);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// Deliberately do not wait for the first upsert to complete before starting the second,
|
|
129
|
+
// so their transactions will be interleaved unless we're correctly serializing updates
|
|
130
|
+
store.upsert(dataKind.features, ver2, callback);
|
|
131
|
+
store.upsert(dataKind.features, ver1, callback);
|
|
132
|
+
|
|
133
|
+
// Now wait until both have completed
|
|
134
|
+
await calls.take();
|
|
135
|
+
await calls.take();
|
|
136
|
+
const result = await promisifySingle(store.get)(dataKind.features, feature1.key);
|
|
137
|
+
expect(result).toEqual(ver2);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('deletes with newer version', async () => {
|
|
142
|
+
await withInitedStore(async store => {
|
|
143
|
+
await promisifySingle(store.delete)(dataKind.features, feature1.key, feature1.version + 1);
|
|
144
|
+
const result = await promisifySingle(store.get)(dataKind.features, feature1.key);
|
|
145
|
+
expect(result).toBe(null);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('does not delete with older version', async () => {
|
|
150
|
+
await withInitedStore(async store => {
|
|
151
|
+
await promisifySingle(store.delete)(dataKind.features, feature1.key, feature1.version - 1);
|
|
152
|
+
const result = await promisifySingle(store.get)(dataKind.features, feature1.key);
|
|
153
|
+
expect(result).not.toBe(null);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('allows deleting unknown feature', async () => {
|
|
158
|
+
await withInitedStore(async store => {
|
|
159
|
+
await promisifySingle(store.delete)(dataKind.features, 'biz', 99);
|
|
160
|
+
const result = await promisifySingle(store.get)(dataKind.features, 'biz');
|
|
161
|
+
expect(result).toBe(null);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('does not upsert older version after delete', async () => {
|
|
166
|
+
await withInitedStore(async store => {
|
|
167
|
+
await promisifySingle(store.delete)(dataKind.features, feature1.key, feature1.version + 1);
|
|
168
|
+
await promisifySingle(store.upsert)(dataKind.features, feature1);
|
|
169
|
+
const result = await promisifySingle(store.get)(dataKind.features, feature1.key);
|
|
170
|
+
expect(result).toBe(null);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
module.exports = {
|
|
176
|
+
runFeatureStoreTests,
|
|
177
|
+
};
|