alexa-router
Advanced tools
Comparing version 1.0.0 to 2.0.0
@@ -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 | ||
} |
@@ -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" | ||
} | ||
} |
335
README.md
# 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 | ||
<b>The Next Action Object</b> <br> | ||
type <br> | ||
*Required* <br> | ||
Type: `String`<br> | ||
One of 'intent', 'launch', 'sessionEnded', or 'unexpected' | ||
action <br> | ||
*Required* <br> | ||
Type: `String` <br> | ||
The name of the action that should be called if this route is activated | ||
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' | ||
params <br> | ||
*Optional* <br> | ||
Type: `Any` | ||
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) | ||
})) | ||
}) |
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
56267
17
1158
341
5
4
1
+ Addedrequest@^2.73.0
+ Addedrequest-promise@^4.0.2
+ Addedx509@^0.2.6
+ Addedajv@6.12.6(transitive)
+ Addedasn1@0.2.6(transitive)
+ Addedassert-plus@1.0.0(transitive)
+ Addedasynckit@0.4.0(transitive)
+ Addedaws-sign2@0.7.0(transitive)
+ Addedaws4@1.13.2(transitive)
+ Addedbcrypt-pbkdf@1.0.2(transitive)
+ Addedbluebird@3.7.2(transitive)
+ Addedcaseless@0.12.0(transitive)
+ Addedcombined-stream@1.0.8(transitive)
+ Addedcore-util-is@1.0.2(transitive)
+ Addeddashdash@1.14.1(transitive)
+ Addeddelayed-stream@1.0.0(transitive)
+ Addedecc-jsbn@0.1.2(transitive)
+ Addedextend@3.0.2(transitive)
+ Addedextsprintf@1.3.0(transitive)
+ Addedfast-deep-equal@3.1.3(transitive)
+ Addedfast-json-stable-stringify@2.1.0(transitive)
+ Addedforever-agent@0.6.1(transitive)
+ Addedform-data@2.3.3(transitive)
+ Addedgetpass@0.1.7(transitive)
+ Addedhar-schema@2.0.0(transitive)
+ Addedhar-validator@5.1.5(transitive)
+ Addedhttp-signature@1.2.0(transitive)
+ Addedis-typedarray@1.0.0(transitive)
+ Addedisstream@0.1.2(transitive)
+ Addedjsbn@0.1.1(transitive)
+ Addedjson-schema@0.4.0(transitive)
+ Addedjson-schema-traverse@0.4.1(transitive)
+ Addedjson-stringify-safe@5.0.1(transitive)
+ Addedjsprim@1.4.2(transitive)
+ Addedmime-db@1.52.0(transitive)
+ Addedmime-types@2.1.35(transitive)
+ Addednan@2.2.0(transitive)
+ Addedoauth-sign@0.9.0(transitive)
+ Addedperformance-now@2.1.0(transitive)
+ Addedpsl@1.15.0(transitive)
+ Addedpunycode@2.3.1(transitive)
+ Addedqs@6.5.3(transitive)
+ Addedrequest@2.88.2(transitive)
+ Addedrequest-promise@4.2.6(transitive)
+ Addedrequest-promise-core@1.1.4(transitive)
+ Addedsafe-buffer@5.2.1(transitive)
+ Addedsafer-buffer@2.1.2(transitive)
+ Addedsshpk@1.18.0(transitive)
+ Addedstealthy-require@1.1.1(transitive)
+ Addedtough-cookie@2.5.0(transitive)
+ Addedtunnel-agent@0.6.0(transitive)
+ Addedtweetnacl@0.14.5(transitive)
+ Addeduri-js@4.4.1(transitive)
+ Addeduuid@3.4.0(transitive)
+ Addedverror@1.10.0(transitive)
+ Addedx509@0.2.6(transitive)