Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

launchdarkly-js-sdk-common

Package Overview
Dependencies
Maintainers
1
Versions
79
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

launchdarkly-js-sdk-common - npm Package Compare versions

Comparing version 4.0.3 to 4.1.0

src/__tests__/headers-test.js

8

CHANGELOG.md

@@ -5,2 +5,6 @@ # Change log

## [4.0.3] - 2022-02-16
### Fixed:
- If the SDK receives invalid JSON data from a streaming connection (possibly as a result of the connection being cut off), it now uses its regular error-handling logic: the error is emitted as an `error` event or, if there are no `error` event listeners, it is logged. Previously, it would be thrown as an unhandled exception.
## [4.0.2] - 2022-01-25

@@ -23,2 +27,6 @@ ### Removed:

## [3.5.1] - 2022-02-17
### Fixed:
- If the SDK receives invalid JSON data from a streaming connection (possibly as a result of the connection being cut off), it now uses its regular error-handling logic: the error is emitted as an `error` event or, if there are no `error` event listeners, it is logged. Previously, it would be thrown as an unhandled exception.
## [3.5.0] - 2022-01-14

@@ -25,0 +33,0 @@ ### Added:

2

package.json
{
"name": "launchdarkly-js-sdk-common",
"version": "4.0.3",
"version": "4.1.0",
"description": "LaunchDarkly SDK for JavaScript - common code",

@@ -5,0 +5,0 @@ "author": "LaunchDarkly <team@launchdarkly.com>",

@@ -217,2 +217,41 @@ import { sleepAsync, eventSink } from 'launchdarkly-js-test-helpers';

});
it('handles a valid application id', async () => {
const listener = errorListener();
const configIn = { application: { id: 'test-application' } };
expect(configuration.validate(configIn, listener.emitter, null, listener.logger).application.id).toEqual(
'test-application'
);
});
it('logs a warning with an invalid application id', async () => {
const listener = errorListener();
const configIn = { application: { id: 'test #$#$#' } };
expect(configuration.validate(configIn, listener.emitter, null, listener.logger).application.id).toBeUndefined();
await listener.expectWarningOnly(messages.invalidTagValue('application.id'));
});
it('handles a valid application version', async () => {
const listener = errorListener();
const configIn = { application: { version: 'test-version' } };
expect(configuration.validate(configIn, listener.emitter, null, listener.logger).application.version).toEqual(
'test-version'
);
});
it('logs a warning with an invalid application version', async () => {
const listener = errorListener();
const configIn = { application: { version: 'test #$#$#' } };
expect(
configuration.validate(configIn, listener.emitter, null, listener.logger).application.version
).toBeUndefined();
await listener.expectWarningOnly(messages.invalidTagValue('application.version'));
});
it('includes application id and version in tags when present', async () => {
expect(configuration.getTags({ application: { id: 'test-id', version: 'test-version' } })).toEqual({
'application-id': ['test-id'],
'application-version': ['test-version'],
});
});
});
import { DiagnosticsAccumulator } from '../diagnosticEvents';
import * as messages from '../messages';
import Stream from '../Stream';
import { getLDHeaders } from '../utils';
import { getLDHeaders } from '../headers';

@@ -6,0 +6,0 @@ import { sleepAsync } from 'launchdarkly-js-test-helpers';

import {
appendUrlPath,
base64URLEncode,
getLDHeaders,
transformHeaders,
getLDUserAgentString,

@@ -13,2 +12,9 @@ wrapPromiseCallback,

describe('utils', () => {
it('appendUrlPath', () => {
expect(appendUrlPath('http://base', '/path')).toEqual('http://base/path');
expect(appendUrlPath('http://base', 'path')).toEqual('http://base/path');
expect(appendUrlPath('http://base/', '/path')).toEqual('http://base/path');
expect(appendUrlPath('http://base/', '/path')).toEqual('http://base/path');
});
describe('wrapPromiseCallback', () => {

@@ -52,73 +58,2 @@ it('should resolve to the value', done => {

describe('getLDHeaders', () => {
it('sends no headers unless sendLDHeaders is true', () => {
const platform = stubPlatform.defaults();
const headers = getLDHeaders(platform, {});
expect(headers).toEqual({});
});
it('adds user-agent header', () => {
const platform = stubPlatform.defaults();
const headers = getLDHeaders(platform, { sendLDHeaders: true });
expect(headers).toMatchObject({ 'User-Agent': getLDUserAgentString(platform) });
});
it('adds user-agent header with custom name', () => {
const platform = stubPlatform.defaults();
platform.userAgentHeaderName = 'X-Fake-User-Agent';
const headers = getLDHeaders(platform, { sendLDHeaders: true });
expect(headers).toMatchObject({ 'X-Fake-User-Agent': getLDUserAgentString(platform) });
});
it('adds wrapper info if specified, without version', () => {
const platform = stubPlatform.defaults();
const headers = getLDHeaders(platform, { sendLDHeaders: true, wrapperName: 'FakeSDK' });
expect(headers).toMatchObject({
'User-Agent': getLDUserAgentString(platform),
'X-LaunchDarkly-Wrapper': 'FakeSDK',
});
});
it('adds wrapper info if specified, with version', () => {
const platform = stubPlatform.defaults();
const headers = getLDHeaders(platform, { sendLDHeaders: true, wrapperName: 'FakeSDK', wrapperVersion: '9.9' });
expect(headers).toMatchObject({
'User-Agent': getLDUserAgentString(platform),
'X-LaunchDarkly-Wrapper': 'FakeSDK/9.9',
});
});
});
describe('transformHeaders', () => {
it('does not modify the headers if the option is not available', () => {
const inputHeaders = { a: '1', b: '2' };
const headers = transformHeaders(inputHeaders, {});
expect(headers).toEqual(inputHeaders);
});
it('modifies the headers if the option has a transform', () => {
const inputHeaders = { c: '3', d: '4' };
const outputHeaders = { c: '9', d: '4', e: '5' };
const headerTransform = input => {
const output = { ...input };
output['c'] = '9';
output['e'] = '5';
return output;
};
const headers = transformHeaders(inputHeaders, { requestHeaderTransform: headerTransform });
expect(headers).toEqual(outputHeaders);
});
it('cannot mutate the input header object', () => {
const inputHeaders = { f: '6' };
const expectedInputHeaders = { f: '6' };
const headerMutate = input => {
input['f'] = '7'; // eslint-disable-line no-param-reassign
return input;
};
transformHeaders(inputHeaders, { requestHeaderTransform: headerMutate });
expect(inputHeaders).toEqual(expectedInputHeaders);
});
});
describe('getLDUserAgentString', () => {

@@ -125,0 +60,0 @@ it('uses platform user-agent and unknown version by default', () => {

@@ -41,4 +41,34 @@ const errors = require('./errors');

autoAliasingOptOut: { default: false },
application: { validator: applicationConfigValidator },
};
/**
* Expression to validate characters that are allowed in tag keys and values.
*/
const allowedTagCharacters = /^(\w|\.|-)+$/;
/**
* Verify that a value meets the requirements for a tag value.
* @param {Object} config
* @param {string} tagValue
*/
function validateTagValue(name, config, tagValue, logger) {
if (typeof tagValue !== 'string' || !tagValue.match(allowedTagCharacters)) {
logger.warn(messages.invalidTagValue(name));
return undefined;
}
return tagValue;
}
function applicationConfigValidator(name, config, value, logger) {
const validated = {};
if (value.id) {
validated.id = validateTagValue(`${name}.id`, config, value.id, logger);
}
if (value.version) {
validated.version = validateTagValue(`${name}.version`, config, value.version, logger);
}
return validated;
}
function validate(options, emitter, extraOptionDefs, logger) {

@@ -108,3 +138,11 @@ const optionDefs = utils.extend({ logger: { default: logger } }, baseOptionDefs, extraOptionDefs);

const expectedType = optionDef.type || typeDescForValue(optionDef.default);
if (expectedType !== 'any') {
const validator = optionDef.validator;
if (validator) {
const validated = validator(name, config, config[name], logger);
if (validated !== undefined) {
ret[name] = validated;
} else {
delete ret[name];
}
} else if (expectedType !== 'any') {
const allowedTypes = expectedType.split('|');

@@ -150,5 +188,28 @@ const actualType = typeDescForValue(value);

/**
* Get tags for the specified configuration.
*
* If any additional tags are added to the configuration, then the tags from
* this method should be extended with those.
* @param {Object} config The already valiated configuration.
* @returns {Object} The tag configuration.
*/
function getTags(config) {
const tags = {};
if (config) {
if (config.application && config.application.id !== undefined && config.application.id !== null) {
tags['application-id'] = [config.application.id];
}
if (config.application && config.application.version !== undefined && config.application.id !== null) {
tags['application-version'] = [config.application.version];
}
}
return tags;
}
module.exports = {
baseOptionDefs,
validate,
getTags,
};

@@ -8,2 +8,3 @@ const { v1: uuidv1 } = require('uuid');

const messages = require('./messages');
const { appendUrlPath } = require('./utils');

@@ -84,3 +85,3 @@ function DiagnosticId(sdkKey) {

const localStorageKey = 'ld:' + environmentId + ':$diagnostics';
const diagnosticEventsUrl = config.eventsUrl + '/events/diagnostic/' + environmentId;
const diagnosticEventsUrl = appendUrlPath(config.eventsUrl, '/events/diagnostic/' + environmentId);
const periodicInterval = config.diagnosticRecordingInterval;

@@ -87,0 +88,0 @@ const acc = accumulator;

@@ -18,3 +18,3 @@ const EventSender = require('./EventSender');

const eventSender = sender || EventSender(platform, environmentId, options);
const mainEventsUrl = options.eventsUrl + '/events/bulk/' + environmentId;
const mainEventsUrl = utils.appendUrlPath(options.eventsUrl, '/events/bulk/' + environmentId);
const summarizer = EventSummarizer();

@@ -21,0 +21,0 @@ const userFilter = UserFilter(options);

const errors = require('./errors');
const utils = require('./utils');
const { v1: uuidv1 } = require('uuid');
const { getLDHeaders, transformHeaders } = require('./headers');

@@ -9,3 +10,3 @@ const MAX_URL_LENGTH = 2000;

const imageUrlPath = '/a/' + environmentId + '.gif';
const baseHeaders = utils.extend({ 'Content-Type': 'application/json' }, utils.getLDHeaders(platform, options));
const baseHeaders = utils.extend({ 'Content-Type': 'application/json' }, getLDHeaders(platform, options));
const httpFallbackPing = platform.httpFallbackPing; // this will be set for us if we're in the browser SDK

@@ -38,3 +39,3 @@ const sender = {};

return platform
.httpRequest('POST', url, utils.transformHeaders(headers, options), jsonBody)
.httpRequest('POST', url, transformHeaders(headers, options), jsonBody)
.promise.then(result => {

@@ -41,0 +42,0 @@ if (!result) {

@@ -183,2 +183,4 @@ const errors = require('./errors');

const invalidTagValue = name => `Config option "${name}" must only contain letters, numbers, ., _ or -.`;
module.exports = {

@@ -211,2 +213,3 @@ bootstrapInvalid,

invalidKey,
invalidTagValue,
invalidUser,

@@ -213,0 +216,0 @@ localStorageUnavailable,

@@ -5,2 +5,3 @@ const utils = require('./utils');

const promiseCoalescer = require('./promiseCoalescer');
const { transformHeaders, getLDHeaders } = require('./headers');

@@ -35,3 +36,3 @@ const jsonContentType = 'application/json';

const method = body ? 'REPORT' : 'GET';
const headers = utils.getLDHeaders(platform, options);
const headers = getLDHeaders(platform, options);
if (body) {

@@ -50,3 +51,3 @@ headers['Content-Type'] = jsonContentType;

const req = platform.httpRequest(method, endpoint, utils.transformHeaders(headers, options), body);
const req = platform.httpRequest(method, endpoint, transformHeaders(headers, options), body);
const p = req.promise.then(

@@ -81,3 +82,3 @@ result => {

requestor.fetchJSON = function(path) {
return fetchJSON(baseUrl + path, null);
return fetchJSON(utils.appendUrlPath(baseUrl, path), null);
};

@@ -84,0 +85,0 @@

const messages = require('./messages');
const { base64URLEncode, getLDHeaders, transformHeaders, objectHasOwnProperty } = require('./utils');
const { appendUrlPath, base64URLEncode, objectHasOwnProperty } = require('./utils');
const { getLDHeaders, transformHeaders } = require('./headers');

@@ -23,3 +24,3 @@ // The underlying event source implementation is abstracted via the platform object, which should

const stream = {};
const evalUrlPrefix = baseUrl + '/eval/' + environment;
const evalUrlPrefix = appendUrlPath(baseUrl, '/eval/' + environment);
const useReport = config.useReport;

@@ -102,3 +103,3 @@ const withReasons = config.evaluationReasons;

// if we can't do REPORT, fall back to the old ping-based stream
url = baseUrl + '/ping/' + environment;
url = appendUrlPath(baseUrl, '/ping/' + environment);
query = '';

@@ -105,0 +106,0 @@ }

@@ -6,2 +6,9 @@ const base64 = require('base64-js');

function appendUrlPath(baseUrl, path) {
// Ensure that URL concatenation is done correctly regardless of whether the
// base URL has a trailing slash or not.
const trimBaseUrl = baseUrl.endsWith('/') ? baseUrl.substring(0, baseUrl.length - 1) : baseUrl;
return trimBaseUrl + (path.startsWith('/') ? '' : '/') + path;
}
// See http://ecmanaut.blogspot.com/2006/07/encoding-decoding-utf8-in-javascript.html

@@ -154,23 +161,2 @@ function btoa(s) {

function getLDHeaders(platform, options) {
if (options && !options.sendLDHeaders) {
return {};
}
const h = {};
h[platform.userAgentHeaderName || 'User-Agent'] = getLDUserAgentString(platform);
if (options && options.wrapperName) {
h['X-LaunchDarkly-Wrapper'] = options.wrapperVersion
? options.wrapperName + '/' + options.wrapperVersion
: options.wrapperName;
}
return h;
}
function transformHeaders(headers, options) {
if (!options || !options.requestHeaderTransform) {
return headers;
}
return options.requestHeaderTransform({ ...headers });
}
function extend(...objects) {

@@ -201,2 +187,3 @@ return objects.reduce((acc, obj) => ({ ...acc, ...obj }), {});

module.exports = {
appendUrlPath,
base64URLEncode,

@@ -208,3 +195,2 @@ btoa,

extend,
getLDHeaders,
getLDUserAgentString,

@@ -214,3 +200,2 @@ objectHasOwnProperty,

sanitizeUser,
transformHeaders,
transformValuesToVersionedValues,

@@ -217,0 +202,0 @@ transformVersionedValuesToValues,

@@ -50,3 +50,7 @@

streamReconnectDelay: 1,
logger: logger
logger: logger,
application: {
version: 'version',
id: 'id'
}
};

@@ -53,0 +57,0 @@

@@ -118,6 +118,10 @@ /**

*
* Currently these are used to track what version of the SDK is active. This defaults to true
* (custom headers will be sent). One reason you might want to set it to false is that the presence
* of custom headers causes browsers to make an extra OPTIONS request (a CORS preflight check)
* before each flag request, which could affect performance.
* These are used to send metadata about the SDK (such as the version). They
* are also used to send the application.id and application.version set in
* the options.
*
* This defaults to true (custom headers will be sent). One reason you might
* want to set it to false is that the presence of custom headers causes
* browsers to make an extra OPTIONS request (a CORS preflight check) before
* each flag request, which could affect performance.
*/

@@ -259,2 +263,27 @@ sendLDHeaders?: boolean;

autoAliasingOptOut?: boolean;
/**
* Information about the application where the LaunchDarkly SDK is running.
*/
application?: {
/**
* A unique identifier representing the application where the LaunchDarkly SDK is running.
*
* This can be specified as any string value as long as it only uses the following characters: ASCII letters,
* ASCII digits, period, hyphen, underscore. A string containing any other characters will be ignored.
*
* Example: `authentication-service`
*/
id?: string;
/**
* A unique identifier representing the version of the application where the LaunchDarkly SDK is running.
*
* This can be specified as any string value as long as it only uses the following characters: ASCII letters,
* ASCII digits, period, hyphen, underscore. A string containing any other characters will be ignored.
*
* Example: `1.0.0` (standard version string) or `abcdef` (sha prefix)
*/
version?: string;
}
}

@@ -261,0 +290,0 @@

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc