Socket
Socket
Sign inDemoInstall

graphql-advanced-projection

Package Overview
Dependencies
3
Maintainers
1
Versions
14
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 1.0.0 to 1.0.1

2

package.json
{
"name": "graphql-advanced-projection",
"version": "1.0.0",
"version": "1.0.1",
"description": "Fully customizable Mongoose/MongoDB projection generator.",

@@ -5,0 +5,0 @@ "main": "index.js",

const _ = require('lodash');
const logger = require('../logger');

@@ -11,127 +10,68 @@ const stripType = (typeRef) => {

const makePrefix = (prev, cur, subs) => {
let curr = cur;
if (curr === undefined) {
curr = subs;
}
if (!curr) {
return prev;
}
if (curr.startsWith('.')) {
return curr.substr(1);
}
return prev + curr;
};
function makeProjection(
root,
context,
prefix,
type,
) {
const { pick, info } = root;
logger.debug('Projecting type', type);
const cfg = (pick[type] || _.constant({}))(info);
const result = {};
const pf = makePrefix(prefix, cfg.prefix);
const proj = (reason, k) => {
if (_.isArray(k)) {
k.forEach((v) => {
logger.trace(`>${reason}`, pf + v);
result[pf + v] = 1;
});
return;
}
/* istanbul ignore else */
if (_.isString(k)) {
logger.trace(`>${reason}`, pf + k);
result[pf + k] = 1;
return;
}
/* istanbul ignore next */
throw new Error(`Proj not supported: ${k}`);
};
if (cfg.typeProj) {
proj('TypeProj', cfg.typeProj);
}
context.selectionSet.selections.forEach((sel) => {
const fieldName = _.get(sel, 'name.value');
switch (sel.kind) {
case 'Field': {
logger.debug('Projecting field', fieldName);
const def = _.get(cfg.proj, fieldName) || {};
if (def.query === undefined) {
proj('Default', fieldName);
} else if (def.query === null) {
logger.trace('>Ignored');
} else {
proj('Simple', def.query);
}
if (def.recursive && sel.selectionSet) {
const makeTraverser = ({ typeFunc, fieldFunc, stepFunc, reduceFunc }, seed) => (configs) => {
const { pick } = configs;
const func = (root, context, type) => (args) => {
const { info } = root;
const config = (pick[type] || _.constant({}))(info);
const cfgs = { configs, config, type };
const fieldResults = [];
const typeResult = typeFunc(cfgs, args);
context.selectionSet.selections.forEach((sel) => {
const field = _.get(sel, 'name.value');
switch (sel.kind) {
case 'Field': {
const typeRef = info.schema.getType(type);
/* istanbul ignore if */
if (!typeRef) {
/* istanbul ignore next */
throw new Error('Type not found', type);
}
logger.trace('typeRef', typeRef.toString());
const field = typeRef.getFields()[fieldName];
/* istanbul ignore if */
if (!field) {
/* istanbul ignore next */
throw new Error('Field not found', fieldName);
}
const nextTypeRef = field.type;
logger.trace('nextTypeRef', nextTypeRef.toString());
const core = stripType(nextTypeRef);
logger.trace('Recursive', core);
_.assign(result, makeProjection(
root,
sel,
makePrefix(pf, def.prefix, `${fieldName}.`),
core,
));
const next = stripType(typeRef.getFields()[field].type);
const recursion = sel.selectionSet ? func(root, sel, next) : undefined;
fieldResults.push(fieldFunc({
...cfgs,
field,
next,
}, args, recursion));
return;
}
return;
case 'InlineFragment': {
const newType = _.get(sel, 'typeCondition.name.value');
const next = newType || type;
const recursion = func(root, sel, next);
fieldResults.push(stepFunc({
...cfgs,
field,
next,
}, args, recursion));
return;
}
case 'FragmentSpread': {
const frag = info.fragments[field];
const next = _.get(frag, 'typeCondition.name.value');
const recursion = func(root, frag, next);
fieldResults.push(stepFunc({
...cfgs,
field,
next,
}, args, recursion));
return;
}
/* istanbul ignore next */
default:
/* istanbul ignore next */
throw new Error(`sel.kind not supported: ${sel.kind}`);
}
case 'InlineFragment': {
logger.debug('Projecting inline fragment');
const newType = _.get(sel, 'typeCondition.name.value');
const newPrefix = newType ? pf : prefix;
const core = newType || type;
logger.trace('Recursive', { type: core, prefix: newPrefix });
_.assign(result, makeProjection(
root,
sel,
newPrefix,
core,
));
return;
}
case 'FragmentSpread': {
logger.debug('Projecting fragment', fieldName);
const frag = info.fragments[fieldName];
const newType = _.get(frag, 'typeCondition.name.value');
const newPrefix = newType !== type ? pf : prefix;
logger.trace('Recursive', { type: newType, prefix: newPrefix });
_.assign(result, makeProjection(
root,
frag,
newPrefix,
newType,
));
return;
}
/* istanbul ignore next */
default:
/* istanbul ignore next */
throw new Error(`sel.kind not supported: ${sel.kind}`);
}
});
return result;
}
});
return reduceFunc(cfgs, typeResult, fieldResults);
};
return (info) => {
const context = info.fieldNodes[0];
const type = stripType(info.returnType);
return func(
{ info },
context,
type,
)(seed);
};
};
module.exports = {
stripType,
makeProjection,
makeTraverser,
};

@@ -5,5 +5,5 @@ const _ = require('lodash/fp');

function prepareProjectionConfig(def) {
function prepareProjectionConfig(def, fieldName) {
if (def === undefined) {
return {};
return { query: fieldName };
}

@@ -36,5 +36,5 @@ if (def === null) {

return {
query: def.query,
query: def.query === undefined ? fieldName : def.query,
select: def.select,
recursive: !!def.recursive,
recursive: def.recursive ? true : undefined,
prefix: def.prefix,

@@ -72,3 +72,3 @@ };

const ncfgs = _.compose(
_.mapValues(
_.mapValues.convert({ cap: false })(
(v) => (_.isArray(v)

@@ -78,7 +78,7 @@ ? _.compose(

_.update('[1].proj'),
_.mapValues,
_.mapValues.convert({ cap: false }),
)
: _.compose(
_.update('proj'),
_.mapValues,
_.mapValues.convert({ cap: false }),
)

@@ -85,0 +85,0 @@ )(prepareProjectionConfig)(v),

@@ -1,23 +0,93 @@

const _ = require('lodash/fp');
const { stripType, makeProjection } = require('./core');
const _ = require('lodash');
const { makeTraverser } = require('./core');
const logger = require('../logger');
module.exports.genProjection = ({ root, pick }) => (info) => {
try {
const context = info.fieldNodes[0];
const type = stripType(info.returnType);
const result = _.reduce(_.assign, {})([root, makeProjection(
{ pick, info },
context,
'',
type,
)]);
logger.debug('Project result', result);
const makePrefix = (prev, cur, subs) => {
let curr = cur;
if (curr === undefined) {
curr = subs;
}
if (!curr) {
return prev;
}
if (curr.startsWith('.')) {
return curr.substr(1);
}
return prev + curr;
};
const proj = (reason, pf, k) => {
const result = {};
if (_.isArray(k)) {
k.forEach((v) => {
logger.trace(`>${reason}`, pf + v);
result[pf + v] = 1;
});
return result;
} catch (e) {
/* istanbul ignore next */
logger.error('Projecting', e);
/* istanbul ignore next */
return undefined;
}
/* istanbul ignore else */
if (_.isString(k)) {
logger.trace(`>${reason}`, pf + k);
result[pf + k] = 1;
return result;
}
/* istanbul ignore next */
throw new Error(`Proj not supported: ${k}`);
};
const makeProjection = makeTraverser({
typeFunc({ config }, [prefix]) {
if (config.typeProj) {
const pf = makePrefix(prefix, config.prefix);
return proj('TypeProj', pf, config.typeProj);
}
return {};
},
fieldFunc({ config, field }, [prefix], recursion) {
let result;
logger.debug('Projecting field', field);
const def = _.get(config.proj, field) || { query: field };
const pf = makePrefix(prefix, config.prefix);
if (def.query === null) {
logger.trace('>Ignored');
result = {};
} else {
result = proj('Simple', pf, def.query);
}
if (def.recursive && recursion) {
result = _.assign(result, recursion([makePrefix(pf, def.prefix, `${field}.`)]));
}
return result;
},
stepFunc({ config, field, type, next }, [prefix], recursion) {
logger.debug('Projecting (inline) fragment', field);
const newPrefix = type === next
? prefix
: makePrefix(prefix, config.prefix);
return recursion([newPrefix]);
},
reduceFunc(configs, typeResult, fieldResults) {
return _.assign({}, typeResult, ...fieldResults);
},
}, ['']);
const genProjection = ({ root, pick }) => {
const projector = makeProjection({ pick });
return (info) => {
try {
const result = _.assign({}, root, projector(info));
logger.debug('Project result', result);
return result;
} catch (e) {
/* istanbul ignore next */
logger.error('Projecting', e);
/* istanbul ignore next */
return undefined;
}
};
};
module.exports = {
makeProjection,
genProjection,
};

@@ -9,4 +9,4 @@ const {

it('should accept undefined', () => {
const result = prepareProjectionConfig(undefined);
expect(result.query).toEqual(undefined);
const result = prepareProjectionConfig(undefined, 'fn');
expect(result.query).toEqual('fn');
expect(result.select).toEqual(undefined);

@@ -18,3 +18,3 @@ expect(result.recursive).toBeFalsy();

it('should accept null', () => {
const result = prepareProjectionConfig(null);
const result = prepareProjectionConfig(null, 'fn');
expect(result.query).toEqual(null);

@@ -27,3 +27,3 @@ expect(result.select).toEqual(undefined);

it('should accept true', () => {
const result = prepareProjectionConfig(true);
const result = prepareProjectionConfig(true, 'fn');
expect(result.query).toEqual(null);

@@ -36,3 +36,3 @@ expect(result.select).toEqual(undefined);

it('should accept string', () => {
const result = prepareProjectionConfig('str');
const result = prepareProjectionConfig('str', 'fn');
expect(result.query).toEqual('str');

@@ -45,3 +45,3 @@ expect(result.select).toEqual('str');

it('should accept recursive string', () => {
const result = prepareProjectionConfig('str.');
const result = prepareProjectionConfig('str.', 'fn');
expect(result.query).toEqual(null);

@@ -54,3 +54,3 @@ expect(result.select).toEqual('str');

it('should accept recursive string', () => {
const result = prepareProjectionConfig('str');
const result = prepareProjectionConfig('str', 'fn');
expect(result.query).toEqual('str');

@@ -63,3 +63,3 @@ expect(result.select).toEqual('str');

it('should accept array', () => {
const result = prepareProjectionConfig(['a', 'b']);
const result = prepareProjectionConfig(['a', 'b'], 'fn');
expect(result.query).toEqual(['a', 'b']);

@@ -76,3 +76,3 @@ expect(result.select).toEqual(undefined);

recursive: 0,
});
}, 'fn');
expect(result.query).toEqual('q');

@@ -89,3 +89,3 @@ expect(result.select).toEqual('s');

prefix: 'xxx',
});
}, 'fn');
expect(result.query).toEqual(['a', 'b']);

@@ -96,2 +96,12 @@ expect(result.select).toEqual(undefined);

});
it('should accept object 3', () => {
const result = prepareProjectionConfig({
recursive: true,
}, 'fn');
expect(result.query).toEqual('fn');
expect(result.select).toEqual(undefined);
expect(result.recursive).toBeTruthy();
expect(result.prefix).toBeUndefined();
});
});

@@ -161,2 +171,3 @@

a: 'b',
c: {},
},

@@ -174,2 +185,5 @@ },

},
c: {
query: 'c',
},
},

@@ -182,2 +196,3 @@ },

a: { query: 'b', select: 'b' },
c: { query: 'c' },
},

@@ -184,0 +199,0 @@ });

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

const _ = require('lodash/fp');
const fs = require('fs');

@@ -6,4 +7,801 @@ const path = require('path');

const { prepareConfig } = require('../src/prepareConfig');
const { genProjection } = require('../src/projection');
const { makeProjection, genProjection } = require('../src/projection');
describe('makeProjection', () => {
const typeDefs = fs.readFileSync(path.join(__dirname, 'schema.graphql'), 'utf-8');
const run = (config, query) => new Promise((resolve, reject) => {
const pick = _.mapValues(_.constant)(config);
const go = (info) => {
try {
const proj = makeProjection({ pick })(info);
resolve(proj);
} catch (e) {
reject(e);
}
};
graphql(makeExecutableSchema({
typeDefs,
resolvers: {
Query: {
obj: (parent, args, context, info) => {
go(info);
},
evil: (parent, args, context, info) => {
go(info);
},
},
},
}), query).then((res) => {
if (res.errors) {
throw res.errors;
}
});
});
it('should project default when not configured', () => {
expect.hasAssertions();
return expect(run({}, '{ obj { field1 } }')).resolves.toEqual({
field1: 1,
});
});
it('should project query null', () => {
expect.hasAssertions();
return expect(run({
Obj: {
proj: {
field1: {
query: null,
},
},
},
}, '{ obj { field1 } }')).resolves.toEqual({
});
});
it('should project query simple', () => {
expect.hasAssertions();
return expect(run({
Obj: {
prefix: 'wrap.',
proj: {
field1: {
query: 'value',
},
},
},
}, '{ obj { field1 } }')).resolves.toEqual({
'wrap.value': 1,
});
});
it('should project query multiple', () => {
expect.hasAssertions();
return expect(run({
Obj: {
prefix: 'wrap.',
proj: {
field1: {
query: ['value', 'value2'],
},
},
},
}, '{ obj { field1 } }')).resolves.toEqual({
'wrap.value': 1,
'wrap.value2': 1,
});
});
it('should not project recursive if false', () => {
expect.hasAssertions();
return expect(run({
Obj: {
prefix: 'wrap.',
proj: { },
},
Foo: {
prefix: 'wrap2.',
proj: {
f1: { query: 'foo' },
},
},
}, '{ obj { field2 { f1 } } }')).resolves.toEqual({
'wrap.field2': 1,
});
});
it('should project recursive', () => {
expect.hasAssertions();
return expect(run({
Obj: {
prefix: 'wrap.',
proj: {
field2: {
query: ['evil', 'evils'],
recursive: true,
},
},
},
}, '{ obj { field2 { f1 } } }')).resolves.toEqual({
'wrap.evil': 1,
'wrap.evils': 1,
'wrap.field2.f1': 1,
});
});
it('should project recursive prefix null', () => {
expect.hasAssertions();
return expect(run({
Obj: {
prefix: 'wrap.',
proj: {
field2: {
query: ['evil', 'evils'],
recursive: true,
prefix: null,
},
},
},
}, '{ obj { field2 { f1 } } }')).resolves.toEqual({
'wrap.evil': 1,
'wrap.evils': 1,
'wrap.f1': 1,
});
});
it('should project recursive prefix', () => {
expect.hasAssertions();
return expect(run({
Obj: {
prefix: 'wrap.',
proj: {
field2: {
query: ['evil', 'evils'],
recursive: true,
prefix: 'xxx.',
},
},
},
}, '{ obj { field2 { f1 } } }')).resolves.toEqual({
'wrap.evil': 1,
'wrap.evils': 1,
'wrap.xxx.f1': 1,
});
});
it('should project recursive prefix absolute', () => {
expect.hasAssertions();
return expect(run({
Obj: {
prefix: 'wrap.',
proj: {
field2: {
query: null,
recursive: true,
prefix: '.xxx.',
},
},
},
}, '{ obj { field2 { f1 } } }')).resolves.toEqual({
'xxx.f1': 1,
});
});
it('should project recursive relative', () => {
expect.hasAssertions();
return expect(run({
Obj: {
prefix: 'wrap.',
proj: {
field2: {
query: null,
recursive: true,
prefix: 'xxx.',
},
},
},
Foo: {
prefix: 'wrap2.',
proj: {
f1: { query: 'foo' },
},
},
}, '{ obj { field2 { f1 } } }')).resolves.toEqual({
'wrap.xxx.wrap2.foo': 1,
});
});
it('should project recursive absolute', () => {
expect.hasAssertions();
return expect(run({
Obj: {
prefix: 'wrap.',
proj: {
field2: {
query: null,
recursive: true,
prefix: 'xxx.',
},
},
},
Foo: {
prefix: '.wrap2.',
proj: {
f1: { query: 'foo' },
},
},
}, '{ obj { field2 { f1 } } }')).resolves.toEqual({
'wrap2.foo': 1,
});
});
it('should project inline fragment', () => {
expect.hasAssertions();
return expect(run({
Obj: {
prefix: 'wrap.',
proj: {
field1: { query: 'value' },
},
},
}, `{
obj {
... {
field1
}
}
}`)).resolves.toEqual({
'wrap.value': 1,
});
});
it('should project deep inline fragment', () => {
expect.hasAssertions();
return expect(run({
Obj: {
prefix: 'wrap.',
proj: {
field1: { query: 'value' },
},
},
}, `{
obj {
... {
... {
... {
field1
}
}
}
}
}`)).resolves.toEqual({
'wrap.value': 1,
});
});
it('should project fragment', () => {
expect.hasAssertions();
return expect(run({
Obj: {
prefix: 'wrap.',
proj: {
field1: { query: 'value' },
},
},
}, `{
obj {
...f
}
}
fragment f on Obj {
field1
}
`)).resolves.toEqual({
'wrap.value': 1,
});
});
it('should project deep fragment', () => {
expect.hasAssertions();
return expect(run({
Obj: {
prefix: 'wrap.',
proj: {
field1: { query: 'value' },
},
},
}, `{
obj {
...f
}
}
fragment f on Obj {
...g
}
fragment g on Obj {
...h
}
fragment h on Obj {
field1
}
`)).resolves.toEqual({
'wrap.value': 1,
});
});
it('should project typeProj', () => {
expect.hasAssertions();
return expect(run({
Obj: {
proj: {
field3: {
query: null,
recursive: true,
prefix: 'evil.',
},
},
},
Father: {
prefix: 'wrap.',
typeProj: 'type',
proj: {
g0: { query: 'value' },
},
},
Child: {
prefix: 'wrap2.',
proj: {
g1: { query: 'value2' },
},
},
}, `{
obj {
field3 {
g0
}
}
}`)).resolves.toEqual({
'evil.wrap.type': 1,
'evil.wrap.value': 1,
});
});
it('should project inline fragment with typeCondition', () => {
expect.hasAssertions();
return expect(run({
Obj: {
proj: {
field3: {
query: null,
recursive: true,
prefix: 'evil.',
},
},
},
Father: {
prefix: 'wrap.',
typeProj: 'type',
proj: {
g0: { query: 'value' },
},
},
Child: {
prefix: 'wrap2.',
proj: {
g1: { query: 'value2' },
},
},
}, `{
obj {
field3 {
g0
... on Child {
g1
}
}
}
}`)).resolves.toEqual({
'evil.wrap.type': 1,
'evil.wrap.value': 1,
'evil.wrap.wrap2.value2': 1,
});
});
it('should project deep inline fragment with typeCondition', () => {
expect.hasAssertions();
return expect(run({
Obj: {
proj: {
field3: {
query: null,
recursive: true,
prefix: 'evil.',
},
},
},
Father: {
prefix: 'wrap.',
typeProj: 'type',
proj: {
g0: { query: 'value' },
},
},
Child: {
prefix: 'wrap2.',
proj: {
g1: { query: 'value2' },
},
},
}, `{
obj {
field3 {
g0
... on Child {
... {
... on Child {
g1
}
}
}
}
}
}`)).resolves.toEqual({
'evil.wrap.type': 1,
'evil.wrap.value': 1,
'evil.wrap.wrap2.value2': 1,
});
});
it('should project inline fragment with typeCondition partial', () => {
expect.hasAssertions();
return expect(run({
Obj: {
proj: {
field3: {
query: null,
recursive: true,
prefix: 'evil.',
},
},
},
Father: {
prefix: 'wrap.',
typeProj: 'type',
proj: {
g0: { query: 'value' },
},
},
Child: {
prefix: 'wrap2.',
proj: {
g1: { query: 'value2' },
},
},
}, `{
obj {
field3 {
... on Child {
g0
g1
}
}
}
}`)).resolves.toEqual({
'evil.wrap.type': 1,
'evil.wrap.wrap2.g0': 1,
'evil.wrap.wrap2.value2': 1,
});
});
it('should project fragment with typeCondition', () => {
expect.hasAssertions();
return expect(run({
Obj: {
proj: {
field3: {
query: null,
recursive: true,
prefix: null,
},
},
},
Father: {
prefix: 'wrap.',
typeProj: 'type',
proj: {
g0: { query: 'value' },
},
},
Child: {
prefix: 'wrap2.',
proj: {
g1: { query: 'value2' },
},
},
}, `{
obj {
field3 {
g0
...f
}
}
}
fragment f on Child {
g1
}
`)).resolves.toEqual({
'wrap.type': 1,
'wrap.value': 1,
'wrap.wrap2.value2': 1,
});
});
it('should handle deep nested', () => {
expect.hasAssertions();
return expect(run({
Evil: {
proj: {
self: {
query: null,
recursive: true,
prefix: null,
},
},
},
}, `{
evil {
field
self {
field
self {
field
}
}
}
}
`)).resolves.toEqual({
field: 1,
});
});
it('should handle deep nested prefix', () => {
expect.hasAssertions();
return expect(run({
Evil: {
prefix: '.x.',
proj: {
self: {
query: null,
recursive: true,
prefix: null,
},
},
},
}, `{
evil {
field
self {
field
self {
field
}
}
}
}
`)).resolves.toEqual({
'x.field': 1,
});
});
it('should handle deep nested prefix relative', () => {
expect.hasAssertions();
return expect(run({
Evil: {
prefix: 'x.',
proj: {
self: {
query: null,
recursive: true,
prefix: null,
},
},
},
}, `{
evil {
field
self {
field
self {
field
}
}
}
}
`)).resolves.toEqual({
'x.field': 1,
'x.x.field': 1,
'x.x.x.field': 1,
});
});
it('should handle deep nested proj prefix', () => {
expect.hasAssertions();
return expect(run({
Evil: {
proj: {
self: {
query: null,
recursive: true,
prefix: 'y.',
},
},
},
}, `{
evil {
field
self {
field
self {
field
}
}
}
}
`)).resolves.toEqual({
field: 1,
'y.field': 1,
'y.y.field': 1,
});
});
it('should handle deep nested proj prefix prefix', () => {
expect.hasAssertions();
return expect(run({
Evil: {
prefix: '.x.',
proj: {
self: {
query: null,
recursive: true,
prefix: 'y.',
},
},
},
}, `{
evil {
field
self {
field
self {
field
}
}
}
}
`)).resolves.toEqual({
'x.field': 1,
});
});
it('should handle deep nested proj prefix prefix relative', () => {
expect.hasAssertions();
return expect(run({
Evil: {
prefix: 'x.',
proj: {
self: {
query: null,
recursive: true,
prefix: 'y.',
},
},
},
}, `{
evil {
field
self {
field
self {
field
}
}
}
}
`)).resolves.toEqual({
'x.field': 1,
'x.y.x.field': 1,
'x.y.x.y.x.field': 1,
});
});
it('should handle deep nested proj prefix abs', () => {
expect.hasAssertions();
return expect(run({
Evil: {
proj: {
self: {
query: null,
recursive: true,
prefix: '.y.',
},
},
},
}, `{
evil {
field
self {
field
self {
field
}
}
}
}
`)).resolves.toEqual({
field: 1,
'y.field': 1,
});
});
it('should handle deep nested proj prefix abs prefix', () => {
expect.hasAssertions();
return expect(run({
Evil: {
prefix: '.x.',
proj: {
self: {
query: null,
recursive: true,
prefix: '.y.',
},
},
},
}, `{
evil {
field
self {
field
self {
field
}
}
}
}
`)).resolves.toEqual({
'x.field': 1,
});
});
it('should handle deep nested proj prefix abs prefix relative', () => {
expect.hasAssertions();
return expect(run({
Evil: {
prefix: 'x.',
proj: {
self: {
query: null,
recursive: true,
prefix: '.y.',
},
},
},
}, `{
evil {
field
self {
field
self {
field
}
}
}
}
`)).resolves.toEqual({
'x.field': 1,
'y.x.field': 1,
});
});
});
describe('genProjection', () => {

@@ -10,0 +808,0 @@ const typeDefs = fs.readFileSync(path.join(__dirname, 'schema.graphql'), 'utf-8');

SocketSocket SOC 2 Logo

Product

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

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc