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

restify

Package Overview
Dependencies
Maintainers
0
Versions
184
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

restify - npm Package Compare versions

Comparing version 0.1.13 to 0.1.14

tst/accept.test.js

3

docs/restify-log.md

@@ -25,2 +25,3 @@ restify-log(7) -- The restify Logger

* Off
* Fatal

@@ -35,3 +36,3 @@ * Error

output messages that are of level Fatal/Error/Warn. Everything else will be
surpressed. The default level is Info.
surpressed. The default level is Info. Off disables logging altogether.

@@ -38,0 +39,0 @@ Each level is accessed by a lower-case method of the same name:

@@ -78,3 +78,4 @@ restify-response(7) -- The Response Object

noClose: boolean,
noEnd: boolean
noEnd: boolean,
noContentMD5: boolean
});

@@ -81,0 +82,0 @@

@@ -42,6 +42,6 @@ restify(3) -- Getting Started with restify

serverName: 'MySite', // returned in the HTTP 'Server:` header
exceptionHandler: function(e) {}, // calls function(2) on uncaught errors
maxRequestSize: 8192, // Any request body larger than this gets a 400
clockSkew: 300, // Allow up to N seconds of skew in the Date header
accept: ['application/json'] // Allow these Accept types
accept: ['application/json'], // Allow these Accept types
logTo: process.stderr // Where to direct log output
}

@@ -60,6 +60,2 @@

header. Defaults to `node.js`.
* exceptionHandler:
Installs your function to handle all uncaught JS exceptions. If you **don't**
provide one, restify installs a default handler that simply returns 500
Internal Server Error, and logs a warning.
* maxRequestSize:

@@ -74,2 +70,5 @@ Caps the amount of data a client can send to your HTTP server, in bytes.

`application/json`. Not really useful as of yet.
* logTo:
An instance of `Writable Stream`. All restify.log messages will go to that.
Defaults to process.stderr.

@@ -124,5 +123,5 @@ ## ROUTING

param1: 'dog',
param2: 'cat',
param3: 'bird',
param4: 'turtle'
param2: 'cat',
param3: 'bird',
param4: 'turtle'
}

@@ -129,0 +128,0 @@ }

@@ -5,5 +5,5 @@ // Copyright 2011 Mark Cavage <mcavage@gmail.com> All rights reserved.

// Headers
XRequestId: 'X-Request-Id',
XApiVersion: 'X-Api-Version',
XResponseTime: 'X-Response-Time',
XRequestId: 'x-request-id',
XApiVersion: 'x-api-version',
XResponseTime: 'x-response-time',

@@ -15,3 +15,4 @@ // Misc

ContentTypeFormEncoded: 'application/x-www-form-urlencoded',
DefaultServerName: 'node.js'
DefaultServerName: 'node.js',
NoApiVersion: '__no_version'
};

@@ -15,2 +15,4 @@ // Copyright 2011 Mark Cavage <mcavage@gmail.com> All rights reserved.

function _pad(val) {

@@ -23,2 +25,3 @@ if (parseInt(val, 10) < 10) {

function _rfc822(date) {

@@ -48,26 +51,33 @@ var months = ['Jan',

function _mergeFnArgs() {
function _mergeFnArgs(argv, offset) {
var _handlers = [];
var _args = arguments[0];
var i = 1;
do {
if (_args[i] instanceof Array) {
var _arr = _args[i];
for (var j = 0; j < _arr.length; j++) {
if (!(_arr[j] instanceof Function)) {
throw new Error('Invalid argument type: ' + typeof(_arr[j]));
if (log.trace())
log.trace('_mergeFnArgs: argv=%o, offset=%d', argv, offset);
for (var i = offset; i < argv.length; i++) {
if (argv[i] instanceof Array) {
var arr = argv[i];
for (var j = 0; j < arr.length; j++) {
if (!(arr[j] instanceof Function)) {
throw new TypeError('Invalid argument type: ' + typeof(arr[j]));
}
_handlers.push(_arr[j]);
_handlers.push(arr[j]);
}
} else if (_args[i] instanceof Function) {
_handlers.push(_args[i]);
} else if (argv[i] instanceof Function) {
_handlers.push(argv[i]);
} else {
throw new Error('Invalid argument type: ' + typeof(_args[i]));
throw new TypeError('Invalid argument type: ' + typeof(argv[i]));
}
} while (++i < _args.length);
}
if (log.trace())
log.trace('_mergeFnArgs: %o', _handlers);
return _handlers;
}
////////////////////////

@@ -133,4 +143,6 @@ // Prototype extensions

if (this._config.apiVersion)
headers[Constants.XApiVersion] = this._config.apiVersion;
if (this._apiVersion &&
this._serverVersioned &&
this._apiVersion !== Constants.NoApiVersion)
headers[Constants.XApiVersion] = this._apiVersion;

@@ -160,7 +172,9 @@ headers.Date = _rfc822(now);

this._bytes = data.length;
var hash = crypto.createHash('md5');
hash.update(data);
headers['Content-MD5'] = hash.digest(encoding = 'base64');
if (_opts.code !== HttpCodes.NoContent) {
headers['Content-Length'] = data.length;
if (!_opts.noContentMD5) {
var hash = crypto.createHash('md5');
hash.update(data);
headers['Content-MD5'] = hash.digest(encoding = 'base64');
}
} else {

@@ -190,2 +204,3 @@ headers['Content-Length'] = 0;

http.ServerResponse.prototype.sendError = function sendError(error) {

@@ -210,8 +225,37 @@ if (!error || !error.restCode || !error.message || !error.httpCode) {

http.Server.prototype._mount = function _mount(method, url, handlers) {
/**
* Adds a route for handling.
*
* This method supports the notion of versioned routes. Basically, the routing
* table looks like this:
*
* {
* '1.2.3':
* {
* 'GET':
* {
* '/foo/bar': [f(req, res, next), ...],
* }
* }
* }
*
* An identical "reverse" index is kept for mapping URLs to methods (so just
* mentally swap foo/bar and GET above).
*
* @param {String} method HTTP method.
* @param {String} url the HTTP resource.
* @param {Array} handlers an array of function(req, res, next).
* @param {String} version version for this route.
*
*/
http.Server.prototype._mount = function _mount(method, url, handlers, version) {
if (version !== Constants.NoApiVersion)
this._versioned = true;
if (!this.routes) this.routes = {};
if (!this.routes[method]) this.routes[method] = [];
// reverse index
if (!this.routes[version]) this.routes[version] = {};
if (!this.routes[version][method]) this.routes[version][method] = [];
if (!this.routes.urls) this.routes.urls = {};
if (!this.routes.urls[url]) this.routes.urls[url] = [];
if (!this.routes.urls[version]) this.routes.urls[version] = {};
if (!this.routes.urls[version][url]) this.routes.urls[version][url] = [];

@@ -231,3 +275,2 @@ var _handlers = [];

var r = {

@@ -237,36 +280,131 @@ method: method,

handlers: _handlers,
urlComponents: url.split('/').slice(1)
urlComponents: url.split('/').slice(1),
version: version
};
this.routes[method].push(r);
this.routes.urls[url].push(r);
this.routes[version][method].push(r);
this.routes.urls[version][url].push(r);
if (log.trace())
log.trace('restify._mount: routes now %o', this.routes);
};
http.Server.prototype.del = function(url) {
if (!url) throw new Error('url is required');
return this._mount('DELETE', url, _mergeFnArgs(arguments));
http.Server.prototype.del = function() {
var args = Array.prototype.slice.call(arguments);
var offset = 1;
var url = args[0];
var version = this._config.defaultVersion;
if (!args[0] || typeof(args[0]) !== 'string')
throw new TypeError('argument 0 must be a string (version or url)');
if (!args[1])
throw new TypeError('argument 1 is required (handler chain or url)');
if (typeof(args[1]) === 'string') {
version = args[0];
url = args[1];
offset = 2;
}
var handlers = _mergeFnArgs(args, offset);
return this._mount('DELETE', url, handlers, version);
};
http.Server.prototype.get = function(url) {
if (!url) throw new Error('url is required');
return this._mount('GET', url, _mergeFnArgs(arguments));
http.Server.prototype.get = function() {
var args = Array.prototype.slice.call(arguments);
var offset = 1;
var url = args[0];
var version = this._config.defaultVersion;
if (!args[0] || typeof(args[0]) !== 'string')
throw new TypeError('argument 0 must be a string (version or url)');
if (!args[1])
throw new TypeError('argument 1 is required (handler chain or url)');
if (typeof(args[1]) === 'string') {
version = args[0];
url = args[1];
offset = 2;
}
var handlers = _mergeFnArgs(args, offset);
return this._mount('GET', url, handlers, version);
};
http.Server.prototype.head = function(url, handlers) {
if (!url) throw new Error('url is required');
return this._mount('HEAD', url, _mergeFnArgs(arguments));
http.Server.prototype.head = function() {
var args = Array.prototype.slice.call(arguments);
var offset = 1;
var url = args[0];
var version = this._config.defaultVersion;
if (!args[0] || typeof(args[0]) !== 'string')
throw new TypeError('argument 0 must be a string (version or url)');
if (!args[1])
throw new TypeError('argument 1 is required (handler chain or url)');
if (typeof(args[1]) === 'string') {
version = args[0];
url = args[1];
offset = 2;
}
var handlers = _mergeFnArgs(args, offset);
return this._mount('HEAD', url, handlers, version);
};
http.Server.prototype.post = function(url, handlers) {
if (!url) throw new Error('url is required');
return this._mount('POST', url, _mergeFnArgs(arguments));
http.Server.prototype.post = function() {
var args = Array.prototype.slice.call(arguments);
var offset = 1;
var url = args[0];
var version = this._config.defaultVersion;
if (!args[0] || typeof(args[0]) !== 'string')
throw new TypeError('argument 0 must be a string (version or url)');
if (!args[1])
throw new TypeError('argument 1 is required (handler chain or url)');
if (typeof(args[1]) === 'string') {
version = args[0];
url = args[1];
offset = 2;
}
var handlers = _mergeFnArgs(args, offset);
return this._mount('POST', url, handlers, version);
};
http.Server.prototype.put = function(url, handlers) {
if (!url) throw new Error('url is required');
return this._mount('PUT', url, _mergeFnArgs(arguments));
http.Server.prototype.put = function() {
var args = Array.prototype.slice.call(arguments);
var offset = 1;
var url = args[0];
var version = this._config.defaultVersion;
if (!args[0] || typeof(args[0]) !== 'string')
throw new TypeError('argument 0 must be a string (version or url)');
if (!args[1])
throw new TypeError('argument 1 is required (handler chain or url)');
if (typeof(args[1]) === 'string') {
version = args[0];
url = args[1];
offset = 2;
}
var handlers = _mergeFnArgs(args, offset);
return this._mount('PUT', url, handlers, version);
};

@@ -198,33 +198,5 @@ // Copyright 2011 Mark Cavage <mcavage@gmail.com> All rights reserved.

/**
* Fairly ugly and long method that does the brunt of the
* work for setting up the processing chain.
*
* @param {Object} request node.js request.
* @param {Object} response node.js response.
* @param {Function} next the first call in the chain.
*
*/
function _parseRequest(request, response, next) {
assert.ok(request);
assert.ok(response);
assert.ok(next);
if (log.trace()) {
log.trace('_parseRequest:\n%s %s HTTP/%s\nHeaders: %o',
request.method,
request.url,
request.httpVersion,
request.headers);
}
if (!_parseAccept(request, response)) return;
if (!_parseDate(request, response)) return;
// This is so common it's worth checking up front before we read data
// TODO (mcavage) fix this
var contentType = request.contentType();
if (contentType === 'multipart/form-data') {
return response.sendError(newError({
function _parseContentType(request, response) {
if (request.contentType() === 'multipart/form-data') {
response.sendError(newError({
httpCode: HttpCodes.UnsupportedMediaType,

@@ -234,17 +206,19 @@ restCode: RestCodes.InvalidArgument,

}));
return false;
}
return request.contentType();
}
if (request._config.apiVersion) {
if (request.headers[Constants.XApiVersion] ||
(request.headers[Constants.XApiVersion.toLowerCase()] !==
request._config.apiVersion)) {
return response.sendError(newError({
httpCode: HttpCodes.Conflict,
restCode: RestCodes.InvalidArgument,
message: Constants.XApiVersion + ' must be ' +
request._config.apiVersion
}));
}
function _parseApiVersion(request, response) {
if (request.headers[Constants.XApiVersion] && request._serverVersioned) {
request._apiVersion = request.headers[Constants.XApiVersion];
response._apiVersion = request.headers[Constants.XApiVersion];
}
return true;
}
function _parseQueryString(request, response) {
request._url = url.parse(request.url);

@@ -260,3 +234,33 @@ if (request._url.query) {

}
return true;
}
function _parseHead(request, response) {
assert.ok(request);
assert.ok(response);
if (log.trace()) {
log.trace('_parseHead:\n%s %s HTTP/%s\nHeaders: %o',
request.method,
request.url,
request.httpVersion,
request.headers);
}
if (!_parseAccept(request, response)) return false;
if (!_parseDate(request, response)) return false;
if (!_parseApiVersion(request, response)) return false;
if (!_parseQueryString(request, response)) return false;
if (!_parseContentType(request, response)) return false;
return true;
}
function _parseRequest(request, response, next) {
assert.ok(request);
assert.ok(response);
assert.ok(next);
request.body = '';

@@ -287,2 +291,4 @@ request.on('data', function(chunk) {

}
var contentType = request.contentType();
var bParams;

@@ -347,5 +353,2 @@ if (contentType === Constants.ContentTypeFormEncoded) {

* Default: application/json.
* - clockSkew: If a Date header is present, allow N seconds
* of skew.
* Default: 300
* - apiVersion: Default API version to support; setting this

@@ -355,4 +358,7 @@ * means clients are required to send an

* Default: None.
* - clockSkew: If a Date header is present, allow N seconds
* of skew.
* Default: 300
* - logTo: a `Writable Stream` where log messages should go.
* Default: process.stderr
* Default: process.stderr.
*

@@ -367,9 +373,14 @@ * @return {Object} node HTTP server, with restify "magic".

_response = response;
request.requestId = response.requestId = uuid().toLowerCase();
request.startTime = response.startTime = new Date().getTime();
request._serverVersioned = response._serverVersioned = server._versioned;
request.requestId = response.requestId = uuid().toLowerCase();
request._apiVersion = server._config.defaultVersion;
request._config = server._config;
request.params = {};
request.uriParams = {};
response._apiVersion = server._config.defaultVersion;
response._allowedMethods = [];
response._config = server._config;
request.startTime = response.startTime = new Date().getTime();
response._allowedMethods = [];

@@ -381,4 +392,19 @@ var route;

request.url = path;
if (server.routes[request.method]) {
var routes = server.routes[request.method];
if (!_parseHead(request, response)) return;
if (!server.routes[request._apiVersion]) {
if (log.trace()) {
log.trace('restify: no routes (at all) found for version %s',
request._apiVersion);
}
return response.send(HttpCodes.NotFound);
}
if (log.trace())
log.trace('Looking up route for API version: %s', request._apiVersion);
if (server.routes[request._apiVersion][request.method]) {
var routes = server.routes[request._apiVersion][request.method];
for (i = 0; i < routes.length; i++) {

@@ -394,3 +420,3 @@ params = _matches(path, routes[i]);

if (route) {
server.routes.urls[route.url].forEach(function(r) {
server.routes.urls[request._apiVersion][route.url].forEach(function(r) {
response._allowedMethods.push(r.method);

@@ -432,5 +458,9 @@ });

var _code = HttpCodes.NotFound;
for (k in server.routes.urls) {
if (server.routes.urls.hasOwnProperty(k)) {
route = server.routes.urls[k];
for (k in server.routes.urls[request._apiVersion]) {
if (server.routes.urls[request._apiVersion].hasOwnProperty(k)) {
route = server.routes.urls[request._apiVersion][k];
if (log.trace())
log.trace('restify: 405 path: looking at route %o', route);
var _methods = [];

@@ -473,17 +503,9 @@

server._config.apiVersion = null;
var installedExceptionHandler = false;
if (options) {
if (options.apiVersion)
server._config.apiVersion = options.apiVersion;
if (options.serverName)
server._config.serverName = options.serverName;
if (options.exceptionHandler) {
process.on('uncaughtException', options.exceptionHandler);
installedExceptionHandler = true;
}
if (options.apiVersion)
server._config.defaultVersion = options.apiVersion;

@@ -507,2 +529,5 @@ if (options.maxRequestSize)

if (!server._config.defaultVersion)
server._config.defaultVersion = Constants.NoApiVersion;
if (!server._config.serverName)

@@ -509,0 +534,0 @@ server._config.serverName = Constants.DefaultServerName;

{
"name": "restify",
"description": "REST framework specifically meant for web service APIs",
"version": "0.1.13",
"version": "0.1.14",
"repository": {

@@ -16,3 +16,3 @@ "type": "git",

"pretest": "./node_modules/.bin/jshint lib tst",
"test": "./node_modules/.bin/whiskey -t \"`find tst -name *.test.js | xargs`\""
"test": "./node_modules/.bin/whiskey --timeout 500 -t \"`find tst -name *.test.js | xargs`\""
},

@@ -19,0 +19,0 @@ "man": [

@@ -7,5 +7,5 @@ // Copyright 2011 Mark Cavage <mcavage@gmail.com> All rights reserved.

var restify = require('../lib/restify');
restify.log.level(restify.LogLevel.Trace);
// --- Globals

@@ -12,0 +12,0 @@

@@ -104,187 +104,2 @@ // Copyright 2011 Mark Cavage <mcavage@gmail.com> All rights reserved.

// exports.test_create_default_error_handler = function(test, assert) {
// var server = restify.createServer();
// var socket = '/tmp/.' + uuid();
// server.get('/', function(req, res, next) {
// fs.stat('/tmp', function(err, stats) {
// throw new Error('Default me!');
// });
// });
// server.listen(socket, function() {
// var opts = common.newOptions(socket, '/');
// http.request(opts, function(res) {
// common.checkResponse(assert, res);
// assert.equal(res.headers.server, 'node.js');
// assert.equal(res.statusCode, 500);
// server.on('close', function() {
// test.finish();
// });
// server.close();
// }).end();
// });
// };
// exports.test_create_user_error_handler = function(test, assert) {
// var server = restify.createServer({
// onError: function(err, req, res) {
// assert.ok(res);
// res.send(503);
// }
// });
// var socket = '/tmp/.' + uuid();
// server.get('/', function(req, res, next) { throw new Error('503 me!'); });
// server.listen(socket, function() {
// var opts = common.newOptions(socket, '/');
// http.request(opts, function(res) {
// common.checkResponse(assert, res);
// assert.equal(res.headers.server, 'node.js');
// assert.equal(res.statusCode, 503);
// server.on('close', function() {
// test.finish();
// });
// server.close();
// }).end();
// });
// };
exports.test_accept_default = function(test, assert) {
var server = restify.createServer();
var socket = '/tmp/.' + uuid();
server.get('/', _handler);
server.listen(socket, function() {
var opts = common.newOptions(socket, '/');
opts.headers.accept = 'application/json';
http.request(opts, function(res) {
common.checkResponse(assert, res);
assert.equal(res.headers.server, 'node.js');
assert.equal(res.statusCode, 200);
server.on('close', function() {
test.finish();
});
server.close();
}).end();
});
};
exports.test_accept_bad = function(test, assert) {
var server = restify.createServer();
var socket = '/tmp/.' + uuid();
server.get('/', _handler);
server.listen(socket, function() {
var opts = common.newOptions(socket, '/');
opts.headers.accept = 'application/xml';
http.request(opts, function(res) {
common.checkResponse(assert, res);
assert.equal(res.headers.server, 'node.js');
assert.equal(res.statusCode, 406);
server.on('close', function() {
test.finish();
});
server.close();
}).end();
});
};
exports.test_accept_partial_wildcard = function(test, assert) {
var server = restify.createServer();
var socket = '/tmp/.' + uuid();
server.get('/', _handler);
server.listen(socket, function() {
var opts = common.newOptions(socket, '/');
opts.headers.accept = 'application/*';
http.request(opts, function(res) {
common.checkResponse(assert, res);
assert.equal(res.headers.server, 'node.js');
assert.equal(res.statusCode, 200);
server.on('close', function() {
test.finish();
});
server.close();
}).end();
});
};
exports.test_accept_double_wildcard = function(test, assert) {
var server = restify.createServer();
var socket = '/tmp/.' + uuid();
server.get('/', _handler);
server.listen(socket, function() {
var opts = common.newOptions(socket, '/');
opts.headers.accept = '*/*';
http.request(opts, function(res) {
common.checkResponse(assert, res);
assert.equal(res.headers.server, 'node.js');
assert.equal(res.statusCode, 200);
server.on('close', function() {
test.finish();
});
server.close();
}).end();
});
};
exports.test_accept_explicit = function(test, assert) {
var server = restify.createServer({
accept: ['text/html']
});
var socket = '/tmp/.' + uuid();
server.get('/', _handler);
server.listen(socket, function() {
var opts = common.newOptions(socket, '/');
opts.headers.accept = 'text/html';
http.request(opts, function(res) {
common.checkResponse(assert, res);
assert.equal(res.headers.server, 'node.js');
assert.equal(res.statusCode, 200);
server.on('close', function() {
test.finish();
});
server.close();
}).end();
});
};
exports.test_multiple_accept = function(test, assert) {
var server = restify.createServer({
accept: ['text/html', 'text/xml']
});
var socket = '/tmp/.' + uuid();
server.get('/', _handler);
server.listen(socket, function() {
var opts = common.newOptions(socket, '/');
opts.headers.accept = 'text/xml';
http.request(opts, function(res) {
common.checkResponse(assert, res);
assert.equal(res.headers.server, 'node.js');
assert.equal(res.statusCode, 200);
server.on('close', function() {
test.finish();
});
server.close();
}).end();
});
};
exports.test_server_name = function(test, assert) {

@@ -370,2 +185,3 @@ var server = restify.createServer({

http.request(opts, function(res) {
res._skipAllowedMethods = true;
common.checkResponse(assert, res);

@@ -380,97 +196,1 @@ assert.equal(res.statusCode, 400);

};
exports.test_log_stdout = function(test, assert) {
var server = restify.createServer({
logTo: process.stdout
});
var socket = '/tmp/.' + uuid();
server.get('/', _handler);
server.listen(socket, function() {
var opts = common.newOptions(socket, '/');
http.request(opts, function(res) {
common.checkResponse(assert, res);
assert.equal(res.statusCode, 200);
server.on('close', function() {
log.writeTo(process.stderr);
test.finish();
});
server.close();
}).end();
});
};
exports.test_log_off = function(test, assert) {
log.level(log.Level.Off);
var server = restify.createServer();
var socket = '/tmp/.' + uuid();
server.get('/', _handler);
server.listen(socket, function() {
var opts = common.newOptions(socket, '/');
http.request(opts, function(res) {
common.checkResponse(assert, res);
assert.equal(res.statusCode, 200);
server.on('close', function() {
log.level(log.Level.Trace);
test.finish();
});
server.close();
}).end();
});
};
exports.test_options_with_resource = function(test, assert) {
var server = restify.createServer();
var socket = '/tmp/.' + uuid();
server.get('/', _handler);
server.put('/', _handler);
server.del('/', _handler);
server.listen(socket, function() {
var opts = common.newOptions(socket, '/');
opts.method = 'OPTIONS';
http.request(opts, function(res) {
common.checkResponse(assert, res);
assert.equal(res.statusCode, 200);
assert.ok(res.headers.allow);
assert.ok(res.headers.allow, 'GET, PUT, DELETE');
server.on('close', function() {
test.finish();
});
server.close();
}).end();
});
};
exports.test_options_wildcard_resource = function(test, assert) {
var server = restify.createServer();
var socket = '/tmp/.' + uuid();
server.get('/', _handler);
server.put('/', _handler);
server.del('/', _handler);
server.listen(socket, function() {
var opts = common.newOptions(socket, '*');
opts.method = 'OPTIONS';
http.request(opts, function(res) {
res._skipAllowedMethods = true;
common.checkResponse(assert, res);
assert.equal(res.statusCode, 200);
assert.ok(!res.headers.allow);
server.on('close', function() {
test.finish();
});
server.close();
}).end();
});
};

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

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