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

connect-redis

Package Overview
Dependencies
Maintainers
3
Versions
69
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

connect-redis - npm Package Compare versions

Comparing version 3.4.2 to 4.0.0

.prettierrc

2

index.js

@@ -1,1 +0,1 @@

module.exports = require('./lib/connect-redis');
module.exports = require('./lib/connect-redis')

@@ -7,370 +7,155 @@ /*!

var debug = require('debug')('connect:redis');
var util = require('util');
var noop = function(){};
module.exports = function(session) {
const Store = session.Store
/**
* One day in seconds.
*/
// All callbacks should have a noop if none provided for compatibility
// with the most Redis clients.
const noop = () => {}
var oneDay = 86400;
class RedisStore extends Store {
constructor(options = {}) {
super(options)
if (!options.client) {
throw new Error('A client must be directly provided to the RedisStore')
}
function getTTL(store, sess, sid) {
if (typeof store.ttl === 'number' || typeof store.ttl === 'string') return store.ttl;
if (typeof store.ttl === 'function') return store.ttl(store, sess, sid);
if (store.ttl) throw new TypeError('`store.ttl` must be a number or function.');
this.prefix = options.prefix == null ? 'sess:' : options.prefix
this.scanCount = Number(options.scanCount) || 100
this.serializer = options.serializer || JSON
this.client = options.client
this.ttl = options.ttl || 86400 // One day in seconds.
this.disableTouch = options.disableTouch || false
}
var maxAge = sess.cookie.maxAge;
return (typeof maxAge === 'number'
? Math.floor(maxAge / 1000)
: oneDay);
}
get(sid, cb = noop) {
let key = this.prefix + sid
/**
* Return the `RedisStore` extending `express`'s session Store.
*
* @param {object} express session
* @return {Function}
* @api public
*/
this.client.get(key, (er, data) => {
if (er) return cb(er)
if (!data) return cb()
module.exports = function (session) {
let result
try {
result = this.serializer.parse(data)
} catch (er) {
return cb(er)
}
return cb(null, result)
})
}
/**
* Express's session Store.
*/
set(sid, sess, cb = noop) {
let args = [this.prefix + sid]
var Store = session.Store;
let value
try {
value = this.serializer.stringify(sess)
} catch (er) {
return cb(er)
}
args.push(value)
args.push('EX', this._getTTL(sess))
/**
* Initialize RedisStore with the given `options`.
*
* @param {Object} options
* @api public
*/
function RedisStore (options) {
if (!(this instanceof RedisStore)) {
throw new TypeError('Cannot call RedisStore constructor as a function');
this.client.set(args, cb)
}
var self = this;
touch(sid, sess, cb = noop) {
if (this.disableTouch) return cb()
options = options || {};
Store.call(this, options);
this.prefix = options.prefix == null
? 'sess:'
: options.prefix;
delete options.prefix;
this.scanCount = Number(options.scanCount) || 100;
delete options.scanCount;
this.serializer = options.serializer || JSON;
if (options.url) {
options.socket = options.url;
// Since we need to update the expires value on the cookie,
// we update the whole session object.
this.set(sid, sess, cb)
}
// convert to redis connect params
if (options.client) {
this.client = options.client;
} else {
var redis = require('redis');
if (options.socket) {
this.client = redis.createClient(options.socket, options);
}
else {
this.client = redis.createClient(options);
}
destroy(sid, cb = noop) {
let key = this.prefix + sid
this.client.del(key, cb)
}
// logErrors
if(options.logErrors){
// if options.logErrors is function, allow it to override. else provide default logger. useful for large scale deployment
// which may need to write to a distributed log
if(typeof options.logErrors != 'function'){
options.logErrors = function (err) {
console.error('Warning: connect-redis reported a client error: ' + err);
};
}
this.client.on('error', options.logErrors);
clear(cb = noop) {
this._getAllKeys((err, keys) => {
if (err) return cb(err)
this.client.del(keys, cb)
})
}
if (options.pass) {
this.client.auth(options.pass, function (err) {
if (err) {
throw err;
}
});
length(cb = noop) {
this._getAllKeys((err, keys) => {
if (err) return cb(err)
return cb(null, keys.length)
})
}
this.ttl = options.ttl;
this.disableTTL = options.disableTTL;
ids(cb = noop) {
let prefixLen = this.prefix.length
if (options.unref) this.client.unref();
if ('db' in options) {
if (typeof options.db !== 'number') {
console.error('Warning: connect-redis expects a number for the "db" option');
}
self.client.select(options.db);
self.client.on('connect', function () {
self.client.select(options.db);
});
this._getAllKeys((err, keys) => {
if (err) return cb(err)
keys = keys.map(key => key.substr(prefixLen))
return cb(null, keys)
})
}
self.client.on('error', function (er) {
debug('Redis returned err', er);
self.emit('disconnect', er);
});
all(cb = noop) {
let prefixLen = this.prefix.length
self.client.on('connect', function () {
self.emit('connect');
});
}
this._getAllKeys((err, keys) => {
if (err) return cb(err)
if (keys.length === 0) return cb(null, [])
/**
* Inherit from `Store`.
*/
this.client.mget(keys, (err, sessions) => {
if (err) return cb(err)
util.inherits(RedisStore, Store);
let result
try {
result = sessions.map((data, index) => {
data = this.serializer.parse(data)
data.id = keys[index].substr(prefixLen)
return data
})
} catch (e) {
err = e
}
return cb(err, result)
})
})
}
/**
* Attempt to fetch session by the given `sid`.
*
* @param {String} sid
* @param {Function} fn
* @api public
*/
RedisStore.prototype.get = function (sid, fn) {
var store = this;
var psid = store.prefix + sid;
if (!fn) fn = noop;
debug('GET "%s"', sid);
store.client.get(psid, function (er, data) {
if (er) return fn(er);
if (!data) return fn();
var result;
data = data.toString();
debug('GOT %s', data);
try {
result = store.serializer.parse(data);
_getTTL(sess) {
let ttl
if (sess && sess.cookie && sess.cookie.expires) {
let ms = Number(new Date(sess.cookie.expires)) - Date.now()
ttl = Math.ceil(ms / 1000)
} else {
ttl = this.ttl
}
catch (er) {
return fn(er);
}
return fn(null, result);
});
};
/**
* Commit the given `sess` object associated with the given `sid`.
*
* @param {String} sid
* @param {Session} sess
* @param {Function} fn
* @api public
*/
RedisStore.prototype.set = function (sid, sess, fn) {
var store = this;
var args = [store.prefix + sid];
if (!fn) fn = noop;
try {
var jsess = store.serializer.stringify(sess);
return ttl
}
catch (er) {
return fn(er);
}
args.push(jsess);
if (!store.disableTTL) {
var ttl = getTTL(store, sess, sid);
args.push('EX', ttl);
debug('SET "%s" %s ttl:%s', sid, jsess, ttl);
} else {
debug('SET "%s" %s', sid, jsess);
_getAllKeys(cb = noop) {
let pattern = this.prefix + '*'
this._scanKeys({}, 0, pattern, this.scanCount, cb)
}
store.client.set(args, function (er) {
if (er) return fn(er);
debug('SET complete');
fn.apply(null, arguments);
});
};
_scanKeys(keys = {}, cursor, pattern, count, cb = noop) {
let args = [cursor, 'match', pattern, 'count', count]
this.client.scan(args, (err, data) => {
if (err) return cb(err)
/**
* Destroy the session associated with the given `sid`.
*
* @param {String} sid
* @api public
*/
let [nextCursorId, scanKeys] = data
for (let key of scanKeys) {
keys[key] = true
}
RedisStore.prototype.destroy = function (sid, fn) {
debug('DEL "%s"', sid);
if (!fn) fn = noop;
// This can be a string or a number. We check both.
if (Number(nextCursorId) !== 0) {
return this._scanKeys(keys, nextCursorId, pattern, count, cb)
}
if (Array.isArray(sid)) {
var multi = this.client.multi();
var prefix = this.prefix;
sid.forEach(function (s) {
multi.del(prefix + s);
});
multi.exec(fn);
} else {
sid = this.prefix + sid;
this.client.del(sid, fn);
cb(null, Object.keys(keys))
})
}
};
/**
* Refresh the time-to-live for the session with the given `sid`.
*
* @param {String} sid
* @param {Session} sess
* @param {Function} fn
* @api public
*/
RedisStore.prototype.touch = function (sid, sess, fn) {
var store = this;
var psid = store.prefix + sid;
if (!fn) fn = noop;
if (store.disableTTL) return fn();
var ttl = getTTL(store, sess);
debug('EXPIRE "%s" ttl:%s', sid, ttl);
store.client.expire(psid, ttl, function (er) {
if (er) return fn(er);
debug('EXPIRE complete');
fn.apply(this, arguments);
});
};
/**
* Fetch all sessions' Redis keys using non-blocking SCAN command
*
* @param {Function} fn
* @api private
*/
function allKeys (store, cb) {
var keysObj = {}; // Use an object to dedupe as scan can return duplicates
var pattern = store.prefix + '*';
var scanCount = store.scanCount;
debug('SCAN "%s"', pattern);
(function nextBatch (cursorId) {
store.client.scan(cursorId, 'match', pattern, 'count', scanCount, function (err, result) {
if (err) return cb(err);
var nextCursorId = result[0];
var keys = result[1];
debug('SCAN complete (next cursor = "%s")', nextCursorId);
keys.forEach(function (key) {
keysObj[key] = 1;
});
if (nextCursorId != 0) {
// next batch
return nextBatch(nextCursorId);
}
// end of cursor
return cb(null, Object.keys(keysObj));
});
})(0);
}
/**
* Fetch all sessions' ids
*
* @param {Function} fn
* @api public
*/
RedisStore.prototype.ids = function (fn) {
var store = this;
var prefixLength = store.prefix.length;
if (!fn) fn = noop;
allKeys(store, function (err, keys) {
if (err) return fn(err);
keys = keys.map(function (key) {
return key.substr(prefixLength);
});
return fn(null, keys);
});
};
/**
* Fetch count of all sessions
*
* @param {Function} fn
* @api public
*/
RedisStore.prototype.length = function (fn) {
var store = this;
if (!fn) fn = noop;
allKeys(store, function (err, keys) {
if (err) return fn(err);
return fn(null, keys.length);
});
};
/**
* Fetch all sessions
*
* @param {Function} fn
* @api public
*/
RedisStore.prototype.all = function (fn) {
var store = this;
var prefixLength = store.prefix.length;
if (!fn) fn = noop;
allKeys(store, function (err, keys) {
if (err) return fn(err);
if (keys.length === 0) return fn(null,[]);
store.client.mget(keys, function (err, sessions) {
if (err) return fn(err);
var result;
try {
result = sessions.map(function (data, index) {
data = data.toString();
data = store.serializer.parse(data);
data.id = keys[index].substr(prefixLength);
return data;
});
} catch (e) {
err = e;
}
return fn(err, result);
});
});
};
return RedisStore;
};
return RedisStore
}
{
"name": "connect-redis",
"description": "Redis session store for Connect",
"version": "3.4.2",
"version": "4.0.0",
"author": "TJ Holowaychuk <tj@vision-media.ca>",

@@ -15,5 +15,6 @@ "contributors": [

},
"dependencies": {
"debug": "^4.1.1",
"redis": "^2.8.0"
"peerDependencies": {
"ioredis": "^4.10.0",
"redis": "^2.8.0",
"redis-mock": "^0.46.0"
},

@@ -23,11 +24,14 @@ "devDependencies": {

"bluebird": "^3.5.5",
"eslint": "^3.19.0",
"eslint": "^6.2.2",
"eslint-config-prettier": "^6.1.0",
"eslint-plugin-prettier": "^3.1.0",
"express-session": "^1.16.2",
"ioredis": "^4.10.0",
"istanbul": "^0.4.5",
"sinon": "^2.3.4",
"tape": "^4.2.1"
"nyc": "^14.1.1",
"prettier": "^1.18.2",
"redis": "^2.8.0",
"redis-mock": "^0.46.0"
},
"engines": {
"node": "*"
"node": ">=8.0.0"
},

@@ -38,7 +42,6 @@ "bugs": {

"scripts": {
"test": "istanbul cover tape \"test/*-test.js\"",
"test-debug": "DEBUG=* istanbul cover tape \"test/*-test.js\"",
"bench": "node bench/redisbench.js",
"lint": "eslint index.js test lib bench"
"test": "nyc tape \"test/*-test.js\"",
"lint": "eslint index.js test lib",
"fmt": "prettier --write \"**/*.{js,md,json,*rc}\""
}
}

@@ -1,212 +0,144 @@

/* eslint-env es6 */
var test = require('blue-tape');
var redisSrv = require('./redis-server');
var session = require('express-session');
var RedisStore = require('../')(session);
var redis = require('redis');
var ioRedis = require('ioredis');
var sinon = require('sinon');
var P = require('bluebird');
const test = require('blue-tape')
const redisSrv = require('../test/redis-server')
const session = require('express-session')
const redis = require('redis')
const ioRedis = require('ioredis')
const redisMock = require('redis-mock')
var lifecycleTest = P.coroutine(function *(store, t) {
P.promisifyAll(store);
let RedisStore = require('../')(session)
var ok = yield store.setAsync('123', { cookie: { maxAge: 2000 }, name: 'tj' });
t.equal(ok, 'OK', '#set() ok');
let p = (ctx, method) => (...args) =>
new Promise((resolve, reject) => {
ctx[method](...args, (err, d) => {
if (err) reject(err)
resolve(d)
})
})
var data = yield store.getAsync('123');
t.deepEqual({ cookie: { maxAge: 2000 }, name: 'tj' }, data, '#get() ok');
test('setup', redisSrv.connect)
ok = yield store.setAsync('123', { cookie: { maxAge: undefined }, name: 'tj' });
t.equal(ok, 'OK', '#set() no maxAge ok');
test('defaults', async t => {
t.throws(() => new RedisStore(), 'client is required')
data = yield store.allAsync();
t.deepEqual([{ id: '123', cookie: {}, name: 'tj' }], data, '#all() ok');
var client = redis.createClient(redisSrv.port, 'localhost')
var store = new RedisStore({ client })
data = yield store.idsAsync();
t.deepEqual(['123'], data, '#ids() ok');
t.equal(store.client, client, 'stores client')
t.equal(store.prefix, 'sess:', 'defaults to sess:')
t.equal(store.ttl, 86400, 'defaults to one day')
t.equal(store.scanCount, 100, 'defaults SCAN count to 100')
t.equal(store.serializer, JSON, 'defaults to JSON serialization')
t.equal(store.disableTouch, false, 'defaults to having `touch` enabled')
client.end(false)
})
data = yield store.lengthAsync();
t.deepEqual(1, data, '#length() ok');
test('node_redis', async t => {
var client = redis.createClient(redisSrv.port, 'localhost')
var store = new RedisStore({ client })
await lifecycleTest(store, t)
client.end(false)
})
ok = yield store.destroyAsync('123');
t.equal(ok, 1, '#destroy() ok');
test('ioredis', async t => {
var client = ioRedis.createClient(redisSrv.port, 'localhost')
var store = new RedisStore({ client })
await lifecycleTest(store, t)
client.disconnect()
})
store.client.end(false);
});
test('redis-mock client', async t => {
var client = redisMock.createClient()
var store = new RedisStore({ client })
await lifecycleTest(store, t)
})
test('setup', redisSrv.connect);
test('teardown', redisSrv.disconnect)
test('defaults', function (t) {
var store = new RedisStore();
t.equal(store.prefix, 'sess:', 'defaults to sess:');
t.notOk(store.ttl, 'ttl not set');
t.notOk(store.disableTTL, 'disableTTL not set');
t.ok(store.client, 'creates client');
async function lifecycleTest(store, t) {
let res = await p(store, 'set')('123', { foo: 'bar' })
t.equal(res, 'OK', 'set value')
store.client.end(false);
t.end();
});
res = await p(store, 'get')('123')
t.same(res, { foo: 'bar' }, 'get value')
test('basic', function (t) {
t.throws(RedisStore, TypeError, 'constructor not callable as function');
var store = new RedisStore({ port: redisSrv.port });
return lifecycleTest(store, t);
});
res = await p(store.client, 'ttl')('sess:123')
t.ok(res >= 86399, 'check one day ttl')
test('existing client', function (t) {
var client = redis.createClient(redisSrv.port, 'localhost');
var store = new RedisStore({ client: client });
return lifecycleTest(store, t);
});
let ttl = 60
let expires = new Date(Date.now() + ttl * 1000).toISOString()
res = await p(store, 'set')('456', { cookie: { expires } })
t.equal(res, 'OK', 'set cookie expires')
test('io redis client', function (t) {
var client = ioRedis.createClient(redisSrv.port, 'localhost');
var store = new RedisStore({ client: client });
return lifecycleTest(store, t).then(function () {
t.test('#destroy()', function (p) {
var spy = sinon.spy(ioRedis.prototype, 'sendCommand');
var sidName = 'randomname';
store.destroy(sidName);
p.deepEqual(
spy.firstCall.args[0].args,
[store.prefix + sidName]
);
spy.restore();
p.end();
});
});
});
res = await p(store.client, 'ttl')('sess:456')
t.ok(res <= 60, 'check expires ttl')
test('options', function (t) {
var store = new RedisStore({
host: 'localhost',
port: redisSrv.port,
prefix: 'tobi',
ttl: 1000,
disableTTL: true,
db: 1,
scanCount: 32,
unref: true,
pass: 'secret'
});
ttl = 90
expires = new Date(Date.now() + ttl * 1000).toISOString()
res = await p(store, 'touch')('456', { cookie: { expires } })
t.equal(res, 'OK', 'set cookie expires touch')
t.equal(store.prefix, 'tobi', 'uses provided prefix');
t.equal(store.ttl, 1000, 'ttl set');
t.ok(store.disableTTL, 'disableTTL set');
t.ok(store.client, 'creates client');
t.equal(store.client.address, 'localhost:'+redisSrv.port, 'sets host and port');
t.equal(store.scanCount, 32, 'sets scan count');
res = await p(store.client, 'ttl')('sess:456')
t.ok(res >= 60, 'check expires ttl touch')
var socketStore = new RedisStore({ socket: 'word' });
t.equal(socketStore.client.address, 'word', 'sets socket address');
socketStore.client.end(false);
res = await p(store, 'length')()
t.equal(res, 2, 'stored two keys length')
var urlStore = new RedisStore({ url: 'redis://127.0.0.1:8888' });
t.equal(urlStore.client.address, '127.0.0.1:8888', 'sets url address');
urlStore.client.end(false);
res = await p(store, 'ids')()
res.sort()
t.same(res, ['123', '456'], 'stored two keys ids')
var hostNoPort = new RedisStore({ host: 'host' });
t.equal(hostNoPort.client.address, 'host:6379', 'sets default port');
hostNoPort.client.end(false);
res = await p(store, 'all')()
res.sort((a, b) => (a.id > b.id ? 1 : -1))
t.same(
res,
[{ id: '123', foo: 'bar' }, { id: '456', cookie: { expires } }],
'stored two keys data'
)
return lifecycleTest(store, t);
});
res = await p(store, 'destroy')('456')
t.equal(res, 1, 'destroyed one')
test('ttl options', P.coroutine(function *(t) {
var store = new RedisStore({ port: redisSrv.port });
res = await p(store, 'length')()
t.equal(res, 1, 'one key remains')
var sid = '123';
var data, ok;
sinon.stub(store.client, 'set').callsArgWith(1, null, 'OK');
P.promisifyAll(store);
res = await p(store, 'clear')()
t.equal(res, 1, 'cleared remaining key')
// Basic (one day)
data = { cookie: {}, name: 'tj' };
ok = yield store.setAsync(sid, data);
t.equal(ok, 'OK', '#set() ok');
assertSetCalledWith(t, store, sid, data, ['EX', 86400]);
res = await p(store, 'length')()
t.equal(res, 0, 'no key remains')
// maxAge in cookie
data = { cookie: { maxAge: 2000 }, name: 'tj' };
ok = yield store.setAsync(sid, data);
t.equal(ok, 'OK', '#set() ok');
assertSetCalledWith(t, store, sid, data, ['EX', 2]);
let count = 1000
await load(store, count)
// Floors maxage
data = { cookie: { maxAge: 2500 }, name: 'tj' };
ok = yield store.setAsync(sid, data);
t.equal(ok, 'OK', '#set() ok');
assertSetCalledWith(t, store, sid, data, ['EX', 2]);
res = await p(store, 'length')()
t.equal(res, count, 'bulk count')
// store.disableTTL
store.disableTTL = true;
data = { cookie: {}, name: 'tj' };
ok = yield store.setAsync(sid, data);
t.equal(ok, 'OK', '#set() ok');
assertSetCalledWith(t, store, sid, data);
store.disableTTL = false;
res = await p(store, 'clear')()
t.equal(res, count, 'bulk clear')
}
// store.ttl: number
store.ttl = 50;
data = { cookie: {}, name: 'tj' };
ok = yield store.setAsync(sid, data);
t.equal(ok, 'OK', '#set() ok');
assertSetCalledWith(t, store, sid, data, ['EX', 50]);
store.ttl = null;
function load(store, count) {
return new Promise((resolve, reject) => {
let set = sid => {
store.set(
's' + sid,
{
cookie: { expires: new Date(Date.now() + 1000) },
data: 'some data',
},
err => {
if (err) {
return reject(err)
}
// store.ttl: function
store.ttl = sinon.stub().returns(200);
data = { cookie: {}, name: 'tj' };
ok = yield store.setAsync(sid, data);
t.equal(ok, 'OK', '#set() ok');
assertSetCalledWith(t, store, sid, data, ['EX', 200]);
t.ok(store.ttl.called, 'TTL fn was called');
t.deepEqual(store.ttl.firstCall.args, [store, data, sid]);
store.ttl = null;
if (sid === count) {
return resolve()
}
// store.ttl: string (invalid)
store.ttl = {};
data = { cookie: {}, name: 'tj' };
try {
ok = yield store.setAsync(sid, data);
t.ok(false, '#set() should throw with bad TTL');
} catch (e) {
t.ok(/must be a number or function/i.test(e.message), 'bad TTL type throws error');
}
store.ttl = null;
store.client.end(false);
}));
function assertSetCalledWith(t, store, sid, data, addl) {
var args = [store.prefix + sid, store.serializer.stringify(data)];
if (Array.isArray(addl)) args = args.concat(addl);
t.deepEqual(store.client.set.lastCall.args[0], args, '#.set() called with expected params');
set(sid + 1)
}
)
}
set(1)
})
}
test('interups', function (t) {
var store = P.promisifyAll(new RedisStore({ port: redisSrv.port, connect_timeout: 500 }));
return store.setAsync('123', { cookie: { maxAge: 2000 }, name: 'tj' })
.catch(function (er) {
t.ok(/broken/.test(er.message), 'failed connection');
store.client.end(false);
});
});
test('serializer', function (t) {
var serializer = {
stringify: function() { return 'XXX'+JSON.stringify.apply(JSON, arguments); },
parse: function(x) {
t.ok(x.match(/^XXX/));
return JSON.parse(x.substring(3));
}
};
t.equal(serializer.stringify('UnitTest'), 'XXX"UnitTest"');
t.equal(serializer.parse(serializer.stringify('UnitTest')), 'UnitTest');
var store = new RedisStore({ port: redisSrv.port, serializer: serializer });
return lifecycleTest(store, t);
});
test('teardown', redisSrv.disconnect);

@@ -1,17 +0,21 @@

var P = require('bluebird');
var spawn = require('child_process').spawn;
var redisSrv;
var port = exports.port = 18543;
const spawn = require('child_process').spawn
const port = (exports.port = 18543)
let redisSrv
exports.connect = function () {
redisSrv = spawn('redis-server', [
'--port', port,
'--loglevel', 'notice',
], { stdio: 'inherit' });
return P.delay(1500);
};
exports.connect = () =>
new Promise((resolve, reject) => {
redisSrv = spawn('redis-server', ['--port', port, '--loglevel', 'notice'], {
stdio: 'inherit',
})
exports.disconnect = function () {
redisSrv.kill('SIGKILL');
return P.resolve();
};
redisSrv.on('error', function(err) {
reject(new Error('Error caught spawning the server:' + err.message))
})
setTimeout(resolve, 1500)
})
exports.disconnect = function() {
redisSrv.kill('SIGKILL')
return Promise.resolve()
}

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