New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

gl-wiretap

Package Overview
Dependencies
Maintainers
1
Versions
14
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

gl-wiretap - npm Package Compare versions

Comparing version 0.1.1 to 0.2.0

8

index.d.ts

@@ -8,9 +8,11 @@ export interface IGLWiretapOptions {

throwGetProgramParameter?: Boolean;
useTrackableNumbers?: Boolean;
}
export interface IGLExtensionWiretapOptions {
gl: GLWiretapProxy;
contextName: string;
contextVariables: any[];
getEntity: (value: number) => string | number;
useTrackableNumbers?: Boolean;
recording: string[];
variableName: string;
variables: any;
}

@@ -17,0 +19,0 @@

/**
*
* @param {WebGLRenderingContext} gl
* @param {{
* contextName: String,
* readPixelsFile: String,
* recording: String[],
* throwGetError: Boolean,
* throwGetShaderParameter: Boolean,
* throwGetProgramParameter: Boolean,
* }} options?
* @param {IGLWiretapOptions} options?
* @returns {WebGLRenderingContext}
*/
function glWiretap(gl, options) {
options = options || {};
const recording = options.recording || [];
const readPixelsFile = options.readPixelsFile;
const throwGetError = options.throwGetError;
const throwGetShaderParameter = options.throwGetShaderParameter;
const throwGetProgramParameter = options.throwGetProgramParameter;
const contextName = options.contextName || 'gl';
function glWiretap(gl, options = {}) {
const {
contextName = 'gl',
throwGetError,
useTrackableNumbers,
readPixelsFile,
recording = [],
} = options;
const proxy = new Proxy(gl, { get: listen });
const variables = {
programs: [],
vertexShaders: [],
fragmentShaders: [],
pixels: [],
bufferOutputs: [],
buffers: [],
imageData: [],
attributeLocations: [],
uniformLocations: [],
textures: [],
framebuffers: [],
extensions: [],
};
const glEntityNames = Object.getOwnPropertyNames(gl).reduce((obj, name) => {
obj[gl[name]] = name;
return obj;
}, {});
const contextVariables = [];
const entityNames = {};
let imageCount = 0;
return proxy;

@@ -51,241 +29,52 @@ function listen(obj, property) {

if (throwGetError) {
recording.push(`if (${contextName}.getError() !== ${contextName}.NONE) throw new Error("error");`);
recording.push(`if (${contextName}.getError() !== ${contextName}.NONE) throw new Error('error');`);
} else {
recording.push(`${contextName}.getError();`); // flush out errors
}
break;
case 'createProgram':
recording.push(`const program${variables.programs.length} = ${contextName}.createProgram();`);
const program = gl.createProgram();
variables.programs.push(program);
return program;
case 'createShader':
if (arguments[0] === gl.VERTEX_SHADER) {
recording.push(`const vertexShader${variables.vertexShaders.length} = ${contextName}.createShader(${contextName}.VERTEX_SHADER);`);
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
variables.vertexShaders.push(vertexShader);
return vertexShader;
} else if (arguments[0] === gl.FRAGMENT_SHADER) {
recording.push(`const fragmentShader${variables.fragmentShaders.length} = ${contextName}.createShader(${contextName}.FRAGMENT_SHADER);`);
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
variables.fragmentShaders.push(fragmentShader);
return fragmentShader
} else {
throw new Error('unrecognized shader type');
}
case 'createTexture':
recording.push(`const texture${variables.textures.length} = ${contextName}.createTexture();`);
const texture = gl.createTexture();
variables.textures.push(texture);
return texture;
case 'createFramebuffer':
recording.push(`const framebuffer${variables.framebuffers.length} = ${contextName}.createFramebuffer();`);
const framebuffer = gl.createFramebuffer();
variables.framebuffers.push(framebuffer);
return framebuffer;
return gl.getError();
case 'getExtension':
recording.push(`const extension${variables.extensions.length} = ${contextName}.getExtension("${arguments[0]}");`);
const variableName = `${contextName}Variables${contextVariables.length}`;
recording.push(`const ${variableName} = ${contextName}.getExtension('${arguments[0]}');`);
const extension = glExtensionWiretap(gl.getExtension(arguments[0]), {
gl,
getEntity,
useTrackableNumbers,
recording,
variableName: `extension${variables.extensions.length}`,
variables,
contextName: variableName,
contextVariables,
});
variables.extensions.push(extension);
contextVariables.push(extension);
return extension;
case 'shaderSource':
if (variables.vertexShaders.indexOf(arguments[0]) > -1) {
recording.push(contextName + '.shaderSource(vertexShader' + variables.vertexShaders.indexOf(arguments[0]) + ', `' + arguments[1] + '`);');
} else if (variables.fragmentShaders.indexOf(arguments[0]) > -1) {
recording.push(contextName + '.shaderSource(fragmentShader' + variables.fragmentShaders.indexOf(arguments[0]) + ', `' + arguments[1] + '`);');
case 'readPixels':
const i = contextVariables.indexOf(arguments[6]);
let targetVariableName;
if (i === -1) {
targetVariableName = `${contextName}Variable${contextVariables.length}`;
recording.push(`const ${targetVariableName} = new ${arguments[6].constructor.name}(${arguments[6].length});`);
contextVariables.push(arguments[6]);
} else {
throw new Error('unrecognized shader type');
targetVariableName = `${contextName}Variable${i}`;
}
break;
case 'compileShader':
if (variables.vertexShaders.indexOf(arguments[0]) > -1) {
recording.push(`${contextName}.compileShader(vertexShader${variables.vertexShaders.indexOf(arguments[0])});`);
} else if (variables.fragmentShaders.indexOf(arguments[0]) > -1) {
recording.push(`${contextName}.compileShader(fragmentShader${variables.fragmentShaders.indexOf(arguments[0])});`);
} else {
throw new Error('unrecognized shader type');
}
break;
case 'getShaderParameter':
if (throwGetShaderParameter) {
if (variables.vertexShaders.indexOf(arguments[0]) > -1) {
recording.push(`if (!${contextName}.getShaderParameter(vertexShader${variables.vertexShaders.indexOf(arguments[0])}, ${contextName}.COMPILE_STATUS)) throw new Error("shader did not compile");`);
} else if (variables.fragmentShaders.indexOf(arguments[0]) > -1) {
recording.push(`if (!${contextName}.getShaderParameter(fragmentShader${variables.fragmentShaders.indexOf(arguments[0])}, ${contextName}.COMPILE_STATUS)) throw new Error("shader did not compile");`);
} else {
throw new Error('unrecognized shader type');
}
}
break;
case 'attachShader':
if (variables.vertexShaders.indexOf(arguments[1]) > -1) {
recording.push(`${contextName}.attachShader(program${variables.programs.indexOf(arguments[0])}, vertexShader${variables.vertexShaders.indexOf(arguments[1])});`);
} else if (variables.fragmentShaders.indexOf(arguments[1]) > -1) {
recording.push(`${contextName}.attachShader(program${variables.programs.indexOf(arguments[0])}, fragmentShader${variables.fragmentShaders.indexOf(arguments[1])});`);
} else if (arguments[1] === null) {
recording.push(`${contextName}.attachShader(program${variables.programs.indexOf(arguments[0])}, null);`);
} else {
throw new Error('unrecognized shader type');
}
break;
case 'bindAttribLocation':
recording.push(contextName + '.bindAttribLocation(program' + variables.programs.indexOf(arguments[0]) + ', ' + arguments[1] + ', `' + arguments[2] + '`);');
break;
case 'linkProgram':
recording.push(contextName + `.linkProgram(program${variables.programs.indexOf(arguments[0])});`);
break;
case 'getProgramParameter':
if (throwGetProgramParameter) {
recording.push(`if (!${contextName}.getProgramParameter(program${variables.programs.indexOf(arguments[0])}, ${contextName}.LINK_STATUS)) throw new Error(${contextName}.getProgramInfoLog(program${variables.programs.indexOf(arguments[0])}));`);
}
break;
case 'useProgram':
recording.push(`${contextName}.useProgram(program${variables.programs.indexOf(arguments[0])});`);
break;
case 'createBuffer':
recording.push(`const buffer${variables.buffers.length} = ${contextName}.createBuffer();`);
const buffer = gl.createBuffer();
variables.buffers.push(buffer);
return buffer;
case 'bindBuffer':
if (arguments[0] === gl.ARRAY_BUFFER) {
recording.push(`${contextName}.bindBuffer(${contextName}.ARRAY_BUFFER, buffer${variables.buffers.indexOf(arguments[1])});`);
} else if (arguments[0] === gl.ELEMENT_ARRAY_BUFFER) {
recording.push(`${contextName}.bindBuffer(${contextName}.ARRAY_BUFFER, buffer${variables.buffers.indexOf(arguments[1])});`);
} else {
throw new Error('unrecognized buffer type');
}
break;
case 'bufferData':
if (arguments[0] === gl.ARRAY_BUFFER) {
recording.push(`const bufferOutput${variables.bufferOutputs.length} = new Float32Array([${Array.from(arguments[1]).join(',')}]);`);
recording.push(`${contextName}.bufferData(${contextName}.ARRAY_BUFFER, bufferOutput${variables.bufferOutputs.length}, ${arguments[2]});`);
variables.bufferOutputs.push(arguments[1]);
} else if (arguments[0] === gl.ELEMENT_ARRAY_BUFFER) {
recording.push(`const bufferOutput${variables.bufferOutputs.length} = new Uint16Array([${Array.from(arguments[1]).join(',')}]);`);
recording.push(`${contextName}.bufferData(${contextName}.ELEMENT_ARRAY_BUFFER, bufferOutput${variables.bufferOutputs.length}, ${arguments[2]});`);
variables.bufferOutputs.push(arguments[1]);
} else {
throw new Error('unrecognized buffer type');
}
break;
case 'readPixels':
recording.push(`const pixels${variables.pixels.length} = new Uint8Array(${arguments[2] * arguments[3] * 4});`);
recording.push(`${contextName}.readPixels(${arguments[0]}, ${arguments[1]}, ${arguments[2]}, ${arguments[3]}, ${arguments[4]}, ${arguments[5]}, pixels${variables.pixels.length});`);
recording.push(`${contextName}.readPixels(${arguments[0]}, ${arguments[1]}, ${arguments[2]}, ${arguments[3]}, ${getEntity(arguments[4])}, ${getEntity(arguments[5])}, ${targetVariableName});`);
if (readPixelsFile) {
writePPM(arguments[2], arguments[3]);
}
variables.pixels.push(null);
break;
case 'deleteProgram':
if (arguments[0] === null) {
recording.push(`${contextName}.deleteProgram(null);`);
} else {
recording.push(`${contextName}.deleteProgram(program${variables.programs.indexOf(arguments[0])});`);
}
break;
case 'getAttribLocation':
recording.push('const attributeLocation' + variables.attributeLocations.length + ' = ' + contextName + '.getAttribLocation(program' + variables.programs.indexOf(arguments[0]) + ', `' + arguments[1] + '`;');
const attributeLocation = gl.getAttribLocation(arguments[0], arguments[1]);
variables.attributeLocations.push(attributeLocation);
return attributeLocation;
case 'getUniformLocation':
recording.push(`const uniformLocation${variables.uniformLocations.length} = ${contextName}.getUniformLocation(program${variables.programs.indexOf(arguments[0])}, '${arguments[1]}');`);
const uniformLocation = gl.getUniformLocation(arguments[0], arguments[1]);
variables.uniformLocations.push(uniformLocation);
return uniformLocation;
case 'vertexAttribPointer':
if (variables.attributeLocations.indexOf(arguments[0]) > -1) {
recording.push(`${contextName}.vertexAttribPointer(attributeLocation${variables.attributeLocations.indexOf(arguments[0])}, ${arguments[1]}, ${arguments[2]}, ${arguments[3]}, ${arguments[4]}, ${arguments[5]});`);
} else {
recording.push(`${contextName}.vertexAttribPointer(${arguments[0]}, ${arguments[1]}, ${arguments[2]}, ${arguments[3]}, ${arguments[4]}, ${arguments[5]});`);
}
break;
case 'enableVertexAttribArray':
if (variables.attributeLocations.indexOf(arguments[0]) > -1) {
recording.push(`${contextName}.enableVertexAttribArray(attributeLocation${variables.attributeLocations.indexOf(arguments[0])});`);
} else {
recording.push(`${contextName}.enableVertexAttribArray(${arguments[0]});`);
}
break;
case 'getShaderInfoLog':
if (variables.vertexShaders.indexOf(arguments[0]) > -1) {
recording.push(`${contextName}.getShaderInfoLog(vertexShader${variables.vertexShaders.indexOf(arguments[0])});`);
} else if (variables.fragmentShaders.indexOf(arguments[0]) > -1) {
recording.push(`${contextName}.getShaderInfoLog(fragmentShader${variables.fragmentShaders.indexOf(arguments[0])});`);
} else {
throw new Error('unrecognized shader type');
}
break;
case 'deleteShader':
if (variables.vertexShaders.indexOf(arguments[0]) > -1) {
recording.push(`${contextName}.deleteShader(vertexShader${variables.vertexShaders.indexOf(arguments[0])});`);
} else if (variables.fragmentShaders.indexOf(arguments[0]) > -1) {
recording.push(`${contextName}.deleteShader(fragmentShader${variables.fragmentShaders.indexOf(arguments[0])});`);
} else {
throw new Error('unrecognized shader type');
}
break;
case 'getProgramInfoLog':
recording.push(`${contextName}.getProgramInfoLog(program${variables.programs.indexOf(arguments[0])});`);
break;
case 'bindTexture':
recording.push(`${contextName}.bindTexture(${arguments[0]}, texture${variables.textures.indexOf(arguments[1])});`);
break;
case 'bindFramebuffer':
if (arguments[1] === null) {
recording.push(`${contextName}.bindFramebuffer(${getGLEntity(arguments[0])}, null);`);
} else {
recording.push(`${contextName}.bindFramebuffer(${getGLEntity(arguments[0])}, framebuffer${variables.framebuffers.indexOf(arguments[1])});`);
}
break;
case 'framebufferTexture2D':
recording.push(`${contextName}.framebufferTexture2D(${arguments[0]}, ${arguments[1]}, ${arguments[2]}, texture${variables.textures.indexOf(arguments[3])}, ${arguments[4]});`);
break;
case 'deleteFramebuffer':
recording.push(`${contextName}.deleteFramebuffer(framebuffer${variables.framebuffers.indexOf(arguments[0])});`);
break;
case 'deleteTexture':
recording.push(`${contextName}.deleteTexture(texture${variables.textures.indexOf(arguments[0])});`);
break;
case 'uniform1fv':
case 'uniform1iv':
case 'uniform2fv':
case 'uniform2iv':
case 'uniform3fv':
case 'uniform3iv':
case 'uniform4fv':
case 'uniform4iv':
recording.push(`${contextName}.${property}(uniformLocation${variables.uniformLocations.indexOf(arguments[0])}, ${JSON.stringify(Array.from(arguments[1]))});`);
break;
case 'uniform1f':
case 'uniform1i':
recording.push(`${contextName}.${property}(uniformLocation${variables.uniformLocations.indexOf(arguments[0])}, ${arguments[1]});`);
break;
case 'uniform2f':
case 'uniform2i':
recording.push(`${contextName}.${property}(uniformLocation${variables.uniformLocations.indexOf(arguments[0])}, ${arguments[1]}, ${arguments[2]});`);
break;
case 'uniform3f':
case 'uniform3i':
recording.push(`${contextName}.${property}(uniformLocation${variables.uniformLocations.indexOf(arguments[0])}, ${arguments[1]}, ${arguments[2]}, ${arguments[3]});`);
break;
case 'uniform4f':
case 'uniform4i':
recording.push(`${contextName}.${property}(uniformLocation${variables.uniformLocations.indexOf(arguments[0])}, ${arguments[1]}, ${arguments[2]}, ${arguments[3]}, ${arguments[4]});`);
break;
case 'getParameter':
recording.push(`${contextName}.getParameter(${getGLEntity(arguments[0])});`);
break;
default:
recording.push(`${contextName}.${property}(${argumentsToString(arguments)});`);
return gl.readPixels.apply(gl, arguments);
case 'drawBuffers':
recording.push(`${contextName}.drawBuffers([${argumentsToString(arguments[0], { contextName, contextVariables, getEntity, addVariable } )}]);`);
return gl.drawBuffers(arguments[0]);
}
return gl[property].apply(gl, arguments);
let result = gl[property].apply(gl, arguments);
if (typeof result !== 'undefined' && contextVariables.indexOf(result) === -1) {
if (typeof result === 'number' && useTrackableNumbers) {
contextVariables.push(result = trackableNumber(result));
}
recording.push(`const ${contextName}Variable${contextVariables.length} = ${methodCallToString(property, arguments)};`);
contextVariables.push(result);
} else {
recording.push(`${methodCallToString(property, arguments)};`);
}
return result;
}
}
entityNames[gl[property]] = property;
return gl[property];

@@ -296,29 +85,23 @@ }

}
function getGLEntity(number) {
const name = glEntityNames[number];
if (!name) {
for (let extensionIndex = 0; extensionIndex < variables.extensions.length; extensionIndex++) {
const extension = variables.extensions[extensionIndex];
const extensionEntityNames = Object.getOwnPropertyNames(variables.extensions[extensionIndex]);
for (let extensionEntityNamesIndex = 0; extensionEntityNamesIndex < extensionEntityNames.length; extensionEntityNamesIndex++) {
const extensionEntityName = extensionEntityNames[extensionEntityNamesIndex];
if (extension[extensionEntityName] === number) {
return 'extension' + extensionIndex + '.' + extensionEntityName;
}
}
}
console.warn('GL entity not found');
return number;
function getEntity(value) {
const name = entityNames[value];
if (name) {
return contextName + '.' + name;
}
return contextName + '.' + name;
return value;
}
function addVariable(value) {
contextVariables.push(value);
}
function writePPM(width, height) {
recording.push(`let imageDatum${variables.imageData.length} = ["P3\\n# ${contextName}.ppm\\n", ${width}, " ", ${height}, "\\n255\\n"].join("");`);
recording.push(`for (let i = 0; i < pixels${variables.pixels.length}.length; i += 4) {`);
recording.push(` imageDatum${variables.imageData.length} += pixels${variables.pixels.length}[i] + " " + pixels${variables.pixels.length}[i + 1] + " " + pixels${variables.pixels.length}[i + 2] + " ";`);
const sourceVariable = `${contextName}Variable${contextVariables.length}`;
const imageVariable = `imageDatum${imageCount}`;
recording.push(`let ${imageVariable} = ["P3\\n# ${readPixelsFile}.ppm\\n", ${width}, ' ', ${height}, "\\n255\\n"].join("");`);
recording.push(`for (let i = 0; i < ${imageVariable}.length; i += 4) {`);
recording.push(` ${imageVariable} += ${sourceVariable}[i] + ' ' + ${sourceVariable}[i + 1] + ' ' + ${sourceVariable}[i + 2] + ' ';`);
recording.push('}');
recording.push('if (typeof require !== "undefined") {');
recording.push(` require("fs").writeFileSync("./${readPixelsFile}.ppm", imageDatum${variables.imageData.length});`);
recording.push(` require('fs').writeFileSync('./${readPixelsFile}.ppm', ${imageVariable});`);
recording.push('}');
variables.imageData.push(null);
imageCount++;
}

@@ -342,2 +125,5 @@ function addComment(value) {

}
function methodCallToString(method, args) {
return `${contextName}.${method}(${argumentsToString(args, { contextName, contextVariables, getEntity, addVariable })})`;
}
}

@@ -348,3 +134,3 @@

* @param extension
* @param {{recording: String, variableName: String}} options
* @param {IGLExtensionWiretapOptions} options
* @returns {*}

@@ -354,6 +140,10 @@ */

const proxy = new Proxy(extension, { get: listen });
const gl = options.gl;
const recording = options.recording;
const variableName = options.variableName;
const variables = options.variables;
const extensionEntityNames = {};
const {
contextName,
contextVariables,
getEntity,
useTrackableNumbers,
recording
} = options;
return proxy;

@@ -365,40 +155,93 @@ function listen(obj, property) {

case 'drawBuffersWEBGL':
extension.drawBuffersWEBGL(arguments[0]);
recording.push(`${variableName}.drawBuffersWEBGL([${Array.from(arguments).join(', ')}]);`);
return;
case 'getTranslatedShaderSource':
if (variables.vertexShaders.indexOf(arguments[0]) > -1) {
recording.push(`${variableName}.getTranslatedShaderSource(vertexShader${variables.vertexShaders.indexOf(arguments[0])})`);
} else if (variables.fragmentShaders.indexOf(arguments[0]) > -1) {
recording.push(`${variableName}.getTranslatedShaderSource(fragmentShader${variables.fragmentShaders.indexOf(arguments[0])})`);
} else {
throw new Error('Cannot find shader');
}
return extension.getTranslatedShaderSource(arguments[0]);
recording.push(`${contextName}.drawBuffersWEBGL([${argumentsToString(arguments[0], { contextName, contextVariables, getEntity: getExtensionEntity, addVariable })}]);`);
return extension.drawBuffersWEBGL(arguments[0]);
}
recording.push(`${variableName}.${property}(${argumentsToString(arguments)});`);
let result = extension[property].apply(extension, arguments);
if (typeof result !== 'undefined' && contextVariables.indexOf(result) === -1) {
if (useTrackableNumbers && typeof result === 'number') {
contextVariables.push(result = trackableNumber(result));
}
recording.push(`const ${contextName}Variable${contextVariables.length} = ${methodCallToString(property, arguments)};`);
contextVariables.push(result);
} else {
recording.push(`${methodCallToString(property, arguments)};`);
}
return result;
};
}
extensionEntityNames[extension[property]] = property;
return extension[property];
}
function getExtensionEntity(value) {
return extensionEntityNames[value] || getEntity(value);
}
function methodCallToString(method, args) {
return `${contextName}.${method}(${argumentsToString(args, { contextName, contextVariables, getEntity: getExtensionEntity, addVariable })})`;
}
function addVariable(value, source) {
const variableName = `${contextName}Variable${contextVariables.length}`;
recording.push(`const ${variableName} = ${source};`);
contextVariables.push(value);
return variableName;
}
}
function argumentsToString(args) {
return (Array.from(args).map(function (arg) {
switch (typeof arg) {
case 'string':
return '`' + arg + '`';
case 'number':
return arg;
case 'boolean':
return arg ? 'true' : 'false';
default:
if (arg === null) {
return 'null';
}
throw new Error('unrecognized argument');
}
function argumentsToString(args, options) {
return (Array.from(args).map((arg) => {
return argumentToString(arg, options);
}).join(', '))
}
function argumentToString(arg, options) {
const { contextName, contextVariables, getEntity, addVariable } = options;
if (typeof arg === 'undefined') {
return 'undefined';
}
if (arg === null) {
return 'null';
}
const i = contextVariables.indexOf(arg);
if (i > -1) {
return `${contextName}Variable${i}`;
}
switch (arg.constructor.name) {
case 'String':
const hasLines = /\n/.test(arg);
const hasSingleQuotes = /'/.test(arg);
const hasDoubleQuotes = /"/.test(arg);
if (hasLines) {
return '`' + arg + '`';
} else if (hasSingleQuotes && !hasDoubleQuotes) {
return '"' + arg + '"';
} else if (!hasSingleQuotes && hasDoubleQuotes) {
return "'" + arg + "'";
} else {
return '\'' + arg + '\'';
}
case 'Number':
const name = getEntity(arg);
return name || arg;
case 'Boolean':
return arg ? 'true' : 'false';
case 'Array':
return JSON.stringify(Array.from(arg));
case 'Float32Array':
case 'Uint8Array':
case 'Uint16Array':
case 'Int32Array':
return addVariable(arg, `new ${arg.constructor.name}(${JSON.stringify(Array.from(arg))})`);
default:
debugger;
throw new Error('unrecognized argument');
}
}
function trackableNumber(number) {
// wrapped in object, so track-able
return new Number(number);
}
if (typeof module !== 'undefined') {

@@ -405,0 +248,0 @@ module.exports = { glWiretap, glExtensionWiretap };

{
"name": "gl-wiretap",
"version": "0.1.1",
"version": "0.2.0",
"description": "A gl debugger that listens and replays gl (WebGL, WebGL2, and HeadlessGL) gpu commands",

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

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