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

@segment/analytics.js-integration-segmentio

Package Overview
Dependencies
Maintainers
113
Versions
64
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@segment/analytics.js-integration-segmentio - npm Package Compare versions

Comparing version 4.0.0 to 4.1.1

.circleci/config.yml

58

karma.conf.ci.js

@@ -36,19 +36,7 @@ /* eslint-env node */

},
// FIXME(ndhoule): Bad IE7/8 support in testing packages make these fail
// sl_ie_7: {
// base: 'SauceLabs',
// browserName: 'internet explorer',
// version: '7'
// },
// sl_ie_8: {
// base: 'SauceLabs',
// browserName: 'internet explorer',
// version: '8'
// },
// FIXME(): IE9 tests are failing pre-Browserification
// sl_ie_9: {
// base: 'SauceLabs',
// browserName: 'internet explorer',
// version: '9'
// },
sl_ie_9: {
base: 'SauceLabs',
browserName: 'internet explorer',
version: '9'
},
sl_ie_10: {

@@ -74,33 +62,39 @@ base: 'SauceLabs',

if (!process.env.SAUCE_USERNAME || !process.env.SAUCE_ACCESS_KEY) {
throw new Error('SAUCE_USERNAME and SAUCE_ACCESS_KEY environment variables are required but are missing');
throw new Error(
'SAUCE_USERNAME and SAUCE_ACCESS_KEY environment variables are required but are missing'
);
}
config.set({
browserDisconnectTolerance: 2,
browserDisconnectTimeout: 30000,
browserNoActivityTimeout: 30000,
browserDisconnectTolerance: 1,
browserDisconnectTimeout: 60000,
browserNoActivityTimeout: 60000,
singleRun: true,
reporters: ['progress', 'junit', 'coverage'],
concurrency: 2,
browsers: ['PhantomJS'].concat(Object.keys(customLaunchers)),
retryLimit: 5,
customLaunchers: customLaunchers,
reporters: ['spec', 'summary', 'junit'],
specReporter: {
suppressPassed: true
},
junitReporter: {
outputDir: process.env.TEST_REPORTS_DIR,
outputDir: 'junit-reports',
suite: require('./package.json').name
},
browsers: ['PhantomJS'].concat(Object.keys(customLaunchers)),
customLaunchers: customLaunchers,
sauceLabs: {
testName: require('./package.json').name
},
coverageReporter: {
reporters: [
{ type: 'lcov' }
]
}
});
};
};
/* eslint-env node */
/* eslint-disable no-restricted-globals */
'use strict';
// 10 minutes
var TEST_TIMEOUT = 10 * 60 * 1000;
module.exports = function(config) {

@@ -10,5 +12,7 @@ config.set({

],
browsers: ['PhantomJS'],
singleRun: true,
middleware: ['server'],

@@ -51,3 +55,3 @@

reporters: ['spec', 'coverage'],
reporters: ['spec'],

@@ -58,7 +62,8 @@ preprocessors: {

browserNoActivityTimeout: TEST_TIMEOUT,
client: {
mocha: {
grep: process.env.GREP,
reporter: 'html',
timeout: 10000
timeout: TEST_TIMEOUT
}

@@ -68,23 +73,5 @@ },

browserify: {
debug: true,
transform: [
[
'browserify-istanbul',
{
instrumenterConfig: {
embedSource: true
}
}
]
]
},
coverageReporter: {
reporters: [
{ type: 'text' },
{ type: 'html' },
{ type: 'json' }
]
debug: true
}
});
};
};

@@ -64,2 +64,3 @@ 'use strict';

.option('deleteCrossDomainId', false)
.option('saveCrossDomainIdInLocalStorage', false)
.option('retryQueue', true)

@@ -173,12 +174,2 @@ .option('addBundledMetadata', false)

// Migrate from old cross domain id cookie names
if (this.cookie('segment_cross_domain_id')) {
this.cookie('seg_xid', this.cookie('segment_cross_domain_id'));
this.cookie('seg_xid_fd', this.cookie('segment_cross_domain_id_from_domain'));
this.cookie('seg_xid_ts', this.cookie('segment_cross_domain_id_timestamp'));
this.cookie('segment_cross_domain_id', null);
this.cookie('segment_cross_domain_id_from_domain', null);
this.cookie('segment_cross_domain_id_timestamp', null);
}
// Delete cross domain identifiers.

@@ -289,3 +280,3 @@ this.deleteCrossDomainIdIfNeeded();

if (!ctx.library) ctx.library = { name: 'analytics.js', version: this.analytics.VERSION };
var crossDomainId = this.cookie('seg_xid');
var crossDomainId = this.getCachedCrossDomainId();
if (crossDomainId && this.isCrossDomainAnalyticsEnabled()) {

@@ -446,2 +437,3 @@ if (!ctx.traits) {

if (!this.isCrossDomainAnalyticsEnabled()) {
// Callback is only provided in tests.
if (callback) {

@@ -452,60 +444,122 @@ callback('crossDomainId not enabled', null);

}
if (!this.cookie('seg_xid')) {
var self = this;
var writeKey = this.options.apiKey;
// Exclude the current domain from the list of servers we're querying
var currentTld = getTld(window.location.hostname);
var domains = [];
for (var i=0; i<this.options.crossDomainIdServers.length; i++) {
var domain = this.options.crossDomainIdServers[i];
if (getTld(domain) !== currentTld) {
domains.push(domain);
}
var cachedCrossDomainId = this.getCachedCrossDomainId();
if (cachedCrossDomainId) {
// Callback is only provided in tests.
if (callback) {
callback(null, {
crossDomainId: cachedCrossDomainId
});
}
return;
}
getCrossDomainIdFromServerList(domains, writeKey, function(err, res) {
if (err) {
// We optimize for no conflicting xid as much as possible. So bail out if there is an
// error and we cannot be sure that xid does not exist on any other domains
if (callback) {
callback(err, null);
}
return;
}
var crossDomainId = null;
var fromDomain = null;
if (res) {
crossDomainId = res.id;
fromDomain = res.domain;
} else {
crossDomainId = uuid();
fromDomain = window.location.hostname;
}
var currentTimeMillis = (new Date()).getTime();
self.cookie('seg_xid', crossDomainId);
// Not actively used. Saving for future conflict resolution purposes
self.cookie('seg_xid_fd', fromDomain);
self.cookie('seg_xid_ts', currentTimeMillis);
self.analytics.identify({
crossDomainId: crossDomainId
});
var self = this;
var writeKey = this.options.apiKey;
// Exclude the current domain from the list of servers we're querying
var currentTld = getTld(window.location.hostname);
var domains = [];
for (var i = 0; i < this.options.crossDomainIdServers.length; i++) {
var domain = this.options.crossDomainIdServers[i];
if (getTld(domain) !== currentTld) {
domains.push(domain);
}
}
getCrossDomainIdFromServerList(domains, writeKey, function(err, res) {
if (err) {
// Callback is only provided in tests.
if (callback) {
callback(null, {
crossDomainId: crossDomainId,
fromDomain: fromDomain,
timestamp: currentTimeMillis
});
callback(err, null);
}
// We optimize for no conflicting xid as much as possible. So bail out if there is an
// error and we cannot be sure that xid does not exist on any other domains.
return;
}
var crossDomainId = null;
var fromDomain = null;
if (res) {
crossDomainId = res.id;
fromDomain = res.domain;
} else {
crossDomainId = uuid();
fromDomain = window.location.hostname;
}
self.saveCrossDomainId(crossDomainId);
self.analytics.identify({
crossDomainId: crossDomainId
});
// Callback is only provided in tests.
if (callback) {
callback(null, {
crossDomainId: crossDomainId,
fromDomain: fromDomain
});
}
});
};
/**
* getCachedCrossDomainId returns the cross domain identifier stored on the client based on the `saveCrossDomainIdInLocalStorage` flag.
* If `saveCrossDomainIdInLocalStorage` is false, it reads it from the `seg_xid` cookie.
* If `saveCrossDomainIdInLocalStorage` is true, it reads it from the `seg_xid` key in localStorage.
*
* @return {string} crossDomainId
*/
Segment.prototype.getCachedCrossDomainId = function() {
if (this.options.saveCrossDomainIdInLocalStorage) {
return localstorage('seg_xid');
}
return this.cookie('seg_xid');
};
/**
* saveCrossDomainId saves the cross domain identifier. The implementation differs based on the `saveCrossDomainIdInLocalStorage` flag.
* If `saveCrossDomainIdInLocalStorage` is false, it saves it as the `seg_xid` cookie.
* If `saveCrossDomainIdInLocalStorage` is true, it saves it to localStorage (so that it can be accessed on the current domain)
* and as a httpOnly cookie (so that can it can be provided to other domains).
*
* @api private
*/
Segment.prototype.saveCrossDomainId = function(crossDomainId) {
if (!this.options.saveCrossDomainIdInLocalStorage) {
this.cookie('seg_xid', crossDomainId);
return;
}
var self = this;
// Save the cookie by making a request to the xid server for the current domain.
var currentTld = getTld(window.location.hostname);
for (var i = 0; i < this.options.crossDomainIdServers.length; i++) {
var domain = this.options.crossDomainIdServers[i];
if (getTld(domain) === currentTld) {
var writeKey = this.options.apiKey;
var url = 'https://' + domain + '/v1/saveId?writeKey=' + writeKey + '&xid=' + crossDomainId;
httpGet(url, function(err, res) {
if (err) {
self.debug('could not save id on %O, received %O', url, [err, res]);
return;
}
localstorage('seg_xid', crossDomainId);
});
return;
}
}
};
/**
* Deletes any state persisted by cross domain analytics.
* * seg_xid (and metadata) from cookies
* * seg_xid from localStorage
* * crossDomainId from traits in localStorage
*
* The deletion logic is run only if deletion is enabled for this project, and
* when either the seg_xid cookie or crossDomainId localStorage trait exists.
* The deletion logic is run only if deletion is enabled for this project, and only
* deletes the data that actually exists.
*

@@ -527,2 +581,7 @@ * @api private

// Delete the xid from localStorage if it exists.
if (localstorage('seg_xid')) {
localstorage('seg_xid', null);
}
// Delete the crossDomainId trait in localStorage if it exists.

@@ -619,2 +678,23 @@ if (this.analytics.user().traits().crossDomainId) {

/**
* get makes a get request to the given URL.
* @param {string} url
* @param {function} callback => err, response
*/
function httpGet(url, callback) {
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.withCredentials = true;
xhr.onreadystatechange = function() {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status >= 200 && xhr.status < 300) {
callback(null, xhr.responseText);
} else {
callback(xhr.statusText || xhr.responseText || 'Unknown Error', null);
}
}
};
xhr.send();
}
/**
* getTld

@@ -621,0 +701,0 @@ * Get domain.com from subdomain.domain.com, etc.

{
"name": "@segment/analytics.js-integration-segmentio",
"description": "The Segmentio analytics.js integration.",
"version": "4.0.0",
"version": "4.1.1",
"keywords": [

@@ -13,3 +13,6 @@ "analytics.js",

"scripts": {
"test": "make test"
"test": "make test",
"np": "np --no-publish",
"cz": "git-cz",
"commitmsg": "commitlint -E GIT_PARAMS"
},

@@ -44,2 +47,4 @@ "repository": {

"devDependencies": {
"@commitlint/cli": "^8.0.0",
"@commitlint/config-conventional": "^8.0.0",
"@segment/analytics.js-core": "^3.8.0",

@@ -51,2 +56,6 @@ "@segment/analytics.js-integration-tester": "^2.0.0",

"browserify-istanbul": "^2.0.0",
"codecov": "^3.5.0",
"commitizen": "^3.1.1",
"commitlint-circle": "^1.0.0",
"cz-conventional-changelog": "^2.1.0",
"eslint": "^2.9.0",

@@ -67,2 +76,3 @@ "eslint-plugin-mocha": "^2.2.0",

"mocha": "^2.2.5",
"np": "^3.0.4",
"npm-check": "^5.2.1",

@@ -72,4 +82,23 @@ "phantomjs-prebuilt": "^2.1.7",

"sinon": "^1.17.4",
"snyk": "^1.177.0",
"watchify": "^3.7.0"
},
"commitlint": {
"extends": [
"@commitlint/config-conventional"
]
},
"lint-staged": {
"linters": {
"*.{js,json,md}": [
"prettier-eslint --write",
"git add"
]
}
},
"config": {
"commitizen": {
"path": "cz-conventional-changelog"
}
}
}

@@ -47,2 +47,5 @@ 'use strict';

resetCookies();
if (window.localStorage) {
window.localStorage.clear();
}
});

@@ -54,2 +57,5 @@

resetCookies();
if (window.localStorage) {
window.localStorage.clear();
}
segment.reset();

@@ -888,10 +894,2 @@ sandbox();

it('should migrate cookies from old to new name', function() {
segment.cookie('segment_cross_domain_id', 'xid-test-1');
segment.initialize();
analytics.assert(segment.cookie('segment_cross_domain_id') == null);
analytics.assert(segment.cookie('seg_xid') === 'xid-test-1');
});
it('should not crash with invalid config', function() {

@@ -911,145 +909,233 @@ segment.options.crossDomainIdServers = undefined;

it('should generate xid locally if there is only one (current hostname) server', function() {
it('should use cached cross domain identifier from LS when saveCrossDomainIdInLocalStorage is true', function() {
segment.options.crossDomainIdServers = [
'localhost'
];
segment.options.saveCrossDomainIdInLocalStorage = true;
store('seg_xid', 'test_xid_cache_ls');
var res = null;
segment.retrieveCrossDomainId(function(err, response) {
var err = null;
segment.retrieveCrossDomainId(function(error, response) {
res = response;
err = error;
});
var identify = segment.onidentify.args[0];
var crossDomainId = identify[0].traits().crossDomainId;
analytics.assert(crossDomainId);
analytics.assert(res.crossDomainId === crossDomainId);
analytics.assert(res.fromDomain === 'localhost');
assert.isNull(err);
assert.deepEqual(res, {
crossDomainId: 'test_xid_cache_ls'
});
});
it('should obtain crossDomainId', function() {
it('should use cached cross domain identifier from cookies when saveCrossDomainIdInLocalStorage is false', function() {
segment.options.crossDomainIdServers = [
'localhost'
];
segment.options.saveCrossDomainIdInLocalStorage = false;
segment.cookie('seg_xid', 'test_xid_cache_cookie');
var res = null;
segment.retrieveCrossDomainId(function(err, response) {
var err = null;
segment.retrieveCrossDomainId(function(error, response) {
res = response;
err = error;
});
server.respondWith('GET', 'https://xid.domain2.com/v1/id/' + segment.options.apiKey, [
200,
{ 'Content-Type': 'application/json' },
'{ "id": "xdomain-id-1" }'
]);
server.respond();
var identify = segment.onidentify.args[0];
analytics.assert(identify[0].traits().crossDomainId === 'xdomain-id-1');
analytics.assert(res.crossDomainId === 'xdomain-id-1');
analytics.assert(res.fromDomain === 'xid.domain2.com');
assert.isNull(err);
assert.deepEqual(res, {
crossDomainId: 'test_xid_cache_cookie'
});
});
it('should generate crossDomainId if no server has it', function() {
var res = null;
segment.retrieveCrossDomainId(function(err, response) {
res = response;
describe('getCachedCrossDomainId', function() {
it('should return identifiers from localstorage when saveCrossDomainIdInLocalStorage is true', function() {
store('seg_xid', 'test_xid_cache_ls');
segment.cookie('seg_xid', 'test_xid_cache_cookie');
segment.options.saveCrossDomainIdInLocalStorage = true;
assert.equal(segment.getCachedCrossDomainId(), 'test_xid_cache_ls');
});
server.respondWith('GET', 'https://xid.domain2.com/v1/id/' + segment.options.apiKey, [
200,
{ 'Content-Type': 'application/json' },
'{ "id": null }'
]);
server.respondWith('GET', 'https://userdata.example1.com/v1/id/' + segment.options.apiKey, [
200,
{ 'Content-Type': 'application/json' },
'{ "id": null }'
]);
server.respond();
it('should return identifiers from localstorage when saveCrossDomainIdInLocalStorage is true', function() {
store('seg_xid', 'test_xid_cache_ls');
segment.cookie('seg_xid', 'test_xid_cache_cookie');
var identify = segment.onidentify.args[0];
var crossDomainId = identify[0].traits().crossDomainId;
analytics.assert(crossDomainId);
segment.options.saveCrossDomainIdInLocalStorage = false;
analytics.assert(res.crossDomainId === crossDomainId);
analytics.assert(res.fromDomain === 'localhost');
assert.equal(segment.getCachedCrossDomainId(), 'test_xid_cache_cookie');
});
});
it('should bail if all servers error', function() {
var err = null;
var res = null;
segment.retrieveCrossDomainId(function(error, response) {
err = error;
res = response;
});
var cases = {
'saveCrossDomainIdInLocalStorage true': true,
'saveCrossDomainIdInLocalStorage false': false
};
server.respondWith('GET', 'https://xid.domain2.com/v1/id/' + segment.options.apiKey, [
500,
{ 'Content-Type': 'application/json' },
''
]);
server.respondWith('GET', 'https://userdata.example1.com/v1/id/' + segment.options.apiKey, [
500,
{ 'Content-Type': 'application/json' },
''
]);
server.respond();
for (var scenario in cases) {
if (!cases.hasOwnProperty(scenario)) {
continue;
}
var identify = segment.onidentify.args[0];
analytics.assert(!identify);
analytics.assert(!res);
analytics.assert(err === 'Internal Server Error');
});
describe('with ' + scenario, function() {
it('should generate xid locally if there is only one (current hostname) server', function() {
segment.options.crossDomainIdServers = [
'localhost'
];
segment.options.saveCrossDomainIdInLocalStorage = cases[scenario];
it('should bail if some servers fail and others have no xid', function() {
var err = null;
var res = null;
segment.retrieveCrossDomainId(function(error, response) {
err = error;
res = response;
});
var res = null;
segment.retrieveCrossDomainId(function(err, response) {
res = response;
});
server.respondWith('GET', 'https://xid.domain2.com/v1/id/' + segment.options.apiKey, [
400,
{ 'Content-Type': 'application/json' },
''
]);
server.respondWith('GET', 'https://userdata.example1.com/v1/id/' + segment.options.apiKey, [
200,
{ 'Content-Type': 'application/json' },
'{ "id": null }'
]);
server.respond();
var identify = segment.onidentify.args[0];
var crossDomainId = identify[0].traits().crossDomainId;
analytics.assert(crossDomainId);
var identify = segment.onidentify.args[0];
analytics.assert(!identify);
analytics.assert(!res);
analytics.assert(err === 'Bad Request');
});
analytics.assert(res.crossDomainId === crossDomainId);
analytics.assert(res.fromDomain === 'localhost');
it('should succeed even if one server fails', function() {
var err = null;
var res = null;
segment.retrieveCrossDomainId(function(error, response) {
err = error;
res = response;
});
assert.equal(segment.getCachedCrossDomainId(), crossDomainId);
});
server.respondWith('GET', 'https://xid.domain2.com/v1/id/' + segment.options.apiKey, [
500,
{ 'Content-Type': 'application/json' },
''
]);
server.respondWith('GET', 'https://userdata.example1.com/v1/id/' + segment.options.apiKey, [
200,
{ 'Content-Type': 'application/json' },
'{ "id": "xidxid" }'
]);
server.respond();
it('should obtain crossDomainId', function() {
var res = null;
segment.retrieveCrossDomainId(function(err, response) {
res = response;
});
server.respondWith('GET', 'https://xid.domain2.com/v1/id/' + segment.options.apiKey, [
200,
{ 'Content-Type': 'application/json' },
'{ "id": "xdomain-id-1" }'
]);
server.respond();
var identify = segment.onidentify.args[0];
analytics.assert(identify[0].traits().crossDomainId === 'xidxid');
var identify = segment.onidentify.args[0];
analytics.assert(identify[0].traits().crossDomainId === 'xdomain-id-1');
analytics.assert(res.crossDomainId === 'xidxid');
analytics.assert(res.fromDomain === 'userdata.example1.com');
analytics.assert(!err);
});
analytics.assert(res.crossDomainId === 'xdomain-id-1');
analytics.assert(res.fromDomain === 'xid.domain2.com');
assert.equal(segment.getCachedCrossDomainId(), 'xdomain-id-1');
});
it('should generate crossDomainId if no server has it', function() {
var res = null;
segment.retrieveCrossDomainId(function(err, response) {
res = response;
});
server.respondWith('GET', 'https://xid.domain2.com/v1/id/' + segment.options.apiKey, [
200,
{ 'Content-Type': 'application/json' },
'{ "id": null }'
]);
server.respondWith('GET', 'https://userdata.example1.com/v1/id/' + segment.options.apiKey, [
200,
{ 'Content-Type': 'application/json' },
'{ "id": null }'
]);
server.respond();
var identify = segment.onidentify.args[0];
var crossDomainId = identify[0].traits().crossDomainId;
analytics.assert(crossDomainId);
analytics.assert(res.crossDomainId === crossDomainId);
analytics.assert(res.fromDomain === 'localhost');
assert.equal(segment.getCachedCrossDomainId(), crossDomainId);
});
it('should bail if all servers error', function() {
var err = null;
var res = null;
segment.retrieveCrossDomainId(function(error, response) {
err = error;
res = response;
});
server.respondWith('GET', 'https://xid.domain2.com/v1/id/' + segment.options.apiKey, [
500,
{ 'Content-Type': 'application/json' },
''
]);
server.respondWith('GET', 'https://userdata.example1.com/v1/id/' + segment.options.apiKey, [
500,
{ 'Content-Type': 'application/json' },
''
]);
server.respond();
var identify = segment.onidentify.args[0];
analytics.assert(!identify);
analytics.assert(!res);
analytics.assert(err === 'Internal Server Error');
assert.equal(segment.getCachedCrossDomainId(), null);
});
it('should bail if some servers fail and others have no xid', function() {
var err = null;
var res = null;
segment.retrieveCrossDomainId(function(error, response) {
err = error;
res = response;
});
server.respondWith('GET', 'https://xid.domain2.com/v1/id/' + segment.options.apiKey, [
400,
{ 'Content-Type': 'application/json' },
''
]);
server.respondWith('GET', 'https://userdata.example1.com/v1/id/' + segment.options.apiKey, [
200,
{ 'Content-Type': 'application/json' },
'{ "id": null }'
]);
server.respond();
var identify = segment.onidentify.args[0];
analytics.assert(!identify);
analytics.assert(!res);
analytics.assert(err === 'Bad Request');
assert.equal(segment.getCachedCrossDomainId(), null);
});
it('should succeed even if one server fails', function() {
var err = null;
var res = null;
segment.retrieveCrossDomainId(function(error, response) {
err = error;
res = response;
});
server.respondWith('GET', 'https://xid.domain2.com/v1/id/' + segment.options.apiKey, [
500,
{ 'Content-Type': 'application/json' },
''
]);
server.respondWith('GET', 'https://userdata.example1.com/v1/id/' + segment.options.apiKey, [
200,
{ 'Content-Type': 'application/json' },
'{ "id": "xidxid" }'
]);
server.respond();
var identify = segment.onidentify.args[0];
analytics.assert(identify[0].traits().crossDomainId === 'xidxid');
analytics.assert(res.crossDomainId === 'xidxid');
analytics.assert(res.fromDomain === 'userdata.example1.com');
analytics.assert(!err);
assert.equal(segment.getCachedCrossDomainId(), 'xidxid');
});
});
}
describe('isCrossDomainAnalyticsEnabled', function() {

@@ -1128,2 +1214,4 @@ it('should return false when crossDomainIdServers is undefined', function() {

segment.cookie('seg_xid_fd', 'test_xid_fd');
store('seg_xid', 'test_xid');
analytics.identify({

@@ -1138,2 +1226,3 @@ crossDomainId: 'test_xid'

assert.equal(segment.cookie('seg_xid_fd'), null);
assert.equal(store('seg_xid'), null);
assert.equal(analytics.user().traits().crossDomainId, null);

@@ -1193,5 +1282,2 @@ });

xhr = sinon.useFakeXMLHttpRequest();
if (window.localStorage) {
window.localStorage.clear();
}
analytics.once('ready', done);

@@ -1198,0 +1284,0 @@ segment.options.retryQueue = true;

Sorry, the diff of this file is not supported yet

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