Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pull upstream #1

Merged
merged 15 commits into from
May 18, 2023
Merged
11 changes: 7 additions & 4 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,17 @@ workflows:
docker-image: cimg/node:current
run-lint: true
- build-test-linux:
name: Node 16.3
docker-image: cimg/node:16.3
name: Node 17.9
docker-image: cimg/node:17.9
- build-test-linux:
name: Node 16.14
docker-image: cimg/node:16.14
- build-test-linux:
name: Node 15.14
docker-image: cimg/node:15.14
- build-test-linux:
name: Node 14.17
docker-image: cimg/node:14.17
name: Node 14.19
docker-image: cimg/node:14.19
- build-test-linux:
name: Node 13.14
docker-image: cimg/node:13.14
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ junit.xml
npm-debug.log
test-types.js
.vscode
coverage/
5 changes: 3 additions & 2 deletions .ldrelease/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ repo:
private: node-server-sdk-private

branches:
- name: master
description: 6.x
- name: main
description: 7.x
- name: 6.x
- name: 5.x

publications:
Expand Down
50 changes: 50 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,56 @@

All notable changes to the LaunchDarkly Server-Side SDK for Node.js will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org).

## [7.0.1] - 2023-02-08
### Fixed:
- Updated the `async` package dependency. The previous version of `async` was flagged in [CVE-2021-43138](https://nvd.nist.gov/vuln/detail/CVE-2021-43138)

## [7.0.0] - 2022-12-07
The latest version of this SDK supports LaunchDarkly's new custom contexts feature. Contexts are an evolution of a previously-existing concept, "users." Contexts let you create targeting rules for feature flags based on a variety of different information, including attributes pertaining to users, organizations, devices, and more. You can even combine contexts to create "multi-contexts."

For detailed information about this version, please refer to the list below. For information on how to upgrade from the previous version, please read the [migration guide](https://docs.launchdarkly.com/sdk/server-side/node-js/migration-6-to-7).

### Added:
- The types `LDContext`, `LDSingleKindContext`, and `LDMultiKindContext` define the new "context" model.
- All SDK methods that took an `LDUser` parameter now take an `LDContext`. `LDUser` is now a subset of `LDContext`, so existing code based on users will still work.
- Added `contextKeysCapacity` and `contextKeysFlushInterval` to `LDOptions` these supersede the equivalent `user` options.

### Changed _(breaking changes from 6.x)_:
- There is no longer such a thing as a `secondary` meta-attribute that affects percentage rollouts. If you set an attribute with that name in `LDContext`, it will simply be a custom attribute like any other.
- Evaluations now treat the `anonymous` attribute as a simple boolean, with no distinction between a false state and an undefined state.
- `LDClient.getUser` has been replaced with `LDClient.getContext`.
- `privateAttributeNames` has been replaced with `privateAttributes` in `LDOptions`. Private attributes now allow using attribute references.

### Changed (behavioral changes):
- Analytics event data now uses a new JSON schema due to differences between the context model and the old user model.

### Removed:
- Removed all types, fields, and methods that were deprecated as of the most recent 6.x release.
- Removed the `secondary` meta-attribute in `LDUser`.
- The `alias` method no longer exists because alias events are not needed in the new context model.
- The `inlineUsersInEvents` options no longer exist because they are not relevant in the new context model.

## Deprecated:
- Deprecated `userKeysCapacity` and `userKeysFlushInterval` in `LDOptions`. New `context` equivalents have been added.

## [6.4.3] - 2022-09-06
### Fixed:
- The `TestDataRuleBuilder` was using an incorrect field name for operators, which would prevent rules from working correctly with the `TestData` feature. (Thanks, [LiamMorrow](https://github.com/launchdarkly/node-server-sdk/issues/258)!)

## [6.4.2] - 2022-06-06
### Changed:
- Enforce a 64 character limit for `application.id` and `application.version` configuration options.

## [6.4.1] - 2022-04-26
### Fixed:
- When using polling mode (`stream: false`), if the SDK was shut down with `close()`, a timer task could still remain active for up to 30 seconds (or whatever the polling interval was set to).

## [6.4.0] - 2022-03-22
This release is functionally identical to the 6.3.1 release. It exists for compliance with semantic versioning, and to correct the changelog: the new feature described below was accidentally omitted from the changelog, and the minor version number should have been incremented.

### Added:
- `LDOptions.application`, for configuration of application metadata that may be used in LaunchDarkly analytics or other product features. This does not affect feature flag evaluations.

## [6.3.1] - 2022-03-10
### Fixed:
- Removed a transitive dependency on the package `url-parse`, which was flagged in [CVE-2022-0686](https://nvd.nist.gov/vuln/detail/CVE-2022-0686).
Expand Down
6 changes: 6 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ To verify that the TypeScript declarations compile correctly (this involves comp
npm run check-typescript
```

To run the SDK contract test suite (see [`contract-tests/README.md`](./contract-tests/README.md)):

```bash
npm run contract-tests
```

### Auditing package dependencies

The `npm audit` tool compares all dependencies and transitive dependencies to a database of package versions with known vulnerabilities. However, the output of this tool includes both runtime and development dependencies.
Expand Down
217 changes: 217 additions & 0 deletions attribute_reference.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
/**
* Take a key string and escape the characters to allow it to be used as a reference.
* @param {string} key
* @returns {string} The processed key.
*/
function processEscapeCharacters(key) {
return key.replace(/~/g, '~0').replace(/\//g, '~1');
}

/**
* @param {string} reference The reference to get the components of.
* @returns {string[]} The components of the reference. Escape characters will be converted to their representative values.
*/
function getComponents(reference) {
const referenceWithoutPrefix = reference.startsWith('/') ? reference.substring(1) : reference;
return referenceWithoutPrefix
.split('/')
.map(component => (component.indexOf('~') >= 0 ? component.replace(/~1/g, '/').replace(/~0/g, '~') : component));
}

/**
* @param {string} reference The reference to check if it is a literal.
* @returns true if the reference is a literal.
*/
function isLiteral(reference) {
return !reference.startsWith('/');
}

/**
* Get an attribute value from a literal.
* @param {Object} target
* @param {string} literal
*/
function getFromLiteral(target, literal) {
if (target !== null && target !== undefined && Object.prototype.hasOwnProperty.call(target, literal)) {
return target[literal];
}
}

/**
* Gets the `target` object's value at the `reference`'s location.
*
* This method method follows the rules for accessing attributes for use
* in evaluating clauses.
*
* Accessing the root of the target will always result in undefined.
*
* @param {Object} target
* @param {string} reference
* @returns The `target` object's value at the `reference`'s location.
* Undefined if the field does not exist or if the reference is not valid.
*/
function get(target, reference) {
if (reference === '' || reference === '/') {
return undefined;
}

if (isLiteral(reference)) {
return getFromLiteral(target, reference);
}

const components = getComponents(reference);
let current = target;
for (const component of components) {
if (
current !== null &&
current !== undefined &&
typeof current === 'object' &&
// We do not want to allow indexing into an array.
!Array.isArray(current) &&
// For arrays and strings, in addition to objects, a hasOwnProperty check
// will be true for indexes (as strings or numbers), which are present
// in the object/string/array.
Object.prototype.hasOwnProperty.call(current, component)
) {
current = current[component];
} else {
return undefined;
}
}

return current;
}

/**
* Compare two references and determine if they are equivalent.
* @param {string} a
* @param {string} b
*/
function compare(a, b) {
const aIsLiteral = isLiteral(a);
const bIsLiteral = isLiteral(b);
if (aIsLiteral && bIsLiteral) {
return a === b;
}
if (aIsLiteral) {
const bComponents = getComponents(b);
if (bComponents.length !== 1) {
return false;
}
return a === bComponents[0];
}
if (bIsLiteral) {
const aComponents = getComponents(a);
if (aComponents.length !== 1) {
return false;
}
return b === aComponents[0];
}
return a === b;
}

/**
* @param {string} a
* @param {string} b
* @returns The two strings joined by '/'.
*/
function join(a, b) {
return `${a}/${b}`;
}

/**
* There are cases where a field could have been named with a preceeding '/'.
* If that attribute was private, then the literal would appear to be a reference.
* This method can be used to convert a literal to a reference in such situations.
* @param {string} literal The literal to convert to a reference.
* @returns A literal which has been converted to a reference.
*/
function literalToReference(literal) {
return `/${processEscapeCharacters(literal)}`;
}

/**
* Clone an object excluding the values referenced by a list of references.
* @param {Object} target The object to clone.
* @param {string[]} references A list of references from the cloned object.
* @returns {{cloned: Object, excluded: string[]}} The cloned object and a list of excluded values.
*/
function cloneExcluding(target, references) {
const stack = [];
const cloned = {};
const excluded = [];

stack.push(
...Object.keys(target).map(key => ({
key,
ptr: literalToReference(key),
source: target,
parent: cloned,
visited: [target],
}))
);

while (stack.length) {
const item = stack.pop();
if (!references.some(ptr => compare(ptr, item.ptr))) {
const value = item.source[item.key];

// Handle null because it overlaps with object, which we will want to handle later.
if (value === null) {
item.parent[item.key] = value;
} else if (Array.isArray(value)) {
item.parent[item.key] = [...value];
} else if (typeof value === 'object') {
//Arrays and null must already be handled.

//Prevent cycles by not visiting the same object
//with in the same branch. Parallel branches
//may contain the same object.
if (item.visited.includes(value)) {
continue;
}

item.parent[item.key] = {};

stack.push(
...Object.keys(value).map(key => ({
key,
ptr: join(item.ptr, processEscapeCharacters(key)),
source: value,
parent: item.parent[item.key],
visited: [...item.visited, value],
}))
);
} else {
item.parent[item.key] = value;
}
} else {
excluded.push(item.ptr);
}
}
return { cloned, excluded: excluded.sort() };
}

function isValidReference(reference) {
return !reference.match(/\/\/|(^\/.*~[^0|^1])|~$/);
}

/**
* Check if the given attribute reference is for the "kind" attribute.
* @param {string} reference String containing an attribute reference.
*/
function isKind(reference) {
// There are only 2 valid ways to specify the kind attribute,
// so this just checks them. Given the current flow of evaluation
// this is much less intense a process than doing full validation and parsing.
return reference === 'kind' || reference === '/kind';
}

module.exports = {
cloneExcluding,
compare,
get,
isValidReference,
literalToReference,
isKind,
};
17 changes: 12 additions & 5 deletions configuration.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,9 @@ module.exports = (function () {
offline: false,
useLdd: false,
allAttributesPrivate: false,
privateAttributeNames: [],
inlineUsersInEvents: false,
userKeysCapacity: 1000,
userKeysFlushInterval: 300,
privateAttributes: [],
contextKeysCapacity: 1000,
contextKeysFlushInterval: 300,
diagnosticOptOut: false,
diagnosticRecordingInterval: 900,
featureStore: InMemoryFeatureStore(),
Expand Down Expand Up @@ -63,6 +62,10 @@ module.exports = (function () {
config.logger.warn(messages.invalidTagValue(name));
return undefined;
}
if (tagValue.length > 64) {
config.logger.warn(messages.tagValueTooLong(name));
return undefined;
}
return tagValue;
}

Expand All @@ -85,7 +88,10 @@ module.exports = (function () {
};

/* eslint-disable camelcase */
const deprecatedOptions = {};
const deprecatedOptions = {
userKeysCapacity: 'contextKeysCapacity',
userKeysFlushInterval: 'contextKeysFlushInterval',
};
/* eslint-enable camelcase */

function checkDeprecatedOptions(configIn) {
Expand Down Expand Up @@ -133,6 +139,7 @@ module.exports = (function () {
}
return 'object';
};

Object.keys(config).forEach(name => {
const value = config[name];
if (value !== null && value !== undefined) {
Expand Down
Loading