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

alexa-router

Package Overview
Dependencies
Maintainers
1
Versions
2
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

alexa-router - npm Package Compare versions

Comparing version 1.0.0 to 2.0.0

circle.yml

98

lib/errors.js

@@ -63,2 +63,93 @@ 'use strict'

/**
* InvalidCertificateUri class
*/
class InvalidCertificateUri extends Error {
/**
* Create a new InvalidCertificateUri
* @param {String} uri The invalid certificate's URI
* @constructs InvalidCertificateUri
*/
constructor (uri) {
let message = `Unable to valid the certificate's URI as a valid Amazon URI: ${uri}`
super(message)
this.name = this.constructor.name
this.message = message
this.code = 'IC01'
}
}
/**
* InvalidCertificate class
*/
class InvalidCertificate extends Error {
/**
* Create a new InvalidCertificate
* @constructs InvalidCertificate
*/
constructor () {
let message = 'Unable to validate a signature certificate'
super(message)
this.name = this.constructor.name
this.message = message
this.code = 'IC02'
}
}
/**
* InvalidSignature class
*/
class InvalidSignature extends Error {
/**
* Create a new InvalidSignature
* @constructs InvalidSignature
*/
constructor () {
let message = 'Unable to validate a signature'
super(message)
this.name = this.constructor.name
this.message = message
this.code = 'IS00'
}
}
/**
* ExpiredRequest class
*/
class ExpiredRequest extends Error {
/**
* Create a new ExpiredRequest
* @constructs ExpiredRequest
*/
constructor () {
let message = 'Unable to handle a request because the timestamp expired'
super(message)
this.name = this.constructor.name
this.message = message
this.code = 'ER01'
}
}
/**
* InvalidApplicationId class
*/
class InvalidApplicationId extends Error {
/**
* Create a new InvalidApplicationId
* @constructs InvalidApplicationId
*/
constructor () {
let message = 'Unable to handle a request because of an invalid application ID'
super(message)
this.name = this.constructor.name
this.message = message
this.code = 'IA01'
}
}
/**
* ValidationError class

@@ -89,3 +180,8 @@ */

RoutingFailed,
ValidationError
ValidationError,
InvalidCertificateUri,
InvalidCertificate,
InvalidSignature,
ExpiredRequest,
InvalidApplicationId
}

2

lib/request.js

@@ -69,3 +69,3 @@ 'use strict'

* Returns the next flow for this request
* @return {Object} An object containing a valid next flow
* @return {Array} An object containing a valid next flow
*/

@@ -72,0 +72,0 @@ next () {

@@ -60,3 +60,3 @@ 'use strict'

/**
* Setups the output speech and automatically detects SSML
* Sets up the output speech and automatically detects SSML
* @param {String} text The text that Alexa will say to the user

@@ -63,0 +63,0 @@ * @return {Object} An object with the speech if the parameter is undefined

@@ -5,2 +5,7 @@ 'use strict'

let Joi = require('joi')
let url = require('url')
let path = require('path')
let request = require('request-promise')
let crypto = require('crypto')
let x509 = require('x509')

@@ -18,8 +23,20 @@ let helpers = require('./helpers')

* @constructs AlexaRouter
* @param {Object} [config]
* @param {Object} config
* @param {String[]} config.appId Your application ID or an array with many IDs
* @param {Boolean} [config.routeIntentOnly=true] Only deal with intent requests
* @param {Boolean} [config.verifySignature=true] Whenever to validate the request's signature
* @param {Boolean} [config.verifyTimestamp=true] Whenever to validate the request's timestamp
* @param {Boolean} [config.verifyApplication=true] Whenever to validate the request's application ID
*/
constructor (config) {
this.config = _.defaults(config, { routeIntentOnly: true })
this.config = helpers.validate(config, Joi.object({
appId: Joi.array().items(Joi.string()).single().required(),
routeIntentOnly: Joi.bool().default(true),
verifySignature: Joi.bool().default(true),
verifyTimestamp: Joi.bool().default(true),
verifyApplicationId: Joi.bool().default(true)
}))
this.actions = {}
this.certs = {}
}

@@ -60,15 +77,17 @@

* @param {Object} alexaData A valid Alexa request
* @param {Object} headers The headers present in the incoming request
* @return {Promise(Response)} A promise that resolves to a Response instance
*/
dispatch (alexaData) {
dispatch (alexaData, headers) {
return new Promise((resolve, reject) => {
let request = new Request(alexaData, this)
if (this.config.routeIntentOnly && request.type !== 'intent') {
return resolve({})
}
let action = this._actionDiscovery(request)
return Promise.resolve(action()).then(resolve, reject)
Promise.resolve(this._checkSignature(alexaData, headers))
.then(() => {
if (this.config.routeIntentOnly && request.type !== 'intent') {
return {}
} else {
return this._actionDiscovery(request)()
}
})
.then(resolve, reject)
})

@@ -133,4 +152,109 @@ }

}
/**
* Check, retrieve and cache certificates from Amazon
* @param {String} uri The certificate's URI
* @throws {InvalidCertificateUri} If the URI is untrusted
* @return {Promise(String)} A promise that resolves to the certificate
*/
_retrieveCertificate (uri) {
let uriData = url.parse(uri)
return new Promise((resolve, reject) => {
if (this.certs[uri]) {
return resolve(this.certs[uri])
} else if (uriData.protocol.toLowerCase() !== 'https:' ||
uriData.hostname.toLowerCase() !== 's3.amazonaws.com' ||
!path.normalize(uriData.path).startsWith('/echo.api/') ||
(uriData.port !== null && uriData.port !== '443')) {
reject(new errors.InvalidCertificateUri(uri))
} else {
this.certs[uri] = request.get(uri)
.then(cert => this._checkCert(cert))
return resolve(this.certs[uri])
}
})
}
/**
* Check if the certificate is valid
* @param {String} cert The certificate to be validated
* @throws {InvalidCertificate} If cert is invalid
* @return {String} The cert
*/
_checkCert (cert) {
let date = new Date()
let data
try {
data = x509.parseCert(cert)
} catch (_) {
throw new errors.InvalidCertificate()
}
// TODO find a way to test this with a custom certificate
/* $lab:coverage:off$ */
if (new Date(data.notBefore) >= date ||
new Date(data.notAfter) <= date ||
data.altNames.indexOf('echo-api.amazon.com') === -1) {
throw new errors.InvalidCertificate()
} else {
return cert
}
/* $lab:coverage:on$ */
}
/**
* Check if the incoming request has a valid timestamp
* @param {Object} payload The incoming payload
* @throws {ExpiredRequest} If the request has an invalid timestamp
*/
_checkTimestamp (payload) {
let limit = new Date(new Date().getTime() - 15 * 1000)
if (this.config.verifyTimestamp === true && new Date(payload.request.timestamp) < limit) {
throw new errors.ExpiredRequest()
}
}
/**
* Check if the request has a valid application id
* @param {Object} payload The incoming payload
* @throws {InvalidApplicationId} If the request has an invalid application ID
*/
_checkAppId (payload) {
if (this.config.verifyApplicationId === true &&
this.config.appId.indexOf(payload.session.application.applicationId) === -1) {
throw new errors.InvalidApplicationId()
}
}
/**
* Check the signature of the incoming request to make sure it's from Amazon
* @param {Object} payload The incoming payload
* @param {Object} headers The headers present in the original request
* @throws {InvalidSignature} If the request have an invalid signature
* @return {Promise} A promise that resolves if the requests is valid
*/
_checkSignature (payload, headers) {
if (this.config.verifySignature === true) {
return this._retrieveCertificate(headers['signaturecertchainurl'])
.then(cert => {
let verify = crypto.createVerify('RSA-SHA1')
verify.write(JSON.stringify(payload))
if (!verify.verify(new Buffer(cert), new Buffer(headers.signature, 'base64'))) {
throw new errors.InvalidSignature()
} else {
this._checkTimestamp(payload)
this._checkAppId(payload)
}
})
} else {
return Promise.resolve()
}
}
}
module.exports = AlexaRouter
{
"name": "alexa-router",
"version": "1.0.0",
"version": "2.0.0",
"description": "Easily build custom skills for Alexa",

@@ -26,2 +26,3 @@ "scripts": {

"devDependencies": {
"co": "^4.6.0",
"code": "^3.0.1",

@@ -33,4 +34,7 @@ "lab": "^10.9.0",

"joi": "^9.0.0",
"lodash": "^4.13.1"
"lodash": "^4.13.1",
"request": "^2.73.0",
"request-promise": "^4.0.2",
"x509": "^0.2.6"
}
}
# alexa-router
[![Build Status](https://circleci.com/gh/estate/alexa-router.svg?style=shield)](https://circleci.com/gh/estate/alexa-router)
[![Code Climate](https://codeclimate.com/github/estate/alexa-router/badges/gpa.svg)](https://codeclimate.com/github/estate/alexa-router)
[![Test Coverage](https://codeclimate.com/github/estate/alexa-router/badges/coverage.svg)](https://codeclimate.com/github/estate/alexa-router/coverage)
[![Version](https://badge.fury.io/js/alexa-router.svg)](http://badge.fury.io/js/alexa-router)
[![Downloads](http://img.shields.io/npm/dm/alexa-router.svg)](https://www.npmjs.com/package/alexa-router)
The `alexa-router` project allows you to easily develop custom skills for
Amazon's Alexa.
### Getting started
## Why
`alexa-router` makes it easy for you to build custom [Alexa](https://developer.amazon.com/alexa)
skills with complex request/response flows.
All you need to do is `npm install -s alexa-router` and you're already done
with the setup.
## Install
```bash
$ npm install -S alexa-router
```
### Understanding actions
## Usage
The router is configured through actions, next options and globals.
`alexa-router` is available via an instance of the `Router`. Make sure you begin by initializing the
`Router`.
```javascript
let Alexa = require('alexa-router')
let alexa = new Alexa.Router()
let alexa = new Alexa.Router({
appId: 'my-app-id'
})
```
// A simple action, note that the user would never be able to reach this
// action without being present in the next options of some response
alexa.action('hello-world', {
handler: request => {
let response = request.response()
response.speech('Hello world!')
Once you initialize the router, you can either configure `actions` or `dispatch` a HTTP request to be
routed to the actions you have configured.
return response
}
## `Router`
### API
`new Alexa.Router(config)`
### config
*Required* <br>
Type: `Object`
`config.appId` <br>
*Required* <br>
Type: `String[]`
Your application ID or an array with many
`config.routeIntentOnly` <br>
*Optional* <br>
Type: `Boolean` <br>
Default: `true`
Try to route `IntentRequest` only
`config.verifySignature` <br>
*Optional* <br>
Type: `Boolean` <br>
Default: `true`
Verifies the incoming request against a valid Amazon signature to prevent request forgery.
Amazon [requires verification](https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/developing-an-alexa-skill-as-a-web-service#Verifying) as part of the skill submission process
`config.verifyTimestamp` <br>
*Optional* <br>
Type: `Boolean` <br>
Default: `true`
Verifies if the incoming request have a valid timestamp to prevent replay attacks
`config.verifyAppId` <br>
*Optional* <br>
Type: `Boolean` <br>
Default: `true`
Verifies if the incoming request have a valid application ID to prevent replay attacks
from other applications
### Examples
```javascript
let alexa = new Alexa.Router({
appId: 'my-id',
verifySignature: false
})
```
// An action that can be activated by an incoming intent
## `alexa.action`
Routes are defined via the `action` method
### API
`alexa.action(name, config)`
#### name
*Required* <br>
Type: `String`
The action name. You can reference this action by its name when defining complex action flows.
#### config
*Required* <br>
Type: `Object`
#### `config.handler(request[, params])` <br>
*Required* <br>
Type: `Function`
The handler receives a decorated HTTP request and optionally receives params if they were configured to
be passed by a previous action.
##### request <br>
Type: `Object`
The decorated [Alexa Request Body](https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/alexa-skills-kit-interface-reference#Request Format) with additional methods.
`request.next()` <br>
Returns: `Array` <br>
An array of Next Action Objects set by the previous response in the session
`request.response()` <br>
Returns: `Object` <br>
A decorated [Alexa Response Object](https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/alexa-skills-kit-interface-reference#response-object)
```javascript
// Instantiate a new Response object
let response = request.response()
```
`response.session(key, value)`<br>
Param: key `String`<br>
Param: value `String|Object`
Sets, patches, or retrieves the session's attributes
`response.speech(text)`<br>
Param: text `String`<br>
Convenience method to set speech with raw text or [SSML](https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/speech-synthesis-markup-language-ssml-reference)
`response.reprompt(text)`<br>
Param: text `String`<br>
Convenience method to set reprompt with raw text or [SSML](https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/speech-synthesis-markup-language-ssml-reference)
`response.card(card)` <br>
Param: card `Object` a valid [Alexa Card Object](https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/alexa-skills-kit-interface-reference#Response Format) <br>
Convenience method to set the response card
`response.endSession(shouldEndSession)` <br>
Param: shouldEndSession `Boolean` <br>
A convenience to set the response `shouldEndSession` property
`response.clearSession()` <br>
Clears the current session
`response.next(config)` <br>
Param: config `Object|Array` <br>
If you pass an object it will be merged with any previous Next Action Objects that were passed
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <b>The Next Action Object</b> <br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; type <br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; *Required* <br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Type: `String`<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; One of 'intent', 'launch', 'sessionEnded', or 'unexpected'
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; action <br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; *Required* <br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Type: `String` <br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; The name of the action that should be called if this route is activated
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; intent <br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; *Required if type === 'intent'* <br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Type: `String`
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; The custom or [built-in](https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/implementing-the-built-in-intents)
intent that this action should be associated with. e.g. 'AMAZON.YesIntent'
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; params <br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; *Optional* <br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Type: `Any`
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Any data you'd like to pass to follow request if this route is activated
##### params <br>
Type: `Object`<br>
Params set by a previous action
#### `config.global` <br>
*Optional* <br>
Type: `Object`
Actions with the global key are accessible at any point in the routing flow (like a catch-all). These actions can be
used to kick-off a new flow, interrupt an existing flow, etc. An action to help the user know what
commands are available or cancel the request are two examples for where you might use a global action.
`config.global.type` <br>
*Required* <br>
Type: `String`
One of 'intent', 'launch', 'sessionEnded', or 'unexpected'
`config.global.intent` <br>
*Required if type === 'intent'* <br>
Type: `String`
The custom or [built-in](https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/implementing-the-built-in-intents)
intent that this action should be associated with. e.g. 'AMAZON.YesIntent'
### Examples
A simple action that can be activated by an incoming intent
```javascript
alexa.action('global-hello', {

@@ -38,60 +225,79 @@ handler: request => {...},

})
```
// An action that can be activated by an incoming intent
alexa.action('here-be-dragons', {
You can also chain requests by responding with a list of possible actions that could be next in the interaction flow
```javascript
alexa.action('event-create', {
handler: request => {
let response = request.response()
response.speech('Sorry, you can\'t activate that command right now')
}
})
response.speech('What\'s the name of your event?')
// Finally, you can control the user options by setting the next available commands
alexa.action('say-the-weather', {
handler: (request, params) => {
let response = request.response()
if (params.sayWeather) {
response.speech('If you can\'t see the sky then it\'s probably cloudy')
response.endSession(true)
} else {
response.speech('Would you like me to read the weather?')
// The user can say yes
response.next({
// You can define the next actions by passing an array of actions that can come next
response.next([
{
type: 'intent',
intent: 'AMAZON.YesIntent',
action: 'say-the-weather', // call me again
params: { sayWeather: yes } // Will be passed as the parameters if the user says Yes
})
intent: 'EventName', // Custom intent
action: 'event-create-name'
params: { createdAt: new Date() } // Params will be passed to the `event-create-name` handler
},
{
type: 'intent',
intent: 'AMAZON.CancelIntent', // Built-in intent
action: 'event-cancel'
}
])
// If the user send another intent that isn't in the options they will be
// routed to here-be-dragons in the next interaction. You can also
// pass an array of next options for convenience response.next([...])
// Calling next will always merge new next options with previous ones
response.next({
type: 'unexpected',
action: 'here-be-dragons'
})
}
// You can also pass an individual object and it will be merged with the previous ones
response.next({
type: 'unexpected',
action: 'event-unexpected'
})
// Reply with the response or a promise that resolves to the response
return response
},
global: {
type: 'intent',
intent: 'EventCreate' // Custom intent
}
})
// This action does not have the global attribute so it can only be accessed if passed
// as a `next` action
alexa.action('event-create-name', {
handler: (request, params) => {...}
})
```
### Understanding the routing mechanism
## `alexa.dispatch`
How the internal router works?
The dispatch method takes a HTTP request and routes it to the appropriate action
1. Check if the incoming request has next options
1. If next options are present try to resolve the next action
2. If no action was resolved see if there's an `unexpected` next configured
3. If there's no `unexpected` next then try to find a global `unexpected`
4. If no global `unexpected` then throw `RoutingFailed` error
2. If no next options are present in the request's session then try to match a global action
### API
`alexa.dispatch(requestBody, headers)`
#### requestBody
*Required* <br>
Type: 'Object'
The HTTP request body
#### Headers
*Required* <br>
Type: 'Object'
The headers present in the original incoming request
## Understanding the routing mechanism
1. Check if the incoming request was configured with `next` actions
1. If `next` actions are present, try to resolve the next action
2. If no action was resolved, check for an `unexpected` type `next` option
2. If no next actions are present in the request's session, try to match a global action
3. If no global action was found try to find an `unexpected` global action
4. If no `unexpected` global action then throw `RoutingFailed`
### HTTP handling
## HTTP handling

@@ -107,3 +313,5 @@ `alexa-router` is HTTP server agnostic. This means that you can use it with

let app = express()
let alexa = new Alexa.Router()
let alexa = new Alexa.Router({
appId: 'my-app-id'
})

@@ -113,5 +321,5 @@ // Do all your routing configs

// Configure a route for passing a JSON to alexa-router and reply with a JSON too
// Configure a route for passing JSON to alexa-router
app.post('/alexa/incoming', bodyParser.json(), (req, res) => {
alexa.dispatch(req.body)
alexa.dispatch(req.body, req.headers)
.then(result => res.json(result))

@@ -124,3 +332,4 @@ .catch(err => {

### To-do
## To-do
- [ ] Add plugin support

@@ -130,3 +339,3 @@ - [ ] Add more testing cases

### Testing
## Testing

@@ -133,0 +342,0 @@ ```bash

@@ -5,2 +5,4 @@ 'use strict'

let expect = require('code').expect
let request = require('request-promise')
let co = require('co')

@@ -31,3 +33,7 @@ let Alexa = require('../')

lab.test('should be able to override defaults', (cb) => {
let alexa = new Alexa.Router({ routeIntentOnly: false })
let alexa = new Alexa.Router({
routeIntentOnly: false,
appId: 'hello-world'
})
expect(alexa.config.routeIntentOnly).to.be.false()

@@ -195,2 +201,97 @@

})
lab.test('should throw trying to retrieve a certificate from a malicious source', co.wrap(function * () {
let alexa = fixtures.simpleRouting()
let errors = yield {
one: alexa._retrieveCertificate('http://malicious.com').catch(err => err),
two: alexa._retrieveCertificate('https://malicious.com').catch(err => err),
three: alexa._retrieveCertificate('https://s3.amazonaws.com/malicious').catch(err => err),
four: alexa._retrieveCertificate('https://s3.amazonaws.com:666/echo.api/valid').catch(err => err),
five: alexa._retrieveCertificate('https://s3.amazonaws.com/echo.api/valid').catch(err => err),
six: alexa._retrieveCertificate(fixtures.VALID_CERL_URL).catch(err => err)
}
expect(errors.one).to.be.instanceof(Alexa.errors.InvalidCertificateUri)
expect(errors.two).to.be.instanceof(Alexa.errors.InvalidCertificateUri)
expect(errors.three).to.be.instanceof(Alexa.errors.InvalidCertificateUri)
expect(errors.four).to.be.instanceof(Alexa.errors.InvalidCertificateUri)
expect(errors.five).to.not.be.instanceof(Alexa.errors.InvalidCertificateUri)
expect(errors.six).to.equal(yield request(fixtures.VALID_CERL_URL))
}))
lab.test('should cache certificates', co.wrap(function * () {
let alexa = fixtures.simpleRouting()
alexa.certs['hello-world'] = Promise.resolve('Hello world!')
expect(yield alexa._retrieveCertificate('hello-world')).to.equal('Hello world!')
}))
lab.test('should throw for invalid certificates', co.wrap(function * () {
let alexa = fixtures.simpleRouting()
expect(() => alexa._checkCert('not-a-valid-cert')).to.throw(Alexa.errors.InvalidCertificate)
}))
lab.test('should throw for expired requests', co.wrap(function * () {
let alexa = fixtures.simpleRouting()
alexa.config.verifyTimestamp = true
expect(() => alexa._checkTimestamp({
request: { timestamp: new Date(new Date().getTime() - 16 * 1000) }
})).to.throw(Alexa.errors.ExpiredRequest)
expect(() => alexa._checkTimestamp({
request: { timestamp: new Date() }
})).to.not.throw()
alexa.config.verifyTimestamp = false
expect(() => alexa._checkTimestamp({
request: { timestamp: new Date(new Date().getTime() - 16 * 1000) }
})).to.not.throw()
}))
lab.test('should check application id', co.wrap(function * () {
let alexa = fixtures.simpleRouting()
alexa.config.verifyApplicationId = true
expect(() => alexa._checkAppId({
session: {
application: { applicationId: 'not-my-app' }
}
})).to.throw(Alexa.errors.InvalidApplicationId)
expect(() => alexa._checkAppId({
session: {
application: { applicationId: 'hello-world' }
}
})).to.not.throw()
alexa.config.verifyApplicationId = false
expect(() => alexa._checkAppId({
session: {
application: { applicationId: 'not-my-app' }
}
})).to.not.throw()
}))
lab.test('should validate signature', co.wrap(function * () {
let alexa = fixtures.simpleRouting()
alexa.config.verifySignature = true
alexa.certs[fixtures.VALID_CERL_URL] = Promise.resolve(fixtures.VALID_CERT)
let errors = yield {
one: alexa._checkSignature(JSON.parse(fixtures.VALID_PAYLOAD), {
signature: fixtures.VALID_SIGNATURE,
signaturecertchainurl: fixtures.VALID_CERL_URL
}).catch(err => err),
two: alexa._checkSignature(JSON.parse(fixtures.VALID_PAYLOAD), {
signature: fixtures.VALID_SIGNATURE.substring(1),
signaturecertchainurl: fixtures.VALID_CERL_URL
}).catch(err => err)
}
expect(errors.one).to.be.undefined()
expect(errors.two).to.be.instanceof(Alexa.errors.InvalidSignature)
}))
})
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