diff --git a/.gitignore b/.gitignore index 6f10471..11f808e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,7 @@ node_modules npm-debug.log dist coverage -doc +docs +config/* +-config/index.js +-config/test.js diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index c657c76..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,48 +0,0 @@ -# Community Contributing Guide - -Contributions are always welcome, no matter how large or small, from everyone. - -If you'd like to help make Connector better, you totally rock! Here are some ways to contribute: -- by adding new messaging channels -- by reporting bugs you encounter or suggesting new features -- by improving the documentation -- by improving existing code -- by blackfilling unit tests for modules that lack coverage - -If you want to add a new channel, please check this [wiki page](https://github.com/RecastAI/bot-connector/wiki/01---Add-a-channel) for more information. - -## Guidelines - -* Tests must pass -* Follow the existing coding style -* If you fix a bug, add a test - -## Steps for Contributing - -* create an issue with the bug you want to fix, or the feature that you want to add -* create your own fork on github -* write your code in your local copy -* make the tests and lint pass -* if everything is fine, commit your changes to your fork and create a pull request from there - -## Setup - -#### Installation -```sh -$ git clone https://github.com/RecastAI/Connector.git -$ cd Connector -$ yarn -``` - -#### Running in development mode (hot reload) - -```sh -$ yarn start-dev -``` - -#### Testing - -```sh -$ yarn test -$ yarn lint -``` diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4dde146 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM node:carbon + +# Create app directory +WORKDIR /usr/src/app + +# Install app dependencies +# A wildcard is used to ensure both package.json AND package-lock.json are copied +# where available (npm@5+) +COPY package*.json ./ + +RUN npm install +# If you are building your code for production +# RUN npm install --only=production + +# Bundle app source +COPY . . +RUN npm run build +EXPOSE 3004 +CMD [ "node", "dist/index.js" ] \ No newline at end of file diff --git a/ISSUE.md b/ISSUE.md deleted file mode 100644 index 4bb3328..0000000 --- a/ISSUE.md +++ /dev/null @@ -1,7 +0,0 @@ -# Troubleshooting - -If you encounter an issue with Connector, please check first if it doesn't already exist. -If it's doesn't, create an issue with the following content: -* Connector's version you're using -* the behavior you expect, and the one you see -* if possible, the step to reproduce the unwanted behavior diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..276d192 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 SAP + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 316c40b..e4138ee 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,21 @@ -![Bot Conector Logo](https://cdn.recast.ai/bot-connector/bot-connector-logo.png) +# [Archived] Open Source Bot Connector -# Bot Connector +As communicated earlier in August 2019, this repository has been archived and the source code is not maintained by the SAP Conversational AI team. + +All Bot Connector capabilities are integrated in our [bot building platform](https://cai.tools.sap). + +If you are running a standard / customised version of open source bot connector on your platform, **please migrate to the bot connector available on our bot building platform** (hosted in on SAP Cloud Platform), which offers integration with a wide number of channels which that we plan to make more robust. + +If you have any questions, please [contact our team](https://cai.tools.sap/contact). +## +![Bot Connector Logo](https://cdn.cai.tools.sap/bot-connector/bot-connector-logo.png) + +| [Supported Channels](#supported-channels) | [Getting Started](#getting-started) | [How it works](#how-it-works) | [Messages Formats](#messages-format) | [Getting Started with SAP Conversational AI]( #getting-started-with-sap-conversational-ai) | +|---|---|---|---|---| + +[💬 Questions / Comments? Join the discussion on our community Slack channel!](https://slack.cai.tools.sap) + +## Bot Connector Bot Connector allows you to connect your bot to multiple messaging channels. @@ -8,69 +23,114 @@ It provides a higher level API to manage several messaging platforms at once, an ## Documentation -You can see the API documentation [here](https://recastai.github.io/bot-connector/) +You can see the API documentation [here](https://sapconversationalai.github.io/bot-connector/) Or generate the documentation with the following command: ```bash -yarn doc && open doc/index.html +yarn docs && open docs/index.html ``` ## Supported Channels Bot Connector supports the following channels: -* [Kik](https://github.com/RecastAI/bot-connector/wiki/Channel---Kik) -* [Slack](https://github.com/RecastAI/bot-connector/wiki/Channel---Slack) -* [Messenger](https://github.com/RecastAI/bot-connector/wiki/Channel---Messenger) -* [Callr](https://github.com/RecastAI/bot-connector/wiki/Channel-CALLR) -* [Telegram](https://github.com/RecastAI/bot-connector/wiki/Channel-Telegram) -* [Twilio](https://github.com/RecastAI/bot-connector/wiki/Channel-Twilio) -You can find more information on each channel in the [wiki](https://github.com/RecastAI/bot-connector/wiki) +* [Kik](https://github.com/SAPConversationalAI/bot-connector/wiki/Channel---Kik) +* [Slack](https://github.com/SAPConversationalAI/bot-connector/wiki/Channel---Slack) +* [Facebook Messenger](https://github.com/SAPConversationalAI/bot-connector/wiki/Channel---Messenger) +* [Callr](https://github.com/SAPConversationalAI/bot-connector/wiki/Channel-CALLR) +* [Telegram](https://github.com/SAPConversationalAI/bot-connector/wiki/Channel-Telegram) +* [Twilio](https://github.com/SAPConversationalAI/bot-connector/wiki/Channel-Twilio) +* [Cisco Webex](https://github.com/SAPConversationalAI/bot-connector/wiki/Channel-Cisco) +* [Microsoft Bot Framework (Skype, Teams, Cortana,...)](https://github.com/SAPConversationalAI/bot-connector/wiki/Channel-Microsoft-Bot-Framework) +* [Twitter](https://github.com/SAPConversationalAI/bot-connector/wiki/Channel-Twitter) +* Line + +You can find more information on each channel in the [wiki](https://github.com/SAPConversationalAI/bot-connector/wiki) -More will be added, and you can [contribute](https://github.com/RecastAI/bot-connector/blob/master/CONTRIBUTING.md) if you want to, and add a thumbs up for the channel you want to see implemented first ;) +More will be added, and you can contribute if you want to, and add a thumbs up for the channel you want to see implemented first ;) (To do so, fork this repo, add a thumbs up and make a PR!) -* Cisco Spark 👍👍 -* Discord 👍👍 -* Line 👍 -* Microsoft Teams 👍 + +* Discord 👍👍👍 * Ryver 👍 -* Skype 👍 * Viber * Wechat 👍👍 * Zinc.it 👍 -* Twitter 👍 -* Salesforce - -You can find the current roadmap [here](https://github.com/RecastAI/bot-connector/projects/1). +* Salesforce 👍 ## Getting started The following examples use [yarn](https://github.com/yarnpkg/yarn) package manager but you can use your favorite one like npm, or pnpm. -In order to run the connector you need MongoDB installed and running. The configuration files for MongoDB are stored in *config* directory. +In order to run the connector you need MongoDB and Redis installed and running. The configuration files for both are stored in *config* directory. ### Installation Clone the repository and install the dependencies ```sh -git clone https://github.com/RecastAI/bot-connector.git +git clone https://github.com/SAPConversationalAI/bot-connector.git cd bot-connector yarn install ``` +### Available Commands + +* `yarn start` - Start application in production mode +* `yarn start:dev` - Start application in development mode +* `yarn start:dev:debug` - Start application in development mode with debugger +* `yarn test` - Run unit & integration tests +* `yarn test:debug` - Run unit & integration tests with debugger +* `yarn test:coverage` - Run unit & integration tests with coverage report +* `yarn lint` - Run ESLint +* `yarn build` - Build artifacts for production +* `yarn docs` - Generate apidoc documentation + +#### Configurations/Environments + +You need to create a configuration file based on the following schema: + +config/{env}.js (e.g. `config/development.js` for `NODE_ENV=development`) + +``` +module.exports = { + db: { + host: 'localhost', + port: 27017, + dbName: 'botconnector', + }, + server: { + port: 8080, + }, + redis: { + port: 6379, + host: 'localhost', + auth: '', + db: 7, + options: {}, // see https://github.com/mranney/node_redis#rediscreateclient + }, + mail: {}, // valid object to be passed to nodemail.createTransport() + base_url: '', // base url of the connector + facebook_app_id: '', + facebook_app_secret: '', + facebook_app_webhook_token: '', + amazon_client_id: '', // Client ID for use with Login with Amazon (Amazon Alexa channel) + amazon_client_secret: '', // Client Id for use with Login with Amazon (Amazon Alexa channel) +} + +``` + #### Running in development mode (hot reload) ```bash -yarn start-dev +yarn start:dev ``` #### Setup your connector First of all, you need to create a connector with the Bot Connector's API. ```sh -curl -X POST 'http://localhost:8080/connectors' --data 'url=YOUR_CONNECTOR_ENDPOINT_URL' +curl -X POST 'http://localhost:8080/connectors' --data 'url=YOUR_BOT_ENDPOINT_URL' ``` Then you need some code so the Bot Connector, via the *connector* you've just created, can send you the messages it receives. You can use the code from the *example* as a starter. @@ -80,25 +140,25 @@ yarn install yarn start ``` -Now that your bot (well, your code) and the Bot Connector are running, you have to create channels. Channel is the actual link between your connector and a specific service like Messenger, Slack or Kik. A connector can have multiple channels. +Now that your bot (well, your code) and the Bot Connector are running, you have to create channels. A channel is the actual link between your bot (the connector) and a specific service like Messenger, Slack or Kik. One connector can have multiple channels. ## How it works There are two distinct flows: -* your bot receive a message from a channel -* your bot send a message to a channel +* your bot receives a message from a channel +* your bot sends a message to a channel This pipeline allows us to have an abstraction of messages independent of the platform and implement only a few functions for each messaging platform (input and output parsing). #### Receive a message -The Bot Connector posts on your connector's endpoint each time a new message arrives from a channel. +The Bot Connector posts on the endpoint stored with the connector each time a new message arrives from a channel. * a new message is received by Bot Connector * the message is parsed by the corresponding service * the message is saved in MongoDB -* the message is post to the connector endpoint +* the message is post to the bot endpoint -![BotConnector-Receive](https://cdn.recast.ai/bot-connector/flow-1.png) +![BotConnector-Receive](https://cdn.cai.tools.sap/bot-connector/flow-1.png) #### Post a message @@ -107,89 +167,141 @@ To send a new message, you have to post it to Bot Connector's API * the messages are formatted by the corresponding service to match the channel's format * the messages are sent by Bot Connector to the corresponding channel -![BotConnector-Sending](https://cdn.recast.ai/bot-connector/flow-2.png) +![BotConnector-Sending](https://cdn.cai.tools.sap/bot-connector/flow-2.png) ## Messages format All messages coming from the bot are parsed and modified to match the destination channel specifications. Bot Connector supports several message formats: -* Text message: +* Text -```javascript -[{ +```js +{ type: 'text', - content: 'My text message', -}] + content: 'MY_TEXT', +} +``` + +* Picture + +```js +{ + type: 'picture', + content: 'IMAGE_URL', +} ``` -* Quick Replies: -```javascript -[{ +* Video + +```js +{ + type: 'video', + content: 'VIDEO_URL', +} +``` + +* Quick Replies + +```js +{ type: 'quickReplies', content: { - title: 'My title', + title: 'TITLE', buttons: [ { - title: 'Button title', - value: 'Button value', - }, + title: 'BUTTON_1_TITLE', + value: 'BUTTON_1_VALUE', + }, { + title: 'BUTTON_2_TITLE', + value: 'BUTTON_2_VALUE', + } ] } -}] +} ``` -* Cards: +* List +```js +{ + type: 'list', + content: { + elements: [ + { + title: 'ELEM_1_TITLE', + imageUrl: 'IMAGE_URL', + subtitle: 'ELEM_1_SUBTITLE', + buttons: [ + { + title: 'BUTTON_1_TITLE', + value: 'BUTTON_1_VALUE', + type: 'BUTTON_TYPE', + } + ] + } + ], + buttons: [ + { + title: 'BUTTON_1_TITLE', + value: 'BUTTON_1_VALUE', + type: 'BUTTON_TYPE', + } + ] + } +} +``` -```javascript -[{ +* Card + +```js +{ type: 'card', content: { - title: 'My card title', - imageUrl: 'url_to_my_image', - subtitle: 'My card subtitle', + title: 'CARD_TITLE', + subtitle: 'CARD_SUBTITLE', + imageUrl: 'IMAGE_URL', buttons: [ { - title: 'My button title', - type: 'My button type', // See Facebook Messenger button formats - value: 'My button value', + title: 'BUTTON_TITLE', + type: 'BUTTON_TYPE', // See Facebook Messenger button formats + value: 'BUTTON_VALUE', } ], }, -}] -``` - -* Pictures: - -```javascript -[{ - type: 'picture', - content: 'url_to_my_image', -}] +} ``` -* Videos: -```javascript -[{ - type: 'video', - content: 'url_to_my_video', -}] +* Carousel + +```js +{ + type: 'carousel', + content: [ + { + title: 'CARD_1_TITLE', + imageUrl: 'IMAGE_URL', + buttons: [ + { + title: 'BUTTON_1_TITLE', + value: 'BUTTON_1_VALUE', + type: 'BUTTON_1_TYPE', + } + ] + } + ], +} ``` -### Issue - -If you encounter any issue, please follow this [guide](https://github.com/RecastAI/bot-connector/blob/master/ISSUE.md). - -### Contribution - -Want to contribute? Great! Please check this [guide](https://github.com/RecastAI/bot-connector/blob/master/CONTRIBUTING.md). -### License +## Getting started with SAP Conversational AI -Copyright (c) [2016] [Recast.AI](https://recast.ai) +We build products to help enterprises and developers have a better understanding of user inputs. -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +- **NLP API**: a unique API for text processing, and augmented training. +- **Bot Building Tools**: all you need to create smart bots powered by SAP Conversational AI's NLP API. Design even the most complex conversation flow, use all rich messaging formats and connect to external APIs and services. +- **Bot Connector API**: standardizes the messaging format across all channels, letting you connect your bots to any channel in minutes. -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +Learn more about: -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +| [API Documentation](https://cai.tools.sap/docs/api-reference/) | [Discover the platform](https://cai.tools.sap/docs/concepts/create-builder-bot) | [First bot tutorial](https://cai.tools.sap/blog/build-your-first-bot-with-recast-ai/) | [Advanced NodeJS tutorial](https://cai.tools.sap/blog/nodejs-chatbot-movie-bot/) | [Advanced Python tutorial](https://cai.tools.sap/blog/python-cryptobot/) | +|---|---|---|---|---| diff --git a/config/development.js b/config/development.js deleted file mode 100644 index 3da9faa..0000000 --- a/config/development.js +++ /dev/null @@ -1,12 +0,0 @@ -module.exports = { - db: { - host: 'localhost', - port: '27017', - dbName: 'chatbot-connector', - }, - server: { - port: '8080', - url: 'localhost:8080', - }, - base_url: 'http://localhost:8000', -} diff --git a/config/index.js b/config/index.js index c28a07e..391f9f6 100644 --- a/config/index.js +++ b/config/index.js @@ -1,3 +1,3 @@ const env = process.env.NODE_ENV || 'development' -module.exports = require('./' + env + '.js') +module.exports = require(`./${env}.js`) diff --git a/config/production.js b/config/production.js deleted file mode 100644 index 3da9faa..0000000 --- a/config/production.js +++ /dev/null @@ -1,12 +0,0 @@ -module.exports = { - db: { - host: 'localhost', - port: '27017', - dbName: 'chatbot-connector', - }, - server: { - port: '8080', - url: 'localhost:8080', - }, - base_url: 'http://localhost:8000', -} diff --git a/config/test.js b/config/test.js index 863f631..31998c2 100644 --- a/config/test.js +++ b/config/test.js @@ -2,9 +2,21 @@ module.exports = { db: { host: 'localhost', port: 27017, - dbName: 'chatbot-connector-test', + dbName: 'gromit-test', }, server: { - port: 8080, + port: 2424, }, + redis: { + port: 6379, + host: 'localhost', + auth: '', + db: 14, + options: {}, // see https://github.com/mranney/node_redis#rediscreateclient + }, + base_url: 'http://localhost:2424', + skillsBuilderUrl: 'https://api.cai.tools.sap/build/v1/dialog', + facebook_app_id: '1234567890123456', + facebook_app_secret: 'abcewfnjrefu340bg3', + facebook_app_webhook_token: '', } diff --git a/deploy.sh b/deploy.sh deleted file mode 100755 index 8c21aab..0000000 --- a/deploy.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -rm -rf doc && \ -npm run doc && \ -cd doc && \ -git init && \ -git remote add origin git@github.com:RecastAI/bot-connector.git && \ -git add . && \ -git commit -m "deploy" && \ -git push -f origin master:gh-pages -printf '\n> everything has been done.\n' diff --git a/example/bot.js b/example/bot.js index 21f2317..612dbaa 100644 --- a/example/bot.js +++ b/example/bot.js @@ -1,33 +1,38 @@ -import express from 'express' -import bodyParser from 'body-parser' -import request from 'superagent' +const express = require('express') +const bodyParser = require('body-parser') +const request = require('superagent') const app = express() app.set('port', process.env.PORT || 5000) app.use(bodyParser.json()) -const config = { url: 'http://localhost:8080', botId: 'yourBotId' } +const config = { url: 'http://localhost:8080', connectorId: 'yourConnectorId' } - /* Get the request from the connector */ +/* Get the request from the connector */ - app.post('/', (req, res) => { - const conversationId = req.body.message.conversation - const message = [{ - type: 'text', - content: 'my first message', - }] +app.post('/', (req, res) => { + // const conversationId = req.body.message.conversation + const messages = [{ + type: 'text', + content: 'my first message', + }] - /* Send the message back to the connector */ - request.post(`${config.url}/bots/${config.botId}/conversations/${conversationId}/messages`) - .send({ messages, senderId: req.body.senderId }) - .end((err, res) => { - if (err) { - console.log(err) - } else { - console.log(res) - } - }) - }) + // send a response to the user + res.send(messages) +}) + +/* example for sending a message to the connector */ +// request.post(`${config.url}/connectors/${config.connectorId}/conversations/${conversationId}/messages`) +// .send({ messages, senderId: req.body.senderId }) +// .end((err, response) => { +// if (err) { +// console.error(err) +// res.status(500).send({ error: 'An internal error occured.' }) +// } else { +// console.log(response) +// res.send() +// } +// }) app.listen(app.get('port'), () => { console.log('Our bot is running on port', app.get('port')) diff --git a/example/package.json b/example/package.json index bfac677..972d753 100644 --- a/example/package.json +++ b/example/package.json @@ -4,24 +4,11 @@ "description": "", "main": "bot.js", "scripts": { - "start": "node ./node_modules/babel-cli/bin/babel-node.js bot.js" + "start": "node bot.js" }, "dependencies": { - "body-parser": "^1.15.2", - "express": "^4.14.0", - "superagent": "^2.3.0" - }, - "devDependencies": { - "babel-cli": "^6.11.4", - "babel-eslint": "^6.1.2", - "babel-preset-es2015": "^6.9.0", - "babel-preset-stage-0": "^6.5.0", - "babel-register": "^6.11.5" - }, - "babel": { - "presets": [ - "es2015", - "stage-0" - ] + "body-parser": "^1.18.3", + "express": "^4.16.4", + "superagent": "^4.1.0" } } diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..9d65467 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,13958 @@ +{ + "name": "botconnector", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/babel-types": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@types/babel-types/-/babel-types-7.0.1.tgz", + "integrity": "sha512-EkcOk09rjhivbovP8WreGRbXW20YRfe/qdgXOGq3it3u3aAOWDRNsQhL/XPAWFF7zhZZ+uR+nT+3b+TCkIap1w==" + }, + "@types/babylon": { + "version": "6.16.2", + "resolved": "https://registry.npmjs.org/@types/babylon/-/babylon-6.16.2.tgz", + "integrity": "sha512-+Jty46mPaWe1VAyZbfvgJM4BAdklLWxrT5tc/RjvCgLrtk6gzRY6AOnoWFv4p6hVxhJshDdr2hGVn56alBp97Q==", + "requires": { + "@types/babel-types": "*" + } + }, + "abab": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz", + "integrity": "sha1-X6rZwsB/YN12dw9xzwJbYqY8/U4=" + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, + "accepts": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", + "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", + "requires": { + "mime-types": "~2.1.18", + "negotiator": "0.6.1" + } + }, + "acorn": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.5.3.tgz", + "integrity": "sha512-jd5MkIUlbbmb07nXH0DT3y7rDVtkzDi4XZOUVWAer8ajmF/DTSSbl5oNFyDOl/OXA33Bl79+ypHhl2pN20VeOQ==" + }, + "acorn-globals": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-3.1.0.tgz", + "integrity": "sha1-/YJw9x+7SZawBPqIDuXUZXOnMb8=", + "requires": { + "acorn": "^4.0.4" + }, + "dependencies": { + "acorn": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", + "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=" + } + } + }, + "acorn-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", + "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", + "requires": { + "acorn": "^3.0.4" + }, + "dependencies": { + "acorn": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", + "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=" + } + } + }, + "ajv": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", + "requires": { + "co": "^4.6.0", + "json-stable-stringify": "^1.0.1" + } + }, + "ajv-keywords": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-1.5.1.tgz", + "integrity": "sha1-MU3QpLM2j609/NxU7eYXG4htrzw=" + }, + "alexa-verifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/alexa-verifier/-/alexa-verifier-1.0.0.tgz", + "integrity": "sha512-1XE/40ajf4sESuvAdacxVrxy06tkewC2sJ8qC5T/zQtGiYRsuoQjj6UkSpw7WTqG8wkmESANz/w5NnfIruyCjQ==", + "requires": { + "node-forge": "^0.7.0", + "validator": "^9.0.0" + } + }, + "align-text": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", + "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "requires": { + "kind-of": "^3.0.2", + "longest": "^1.0.1", + "repeat-string": "^1.5.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=" + }, + "ansi-align": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz", + "integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=", + "dev": true, + "requires": { + "string-width": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "ansi-escapes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", + "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=" + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "anymatch": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", + "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", + "dev": true, + "optional": true, + "requires": { + "micromatch": "^2.1.5", + "normalize-path": "^2.0.0" + } + }, + "apidoc": { + "version": "0.17.7", + "resolved": "https://registry.npmjs.org/apidoc/-/apidoc-0.17.7.tgz", + "integrity": "sha512-9Wf4bRPwCuWOIOxR42dDnsXnFw+rhJg5VrMQK+KmNxJwyIh30UqX6gvjjXSG6YO74MqE87F18bbQXUENK9dPGg==", + "dev": true, + "requires": { + "apidoc-core": "~0.8.2", + "commander": "^2.19.0", + "fs-extra": "^7.0.0", + "lodash": "^4.17.10", + "markdown-it": "^8.3.1", + "winston": "^3.0.0" + }, + "dependencies": { + "commander": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", + "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==", + "dev": true + }, + "lodash": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", + "dev": true + }, + "winston": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.1.0.tgz", + "integrity": "sha512-FsQfEE+8YIEeuZEYhHDk5cILo1HOcWkGwvoidLrDgPog0r4bser1lEIOco2dN9zpDJ1M88hfDgZvxe5z4xNcwg==", + "dev": true, + "requires": { + "async": "^2.6.0", + "diagnostics": "^1.1.1", + "is-stream": "^1.1.0", + "logform": "^1.9.1", + "one-time": "0.0.4", + "readable-stream": "^2.3.6", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.2.0" + } + } + } + }, + "apidoc-core": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/apidoc-core/-/apidoc-core-0.8.3.tgz", + "integrity": "sha1-2dY1RYKd8lDSzKBJaDqH53U2S5Y=", + "dev": true, + "requires": { + "fs-extra": "^3.0.1", + "glob": "^7.1.1", + "iconv-lite": "^0.4.17", + "klaw-sync": "^2.1.0", + "lodash": "~4.17.4", + "semver": "~5.3.0" + }, + "dependencies": { + "fs-extra": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz", + "integrity": "sha1-N5TzeMWLNC6n27sjCVEJxLO2IpE=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^3.0.0", + "universalify": "^0.1.0" + } + }, + "semver": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", + "dev": true + } + } + }, + "apparatus": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/apparatus/-/apparatus-0.0.9.tgz", + "integrity": "sha1-N9zSWDStC2UQdllikduCPusZCL0=", + "optional": true, + "requires": { + "sylvester": ">= 0.0.8" + } + }, + "archiver": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-1.3.0.tgz", + "integrity": "sha1-TyGU1tj5nfP1MeaIHxTxXVX6ryI=", + "requires": { + "archiver-utils": "^1.3.0", + "async": "^2.0.0", + "buffer-crc32": "^0.2.1", + "glob": "^7.0.0", + "lodash": "^4.8.0", + "readable-stream": "^2.0.0", + "tar-stream": "^1.5.0", + "walkdir": "^0.0.11", + "zip-stream": "^1.1.0" + } + }, + "archiver-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-1.3.0.tgz", + "integrity": "sha1-5QtMCccL89aA4y/xt5lOn52JUXQ=", + "requires": { + "glob": "^7.0.0", + "graceful-fs": "^4.1.0", + "lazystream": "^1.0.0", + "lodash": "^4.8.0", + "normalize-path": "^2.0.0", + "readable-stream": "^2.0.0" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "requires": { + "sprintf-js": "~1.0.2" + }, + "dependencies": { + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + } + } + }, + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "requires": { + "arr-flatten": "^1.0.1" + } + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true + }, + "array-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", + "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=" + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "requires": { + "array-uniq": "^1.0.1" + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=" + }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=" + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=" + }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" + }, + "ask-sdk": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/ask-sdk/-/ask-sdk-2.0.7.tgz", + "integrity": "sha512-fJDjDv1BqTvfiwHKOgPYuQbi0HCfe320Po1S6qqhdgHN/ZHuolHRYIEe8i3O1sk/qObCsrwOD9xh0l10E48Spw==", + "requires": { + "ask-sdk-core": "^2.0.7", + "ask-sdk-dynamodb-persistence-adapter": "^2.0.7", + "ask-sdk-model": "^1.0.0" + } + }, + "ask-sdk-core": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/ask-sdk-core/-/ask-sdk-core-2.0.7.tgz", + "integrity": "sha512-L0YoF7ls0iUoo/WYDYj7uDjAFThYZSDjzF8YvLHIEZyzKVgrNNqxetRorUB+odDoctPWW7RV1xcep8F4p7c1jg==" + }, + "ask-sdk-dynamodb-persistence-adapter": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/ask-sdk-dynamodb-persistence-adapter/-/ask-sdk-dynamodb-persistence-adapter-2.0.7.tgz", + "integrity": "sha512-RHIOOAfsIwZy7hUtENPF12l1NldB8+rRZ3Jk4I9efpFFZSmXOM510Qho3Lt3OZCH9qTDRp1QWrQvn3ki7fdM8A==", + "requires": { + "aws-sdk": "^2.163.0" + } + }, + "ask-sdk-model": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/ask-sdk-model/-/ask-sdk-model-1.3.1.tgz", + "integrity": "sha512-ZDcmJ8sDRAzfIPz5WhRpy8HJ8SheBOyjoeHtYIcoVO6ZQEgxXtZ11GJGg8FhRDfwwIWj5Ma8G6m7OCgo4nuJDA==" + }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" + }, + "assert-plus": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", + "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=" + }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==" + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true + }, + "async": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.0.tgz", + "integrity": "sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw==", + "requires": { + "lodash": "^4.14.0" + } + }, + "async-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", + "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=", + "dev": true + }, + "async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "atob": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.0.tgz", + "integrity": "sha512-SuiKH8vbsOyCALjA/+EINmt/Kdl+TQPrtFgW7XZZcwtryFu9e5kQoX3bjCW6mIvGH1fbeAZZuvwGR5IlBRznGw==", + "dev": true + }, + "aws-sdk": { + "version": "2.267.1", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.267.1.tgz", + "integrity": "sha512-4YZiZ6yk/Wb5lPTaV6AuiY7wo4f9W5TPNAx/Bempic4nbw9KmURrqwguzcY4oEl0Sq/FeDSp+AngHD+rHd4NlQ==", + "requires": { + "buffer": "4.9.1", + "events": "1.1.1", + "ieee754": "1.1.8", + "jmespath": "0.15.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "uuid": "3.1.0", + "xml2js": "0.4.17" + }, + "dependencies": { + "sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" + }, + "uuid": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", + "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" + } + } + }, + "aws-sign2": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", + "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=" + }, + "aws4": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz", + "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==" + }, + "babel-cli": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-cli/-/babel-cli-6.18.0.tgz", + "integrity": "sha1-khF/NBrdnerZD2+n0Kl8DMCOwYY=", + "dev": true, + "requires": { + "babel-core": "^6.18.0", + "babel-polyfill": "^6.16.0", + "babel-register": "^6.18.0", + "babel-runtime": "^6.9.0", + "chokidar": "^1.0.0", + "commander": "^2.8.1", + "convert-source-map": "^1.1.0", + "fs-readdir-recursive": "^1.0.0", + "glob": "^5.0.5", + "lodash": "^4.2.0", + "output-file-sync": "^1.1.0", + "path-is-absolute": "^1.0.0", + "slash": "^1.0.0", + "source-map": "^0.5.0", + "v8flags": "^2.0.10" + }, + "dependencies": { + "glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "dev": true, + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "requires": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + } + }, + "babel-core": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.0.tgz", + "integrity": "sha1-rzL3izGm/O8RnIew/Y2XU/A6C7g=", + "requires": { + "babel-code-frame": "^6.26.0", + "babel-generator": "^6.26.0", + "babel-helpers": "^6.24.1", + "babel-messages": "^6.23.0", + "babel-register": "^6.26.0", + "babel-runtime": "^6.26.0", + "babel-template": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "convert-source-map": "^1.5.0", + "debug": "^2.6.8", + "json5": "^0.5.1", + "lodash": "^4.17.4", + "minimatch": "^3.0.4", + "path-is-absolute": "^1.0.1", + "private": "^0.1.7", + "slash": "^1.0.0", + "source-map": "^0.5.6" + }, + "dependencies": { + "babel-generator": { + "version": "6.26.1", + "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", + "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", + "requires": { + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "detect-indent": "^4.0.0", + "jsesc": "^1.3.0", + "lodash": "^4.17.4", + "source-map": "^0.5.7", + "trim-right": "^1.0.1" + } + }, + "babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", + "requires": { + "babel-code-frame": "^6.26.0", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "debug": "^2.6.8", + "globals": "^9.18.0", + "invariant": "^2.2.2", + "lodash": "^4.17.4" + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + } + } + }, + "babel-eslint": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-7.2.3.tgz", + "integrity": "sha1-sv4tgBJkcPXBlELcdXJTqJdxCCc=", + "dev": true, + "requires": { + "babel-code-frame": "^6.22.0", + "babel-traverse": "^6.23.1", + "babel-types": "^6.23.0", + "babylon": "^6.17.0" + }, + "dependencies": { + "babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", + "dev": true, + "requires": { + "babel-code-frame": "^6.26.0", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "debug": "^2.6.8", + "globals": "^9.18.0", + "invariant": "^2.2.2", + "lodash": "^4.17.4" + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + } + } + }, + "babel-generator": { + "version": "6.19.0", + "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.19.0.tgz", + "integrity": "sha1-my8kQgR3ej1oEOwSfGc8h7NJ+sU=", + "dev": true, + "requires": { + "babel-messages": "^6.8.0", + "babel-runtime": "^6.9.0", + "babel-types": "^6.19.0", + "detect-indent": "^4.0.0", + "jsesc": "^1.3.0", + "lodash": "^4.2.0", + "source-map": "^0.5.0" + } + }, + "babel-helper-bindify-decorators": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-bindify-decorators/-/babel-helper-bindify-decorators-6.24.1.tgz", + "integrity": "sha1-FMGeXxQte0fxmlJDHlKxzLxAozA=", + "requires": { + "babel-runtime": "^6.22.0", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + }, + "dependencies": { + "babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", + "requires": { + "babel-code-frame": "^6.26.0", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "debug": "^2.6.8", + "globals": "^9.18.0", + "invariant": "^2.2.2", + "lodash": "^4.17.4" + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + } + } + }, + "babel-helper-builder-binary-assignment-operator-visitor": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz", + "integrity": "sha1-zORReto1b0IgvK6KAsKzRvmlZmQ=", + "requires": { + "babel-helper-explode-assignable-expression": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + }, + "dependencies": { + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + } + } + }, + "babel-helper-call-delegate": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz", + "integrity": "sha1-7Oaqzdx25Bw0YfiL/Fdb0Nqi340=", + "requires": { + "babel-helper-hoist-variables": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + }, + "dependencies": { + "babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", + "requires": { + "babel-code-frame": "^6.26.0", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "debug": "^2.6.8", + "globals": "^9.18.0", + "invariant": "^2.2.2", + "lodash": "^4.17.4" + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + } + } + }, + "babel-helper-define-map": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz", + "integrity": "sha1-pfVtq0GiX5fstJjH66ypgZ+Vvl8=", + "requires": { + "babel-helper-function-name": "^6.24.1", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "lodash": "^4.17.4" + }, + "dependencies": { + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + } + } + }, + "babel-helper-explode-assignable-expression": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz", + "integrity": "sha1-8luCz33BBDPFX3BZLVdGQArCLKo=", + "requires": { + "babel-runtime": "^6.22.0", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + }, + "dependencies": { + "babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", + "requires": { + "babel-code-frame": "^6.26.0", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "debug": "^2.6.8", + "globals": "^9.18.0", + "invariant": "^2.2.2", + "lodash": "^4.17.4" + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + } + } + }, + "babel-helper-explode-class": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-explode-class/-/babel-helper-explode-class-6.24.1.tgz", + "integrity": "sha1-fcKjkQ3uAHBW4eMdZAztPVTqqes=", + "requires": { + "babel-helper-bindify-decorators": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + }, + "dependencies": { + "babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", + "requires": { + "babel-code-frame": "^6.26.0", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "debug": "^2.6.8", + "globals": "^9.18.0", + "invariant": "^2.2.2", + "lodash": "^4.17.4" + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + } + } + }, + "babel-helper-function-name": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", + "integrity": "sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=", + "requires": { + "babel-helper-get-function-arity": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + }, + "dependencies": { + "babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", + "requires": { + "babel-code-frame": "^6.26.0", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "debug": "^2.6.8", + "globals": "^9.18.0", + "invariant": "^2.2.2", + "lodash": "^4.17.4" + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + } + } + }, + "babel-helper-get-function-arity": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz", + "integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=", + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + }, + "dependencies": { + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + } + } + }, + "babel-helper-hoist-variables": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz", + "integrity": "sha1-HssnaJydJVE+rbyZFKc/VAi+enY=", + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + }, + "dependencies": { + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + } + } + }, + "babel-helper-optimise-call-expression": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz", + "integrity": "sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc=", + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + }, + "dependencies": { + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + } + } + }, + "babel-helper-regex": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz", + "integrity": "sha1-MlxZ+QL4LyS3T6zu0DY5VPZJXnI=", + "requires": { + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "lodash": "^4.17.4" + }, + "dependencies": { + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + } + } + }, + "babel-helper-remap-async-to-generator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz", + "integrity": "sha1-XsWBgnrXI/7N04HxySg5BnbkVRs=", + "requires": { + "babel-helper-function-name": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + }, + "dependencies": { + "babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", + "requires": { + "babel-code-frame": "^6.26.0", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "debug": "^2.6.8", + "globals": "^9.18.0", + "invariant": "^2.2.2", + "lodash": "^4.17.4" + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + } + } + }, + "babel-helper-replace-supers": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz", + "integrity": "sha1-v22/5Dk40XNpohPKiov3S2qQqxo=", + "requires": { + "babel-helper-optimise-call-expression": "^6.24.1", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + }, + "dependencies": { + "babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", + "requires": { + "babel-code-frame": "^6.26.0", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "debug": "^2.6.8", + "globals": "^9.18.0", + "invariant": "^2.2.2", + "lodash": "^4.17.4" + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + } + } + }, + "babel-helpers": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", + "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=", + "requires": { + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-messages": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-check-es2015-constants": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz", + "integrity": "sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o=", + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-istanbul": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.6.tgz", + "integrity": "sha512-PWP9FQ1AhZhS01T/4qLSKoHGY/xvkZdVBGlKM/HuxxS3+sC66HhTNR7+MpbO/so/cz/wY94MeSWJuP1hXIPfwQ==", + "dev": true, + "requires": { + "babel-plugin-syntax-object-rest-spread": "^6.13.0", + "find-up": "^2.1.0", + "istanbul-lib-instrument": "^1.10.1", + "test-exclude": "^4.2.1" + }, + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "test-exclude": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-4.2.1.tgz", + "integrity": "sha512-qpqlP/8Zl+sosLxBcVKl9vYy26T9NPalxSzzCP/OY6K7j938ui2oKgo+kRZYfxAeIpLqpbVnsHq1tyV70E4lWQ==", + "dev": true, + "requires": { + "arrify": "^1.0.1", + "micromatch": "^3.1.8", + "object-assign": "^4.1.0", + "read-pkg-up": "^1.0.1", + "require-main-filename": "^1.0.1" + } + } + } + }, + "babel-plugin-syntax-async-functions": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz", + "integrity": "sha1-ytnK0RkbWtY0vzCuCHI5HgZHvpU=" + }, + "babel-plugin-syntax-async-generators": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-generators/-/babel-plugin-syntax-async-generators-6.13.0.tgz", + "integrity": "sha1-a8lj67FuzLrmuStZbrfzXDQqi5o=" + }, + "babel-plugin-syntax-class-constructor-call": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-class-constructor-call/-/babel-plugin-syntax-class-constructor-call-6.18.0.tgz", + "integrity": "sha1-nLnTn+Q8hgC+yBRkVt3L1OGnZBY=" + }, + "babel-plugin-syntax-class-properties": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz", + "integrity": "sha1-1+sjt5oxf4VDlixQW4J8fWysJ94=" + }, + "babel-plugin-syntax-decorators": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-decorators/-/babel-plugin-syntax-decorators-6.13.0.tgz", + "integrity": "sha1-MSVjtNvePMgGzuPkFszurd0RrAs=" + }, + "babel-plugin-syntax-do-expressions": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-do-expressions/-/babel-plugin-syntax-do-expressions-6.13.0.tgz", + "integrity": "sha1-V0d1YTmqJtOQ0JQQsDdEugfkeW0=" + }, + "babel-plugin-syntax-dynamic-import": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz", + "integrity": "sha1-jWomIpyDdFqZgqRBBRVyyqF5sdo=" + }, + "babel-plugin-syntax-exponentiation-operator": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz", + "integrity": "sha1-nufoM3KQ2pUoggGmpX9BcDF4MN4=" + }, + "babel-plugin-syntax-export-extensions": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-export-extensions/-/babel-plugin-syntax-export-extensions-6.13.0.tgz", + "integrity": "sha1-cKFITw+QiaToStRLrDU8lbmxJyE=" + }, + "babel-plugin-syntax-function-bind": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-function-bind/-/babel-plugin-syntax-function-bind-6.13.0.tgz", + "integrity": "sha1-SMSV8Xe98xqYHnMvVa3AvdJgH0Y=" + }, + "babel-plugin-syntax-object-rest-spread": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz", + "integrity": "sha1-/WU28rzhODb/o6VFjEkDpZe7O/U=" + }, + "babel-plugin-syntax-trailing-function-commas": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz", + "integrity": "sha1-ugNgk3+NBuQBgKQ/4NVhb/9TLPM=" + }, + "babel-plugin-transform-async-generator-functions": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-generator-functions/-/babel-plugin-transform-async-generator-functions-6.24.1.tgz", + "integrity": "sha1-8FiQAUX9PpkHpt3yjaWfIVJYpds=", + "requires": { + "babel-helper-remap-async-to-generator": "^6.24.1", + "babel-plugin-syntax-async-generators": "^6.5.0", + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-async-to-generator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz", + "integrity": "sha1-ZTbjeK/2yx1VF6wOQOs+n8jQh2E=", + "requires": { + "babel-helper-remap-async-to-generator": "^6.24.1", + "babel-plugin-syntax-async-functions": "^6.8.0", + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-class-constructor-call": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-class-constructor-call/-/babel-plugin-transform-class-constructor-call-6.24.1.tgz", + "integrity": "sha1-gNwoVQWsBn3LjWxl4vbxGrd2Xvk=", + "requires": { + "babel-plugin-syntax-class-constructor-call": "^6.18.0", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-plugin-transform-class-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz", + "integrity": "sha1-anl2PqYdM9NvN7YRqp3vgagbRqw=", + "requires": { + "babel-helper-function-name": "^6.24.1", + "babel-plugin-syntax-class-properties": "^6.8.0", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-plugin-transform-decorators": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-decorators/-/babel-plugin-transform-decorators-6.24.1.tgz", + "integrity": "sha1-eIAT2PjGtSIr33s0Q5Df13Vp4k0=", + "requires": { + "babel-helper-explode-class": "^6.24.1", + "babel-plugin-syntax-decorators": "^6.13.0", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-types": "^6.24.1" + }, + "dependencies": { + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + } + } + }, + "babel-plugin-transform-do-expressions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-do-expressions/-/babel-plugin-transform-do-expressions-6.22.0.tgz", + "integrity": "sha1-KMyvkoEtlJws0SgfaQyP3EaK6bs=", + "requires": { + "babel-plugin-syntax-do-expressions": "^6.8.0", + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-arrow-functions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz", + "integrity": "sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE=", + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-block-scoped-functions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz", + "integrity": "sha1-u8UbSflk1wy42OC5ToICRs46YUE=", + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-block-scoping": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz", + "integrity": "sha1-1w9SmcEwjQXBL0Y4E7CgnnOxiV8=", + "requires": { + "babel-runtime": "^6.26.0", + "babel-template": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "lodash": "^4.17.4" + }, + "dependencies": { + "babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", + "requires": { + "babel-code-frame": "^6.26.0", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "debug": "^2.6.8", + "globals": "^9.18.0", + "invariant": "^2.2.2", + "lodash": "^4.17.4" + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + } + } + }, + "babel-plugin-transform-es2015-classes": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz", + "integrity": "sha1-WkxYpQyclGHlZLSyo7+ryXolhNs=", + "requires": { + "babel-helper-define-map": "^6.24.1", + "babel-helper-function-name": "^6.24.1", + "babel-helper-optimise-call-expression": "^6.24.1", + "babel-helper-replace-supers": "^6.24.1", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + }, + "dependencies": { + "babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", + "requires": { + "babel-code-frame": "^6.26.0", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "debug": "^2.6.8", + "globals": "^9.18.0", + "invariant": "^2.2.2", + "lodash": "^4.17.4" + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + } + } + }, + "babel-plugin-transform-es2015-computed-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz", + "integrity": "sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM=", + "requires": { + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-destructuring": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz", + "integrity": "sha1-mXux8auWf2gtKwh2/jWNYOdlxW0=", + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-duplicate-keys": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz", + "integrity": "sha1-c+s9MQypaePvnskcU3QabxV2Qj4=", + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + }, + "dependencies": { + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + } + } + }, + "babel-plugin-transform-es2015-for-of": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz", + "integrity": "sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE=", + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-function-name": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz", + "integrity": "sha1-g0yJhTvDaxrw86TF26qU/Y6sqos=", + "requires": { + "babel-helper-function-name": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + }, + "dependencies": { + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + } + } + }, + "babel-plugin-transform-es2015-literals": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz", + "integrity": "sha1-T1SgLWzWbPkVKAAZox0xklN3yi4=", + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-modules-amd": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz", + "integrity": "sha1-Oz5UAXI5hC1tGcMBHEvS8AoA0VQ=", + "requires": { + "babel-plugin-transform-es2015-modules-commonjs": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-modules-commonjs": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.0.tgz", + "integrity": "sha1-DYOUApt9xqvhqX7xgeAHWN0uXYo=", + "requires": { + "babel-plugin-transform-strict-mode": "^6.24.1", + "babel-runtime": "^6.26.0", + "babel-template": "^6.26.0", + "babel-types": "^6.26.0" + }, + "dependencies": { + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + } + } + }, + "babel-plugin-transform-es2015-modules-systemjs": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz", + "integrity": "sha1-/4mhQrkRmpBhlfXxBuzzBdlAfSM=", + "requires": { + "babel-helper-hoist-variables": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-modules-umd": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz", + "integrity": "sha1-rJl+YoXNGO1hdq22B9YCNErThGg=", + "requires": { + "babel-plugin-transform-es2015-modules-amd": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-object-super": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz", + "integrity": "sha1-JM72muIcuDp/hgPa0CH1cusnj40=", + "requires": { + "babel-helper-replace-supers": "^6.24.1", + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-parameters": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz", + "integrity": "sha1-V6w1GrScrxSpfNE7CfZv3wpiXys=", + "requires": { + "babel-helper-call-delegate": "^6.24.1", + "babel-helper-get-function-arity": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + }, + "dependencies": { + "babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", + "requires": { + "babel-code-frame": "^6.26.0", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "debug": "^2.6.8", + "globals": "^9.18.0", + "invariant": "^2.2.2", + "lodash": "^4.17.4" + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + } + } + }, + "babel-plugin-transform-es2015-shorthand-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz", + "integrity": "sha1-JPh11nIch2YbvZmkYi5R8U3jiqA=", + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + }, + "dependencies": { + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + } + } + }, + "babel-plugin-transform-es2015-spread": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz", + "integrity": "sha1-1taKmfia7cRTbIGlQujdnxdG+NE=", + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-sticky-regex": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz", + "integrity": "sha1-AMHNsaynERLN8M9hJsLta0V8zbw=", + "requires": { + "babel-helper-regex": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + }, + "dependencies": { + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + } + } + }, + "babel-plugin-transform-es2015-template-literals": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz", + "integrity": "sha1-qEs0UPfp+PH2g51taH2oS7EjbY0=", + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-typeof-symbol": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz", + "integrity": "sha1-3sCfHN3/lLUqxz1QXITfWdzOs3I=", + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-unicode-regex": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz", + "integrity": "sha1-04sS9C6nMj9yk4fxinxa4frrNek=", + "requires": { + "babel-helper-regex": "^6.24.1", + "babel-runtime": "^6.22.0", + "regexpu-core": "^2.0.0" + } + }, + "babel-plugin-transform-exponentiation-operator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz", + "integrity": "sha1-KrDJx/MJj6SJB3cruBP+QejeOg4=", + "requires": { + "babel-helper-builder-binary-assignment-operator-visitor": "^6.24.1", + "babel-plugin-syntax-exponentiation-operator": "^6.8.0", + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-export-extensions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-export-extensions/-/babel-plugin-transform-export-extensions-6.22.0.tgz", + "integrity": "sha1-U3OLR+deghhYnuqUbLvTkQm75lM=", + "requires": { + "babel-plugin-syntax-export-extensions": "^6.8.0", + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-function-bind": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-function-bind/-/babel-plugin-transform-function-bind-6.22.0.tgz", + "integrity": "sha1-xvuOlqwpajELjPjqQBRiQH3fapc=", + "requires": { + "babel-plugin-syntax-function-bind": "^6.8.0", + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-object-rest-spread": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz", + "integrity": "sha1-DzZpLVD+9rfi1LOsFHgTepY7ewY=", + "requires": { + "babel-plugin-syntax-object-rest-spread": "^6.8.0", + "babel-runtime": "^6.26.0" + } + }, + "babel-plugin-transform-regenerator": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz", + "integrity": "sha1-4HA2lvveJ/Cj78rPi03KL3s6jy8=", + "requires": { + "regenerator-transform": "^0.10.0" + } + }, + "babel-plugin-transform-runtime": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-runtime/-/babel-plugin-transform-runtime-6.23.0.tgz", + "integrity": "sha1-iEkNRGUC6puOfvsP4J7E2ZR5se4=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-strict-mode": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz", + "integrity": "sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g=", + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + }, + "dependencies": { + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + } + } + }, + "babel-polyfill": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz", + "integrity": "sha1-N5k3q8Z9eJWXCtxiHyhM2WbPIVM=", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "core-js": "^2.5.0", + "regenerator-runtime": "^0.10.5" + }, + "dependencies": { + "regenerator-runtime": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz", + "integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg=", + "dev": true + } + } + }, + "babel-preset-env": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/babel-preset-env/-/babel-preset-env-1.6.1.tgz", + "integrity": "sha512-W6VIyA6Ch9ePMI7VptNn2wBM6dbG0eSz25HEiL40nQXCsXGTGZSTZu1Iap+cj3Q0S5a7T9+529l/5Bkvd+afNA==", + "requires": { + "babel-plugin-check-es2015-constants": "^6.22.0", + "babel-plugin-syntax-trailing-function-commas": "^6.22.0", + "babel-plugin-transform-async-to-generator": "^6.22.0", + "babel-plugin-transform-es2015-arrow-functions": "^6.22.0", + "babel-plugin-transform-es2015-block-scoped-functions": "^6.22.0", + "babel-plugin-transform-es2015-block-scoping": "^6.23.0", + "babel-plugin-transform-es2015-classes": "^6.23.0", + "babel-plugin-transform-es2015-computed-properties": "^6.22.0", + "babel-plugin-transform-es2015-destructuring": "^6.23.0", + "babel-plugin-transform-es2015-duplicate-keys": "^6.22.0", + "babel-plugin-transform-es2015-for-of": "^6.23.0", + "babel-plugin-transform-es2015-function-name": "^6.22.0", + "babel-plugin-transform-es2015-literals": "^6.22.0", + "babel-plugin-transform-es2015-modules-amd": "^6.22.0", + "babel-plugin-transform-es2015-modules-commonjs": "^6.23.0", + "babel-plugin-transform-es2015-modules-systemjs": "^6.23.0", + "babel-plugin-transform-es2015-modules-umd": "^6.23.0", + "babel-plugin-transform-es2015-object-super": "^6.22.0", + "babel-plugin-transform-es2015-parameters": "^6.23.0", + "babel-plugin-transform-es2015-shorthand-properties": "^6.22.0", + "babel-plugin-transform-es2015-spread": "^6.22.0", + "babel-plugin-transform-es2015-sticky-regex": "^6.22.0", + "babel-plugin-transform-es2015-template-literals": "^6.22.0", + "babel-plugin-transform-es2015-typeof-symbol": "^6.23.0", + "babel-plugin-transform-es2015-unicode-regex": "^6.22.0", + "babel-plugin-transform-exponentiation-operator": "^6.22.0", + "babel-plugin-transform-regenerator": "^6.22.0", + "browserslist": "^2.1.2", + "invariant": "^2.2.2", + "semver": "^5.3.0" + } + }, + "babel-preset-stage-0": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-stage-0/-/babel-preset-stage-0-6.24.1.tgz", + "integrity": "sha1-VkLRUEL5E4TX5a+LyIsduVsDnmo=", + "requires": { + "babel-plugin-transform-do-expressions": "^6.22.0", + "babel-plugin-transform-function-bind": "^6.22.0", + "babel-preset-stage-1": "^6.24.1" + } + }, + "babel-preset-stage-1": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-stage-1/-/babel-preset-stage-1-6.24.1.tgz", + "integrity": "sha1-dpLNfc1oSZB+auSgqFWJz7niv7A=", + "requires": { + "babel-plugin-transform-class-constructor-call": "^6.24.1", + "babel-plugin-transform-export-extensions": "^6.22.0", + "babel-preset-stage-2": "^6.24.1" + } + }, + "babel-preset-stage-2": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-stage-2/-/babel-preset-stage-2-6.24.1.tgz", + "integrity": "sha1-2eKWD7PXEYfw5k7sYrwHdnIZvcE=", + "requires": { + "babel-plugin-syntax-dynamic-import": "^6.18.0", + "babel-plugin-transform-class-properties": "^6.24.1", + "babel-plugin-transform-decorators": "^6.24.1", + "babel-preset-stage-3": "^6.24.1" + } + }, + "babel-preset-stage-3": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-stage-3/-/babel-preset-stage-3-6.24.1.tgz", + "integrity": "sha1-g2raCp56f6N8sTj7kyb4eTSkg5U=", + "requires": { + "babel-plugin-syntax-trailing-function-commas": "^6.22.0", + "babel-plugin-transform-async-generator-functions": "^6.24.1", + "babel-plugin-transform-async-to-generator": "^6.24.1", + "babel-plugin-transform-exponentiation-operator": "^6.24.1", + "babel-plugin-transform-object-rest-spread": "^6.22.0" + } + }, + "babel-register": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz", + "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=", + "requires": { + "babel-core": "^6.26.0", + "babel-runtime": "^6.26.0", + "core-js": "^2.5.0", + "home-or-tmp": "^2.0.0", + "lodash": "^4.17.4", + "mkdirp": "^0.5.1", + "source-map-support": "^0.4.15" + } + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "requires": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + } + }, + "babel-template": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", + "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", + "requires": { + "babel-runtime": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "lodash": "^4.17.4" + }, + "dependencies": { + "babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", + "requires": { + "babel-code-frame": "^6.26.0", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "debug": "^2.6.8", + "globals": "^9.18.0", + "invariant": "^2.2.2", + "lodash": "^4.17.4" + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + } + } + }, + "babel-traverse": { + "version": "6.19.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.19.0.tgz", + "integrity": "sha1-aDY/uCHiYkfVKlGahLLOq4309Vo=", + "dev": true, + "requires": { + "babel-code-frame": "^6.16.0", + "babel-messages": "^6.8.0", + "babel-runtime": "^6.9.0", + "babel-types": "^6.19.0", + "babylon": "^6.11.0", + "debug": "^2.2.0", + "globals": "^9.0.0", + "invariant": "^2.2.0", + "lodash": "^4.2.0" + } + }, + "babel-types": { + "version": "6.19.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.19.0.tgz", + "integrity": "sha1-jbKXLb7QHxGSqLYCuh4eTFFiQLk=", + "dev": true, + "requires": { + "babel-runtime": "^6.9.1", + "esutils": "^2.0.2", + "lodash": "^4.2.0", + "to-fast-properties": "^1.0.1" + } + }, + "babylon": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==" + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "base64-js": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", + "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" + }, + "base64url": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-2.0.0.tgz", + "integrity": "sha1-6sFuA+oUOO/5Qj1puqNiYu0fcLs=" + }, + "basic-auth": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.0.tgz", + "integrity": "sha1-AV2z81PgLlY3d1X5YnQuiYHnu7o=", + "requires": { + "safe-buffer": "5.1.1" + } + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "optional": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "bhttp": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/bhttp/-/bhttp-1.2.4.tgz", + "integrity": "sha1-/tDCT3ZbNa/ElAsIqzIUgT44848=", + "requires": { + "bluebird": "^2.8.2", + "concat-stream": "^1.4.7", + "debug": "^2.1.1", + "dev-null": "^0.1.1", + "errors": "^0.2.0", + "extend": "^2.0.0", + "form-data2": "^1.0.0", + "form-fix-array": "^1.0.0", + "lodash": "^2.4.1", + "stream-length": "^1.0.2", + "string": "^3.0.0", + "through2-sink": "^1.0.0", + "through2-spy": "^1.2.0", + "tough-cookie": "^2.3.1" + }, + "dependencies": { + "bluebird": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz", + "integrity": "sha1-U0uQM8AiyVecVro7Plpcqvu2UOE=" + }, + "extend": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-2.0.1.tgz", + "integrity": "sha1-HugBBonnOV/5RIJByYZSvHWagmA=" + }, + "lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=" + } + } + }, + "binary-extensions": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.11.0.tgz", + "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=", + "dev": true + }, + "bl": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", + "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", + "requires": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, + "bluebird": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", + "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" + }, + "blueimp-md5": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.10.0.tgz", + "integrity": "sha512-EkNUOi7tpV68TqjpiUz9D9NcT8um2+qtgntmMbi5UKssVX2m/2PLqotcric0RE63pB3HPN/fjf3cKHN2ufGSUQ==" + }, + "body-parser": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", + "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", + "requires": { + "bytes": "3.0.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.1", + "http-errors": "~1.6.2", + "iconv-lite": "0.4.19", + "on-finished": "~2.3.0", + "qs": "6.5.1", + "raw-body": "2.3.2", + "type-is": "~1.6.15" + }, + "dependencies": { + "qs": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", + "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" + } + } + }, + "boom": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", + "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", + "requires": { + "hoek": "2.x.x" + } + }, + "botbuilder": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/botbuilder/-/botbuilder-3.12.0.tgz", + "integrity": "sha1-G2BdUbN2WdTJsKPdi/IQV4jBfR0=", + "requires": { + "async": "^1.5.2", + "base64url": "^2.0.0", + "chrono-node": "^1.1.3", + "jsonwebtoken": "^7.0.1", + "promise": "^7.1.1", + "request": "^2.69.0", + "rsa-pem-from-mod-exp": "^0.8.4", + "sprintf-js": "^1.0.3", + "url-join": "^1.1.0" + }, + "dependencies": { + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" + }, + "jsonwebtoken": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-7.4.3.tgz", + "integrity": "sha1-d/UCHeBYtgWheD+hKD6ZgS5kVjg=", + "requires": { + "joi": "^6.10.1", + "jws": "^3.1.4", + "lodash.once": "^4.0.0", + "ms": "^2.0.0", + "xtend": "^4.0.1" + } + } + } + }, + "boxen": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz", + "integrity": "sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==", + "dev": true, + "requires": { + "ansi-align": "^2.0.0", + "camelcase": "^4.0.0", + "chalk": "^2.0.1", + "cli-boxes": "^1.0.0", + "string-width": "^2.0.0", + "term-size": "^1.2.0", + "widest-line": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + }, + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "requires": { + "expand-range": "^1.8.1", + "preserve": "^0.2.0", + "repeat-element": "^1.1.2" + } + }, + "browser-process-hrtime": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.2.tgz", + "integrity": "sha1-Ql1opY00R/AqBKqJQYf86K+Le44=" + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==" + }, + "browserslist": { + "version": "2.11.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-2.11.3.tgz", + "integrity": "sha512-yWu5cXT7Av6mVwzWc8lMsJMHWn4xyjSuGYi4IozbVTLUOEYPSagUB8kiMDUHA1fS3zjr8nkxkn9jdvug4BBRmA==", + "requires": { + "caniuse-lite": "^1.0.30000792", + "electron-to-chromium": "^1.3.30" + } + }, + "bson": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/bson/-/bson-1.0.6.tgz", + "integrity": "sha512-D8zmlb46xfuK2gGvKmUjIklQEouN2nQ0LEHHeZ/NoHM2LDiMk2EYzZ5Ntw/Urk+bgMDosOZxaRzXxvhI5TcAVQ==" + }, + "buffer": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=" + }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, + "buffer-from": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.0.0.tgz", + "integrity": "sha512-83apNb8KK0Se60UE1+4Ukbe3HbfELJ6UlI4ldtOGs7So4KD26orJM8hIY9lxdzP+UpItH1Yh/Y8GUvNFWFFRxA==" + }, + "buffer-shims": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", + "integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E=" + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=" + }, + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "caller-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", + "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", + "requires": { + "callsites": "^0.2.0" + } + }, + "callr": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/callr/-/callr-1.2.0.tgz", + "integrity": "sha1-3ss9PoejvQegGgbBEMxOjfQ7Z2M=" + }, + "callsites": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", + "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=" + }, + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=" + }, + "caniuse-lite": { + "version": "1.0.30000830", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000830.tgz", + "integrity": "sha512-yMqGkujkoOIZfvOYiWdqPALgY/PVGiqCHUJb6yNq7xhI/pR+gQO0U2K6lRDqAiJv4+CIU3CtTLblNGw0QGnr6g==" + }, + "capture-stack-trace": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz", + "integrity": "sha1-Sm+gc5nCa7pH8LJJa00PtAjFVQ0=", + "dev": true + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "center-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", + "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "requires": { + "align-text": "^0.1.3", + "lazy-cache": "^1.0.3" + } + }, + "chai": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.1.2.tgz", + "integrity": "sha1-D2RYS6ZC8PKs4oBiefTwbKI61zw=", + "requires": { + "assertion-error": "^1.0.1", + "check-error": "^1.0.1", + "deep-eql": "^3.0.0", + "get-func-name": "^2.0.0", + "pathval": "^1.0.0", + "type-detect": "^4.0.0" + } + }, + "chai-http": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/chai-http/-/chai-http-4.0.0.tgz", + "integrity": "sha512-R30Lj3JHHPhknOyurh09ZEBgyO4iSSeTjbLmyLvTr88IFC+zwRjAmaxBwj9TbEAGi0IV2uW+RHaTxeah5rdSaQ==", + "requires": { + "cookiejar": "^2.1.1", + "is-ip": "^2.0.0", + "methods": "^1.1.2", + "qs": "^6.5.1", + "superagent": "^3.7.0" + } + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "character-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz", + "integrity": "sha1-x84o821LzZdE5f/CxfzeHHMmH8A=", + "requires": { + "is-regex": "^1.0.3" + } + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=" + }, + "chokidar": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", + "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", + "dev": true, + "optional": true, + "requires": { + "anymatch": "^1.3.0", + "async-each": "^1.0.0", + "fsevents": "^1.0.0", + "glob-parent": "^2.0.0", + "inherits": "^2.0.1", + "is-binary-path": "^1.0.0", + "is-glob": "^2.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.0.0" + } + }, + "chrono-node": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/chrono-node/-/chrono-node-1.3.5.tgz", + "integrity": "sha1-oklSmKMtqCvMAa2b59d++l4kQSI=", + "requires": { + "moment": "^2.10.3" + } + }, + "ci-info": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.1.3.tgz", + "integrity": "sha512-SK/846h/Rcy8q9Z9CAwGBLfCJ6EkjJWdpelWDufQpqVDYq2Wnnv8zlSO6AMQap02jvhVruKKpEtQOufo3pFhLg==", + "dev": true + }, + "circular-json": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", + "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==" + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "clean-css": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.1.11.tgz", + "integrity": "sha1-Ls3xRaujj1R0DybO/Q/z4D4SXWo=", + "requires": { + "source-map": "0.5.x" + } + }, + "cli-boxes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", + "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=", + "dev": true + }, + "cli-cursor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", + "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", + "requires": { + "restore-cursor": "^1.0.1" + } + }, + "cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=" + }, + "cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "requires": { + "center-align": "^0.1.1", + "right-align": "^0.1.1", + "wordwrap": "0.0.2" + }, + "dependencies": { + "wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=" + } + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/color/-/color-3.0.0.tgz", + "integrity": "sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w==", + "dev": true, + "requires": { + "color-convert": "^1.9.1", + "color-string": "^1.5.2" + } + }, + "color-convert": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.2.tgz", + "integrity": "sha512-3NUJZdhMhcdPn8vJ9v2UQJoH0qqoGUkYTgFEPZaPjEtwmmKUfNV46zZmgB2M5M4DCEQHMaCfWHCxiBflLm04Tg==", + "dev": true, + "requires": { + "color-name": "1.1.1" + } + }, + "color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha1-SxQVMEz1ACjqgWQ2Q72C6gWANok=", + "dev": true + }, + "color-string": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz", + "integrity": "sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==", + "dev": true, + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "colornames": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/colornames/-/colornames-1.1.1.tgz", + "integrity": "sha1-+IiQMGhcfE/54qVZ9Qd+t2qBb5Y=", + "dev": true + }, + "colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=" + }, + "colorspace": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.1.tgz", + "integrity": "sha512-pI3btWyiuz7Ken0BWh9Elzsmv2bM9AhA7psXib4anUXy/orfZ/E0MbQwhSOG/9L8hLlalqrU0UhOuqxW1YjmVw==", + "dev": true, + "requires": { + "color": "3.0.x", + "text-hex": "1.0.x" + } + }, + "combined-stream": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", + "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "combined-stream2": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/combined-stream2/-/combined-stream2-1.1.2.tgz", + "integrity": "sha1-9uFLegFWZvjHsKH6xQYkAWSsNXA=", + "requires": { + "bluebird": "^2.8.1", + "debug": "^2.1.1", + "stream-length": "^1.0.1" + }, + "dependencies": { + "bluebird": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz", + "integrity": "sha1-U0uQM8AiyVecVro7Plpcqvu2UOE=" + } + } + }, + "commander": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", + "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==" + }, + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" + }, + "compress-commons": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-1.2.2.tgz", + "integrity": "sha1-UkqfEJA/OoEzibAiXSfEi7dRiQ8=", + "requires": { + "buffer-crc32": "^0.2.1", + "crc32-stream": "^2.0.0", + "normalize-path": "^2.0.0", + "readable-stream": "^2.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "configstore": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.2.tgz", + "integrity": "sha512-vtv5HtGjcYUgFrXc6Kx747B83MRRVS5R1VTEQoXvuP+kMI+if6uywV0nDGoiydJRy4yk7h9od5Og0kxx4zUXmw==", + "dev": true, + "requires": { + "dot-prop": "^4.1.0", + "graceful-fs": "^4.1.2", + "make-dir": "^1.0.0", + "unique-string": "^1.0.0", + "write-file-atomic": "^2.0.0", + "xdg-basedir": "^3.0.0" + } + }, + "constantinople": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-3.1.2.tgz", + "integrity": "sha512-yePcBqEFhLOqSBtwYOGGS1exHo/s1xjekXiinh4itpNQGCu4KA1euPh1fg07N2wMITZXQkBz75Ntdt1ctGZouw==", + "requires": { + "@types/babel-types": "^7.0.0", + "@types/babylon": "^6.16.2", + "babel-types": "^6.26.0", + "babylon": "^6.18.0" + }, + "dependencies": { + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + } + } + }, + "content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "convert-source-map": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.1.tgz", + "integrity": "sha1-uCeAl7m8IpNl3lxiz1/K7YtVmeU=" + }, + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "cookiejar": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.1.tgz", + "integrity": "sha1-Qa1XsbVVlR7BcUEqgZQrHoIA00o=" + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true + }, + "core-js": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.5.tgz", + "integrity": "sha1-sU3ek2xkDAV5prUMq8wTLdYSfjs=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "crc": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.5.0.tgz", + "integrity": "sha1-mLi6fUiWZbo5efWbITgTdBAaGWQ=" + }, + "crc32-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-2.0.0.tgz", + "integrity": "sha1-483TtN8xaN10494/u8t7KX/pCPQ=", + "requires": { + "crc": "^3.4.4", + "readable-stream": "^2.0.0" + } + }, + "create-error-class": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", + "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=", + "dev": true, + "requires": { + "capture-stack-trace": "^1.0.0" + } + }, + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "dependencies": { + "lru-cache": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz", + "integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==", + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + } + } + }, + "cryptiles": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", + "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", + "requires": { + "boom": "2.x.x" + } + }, + "crypto": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/crypto/-/crypto-0.0.3.tgz", + "integrity": "sha1-RwqBuGvkxe4XrMggeh9TFa4g27A=" + }, + "crypto-random-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", + "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=", + "dev": true + }, + "css-parse": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/css-parse/-/css-parse-1.7.0.tgz", + "integrity": "sha1-Mh9s9zeCpv91ERE5D8BeLGV9jJs=" + }, + "cssom": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.2.tgz", + "integrity": "sha1-uANhcMefB6kP8vFuIihAJ6JDhIs=" + }, + "cssstyle": { + "version": "0.2.37", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-0.2.37.tgz", + "integrity": "sha1-VBCXI0yyUTyDzu06zdwn/yeYfVQ=", + "requires": { + "cssom": "0.3.x" + } + }, + "cycle": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", + "integrity": "sha1-IegLK+hYD5i0aPN5QwZisEbDStI=" + }, + "d": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", + "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", + "requires": { + "es5-ext": "^0.10.9" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "data-urls": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.0.0.tgz", + "integrity": "sha512-ai40PPQR0Fn1lD2PPie79CibnlMN2AYiDhwFX/rZHVsxbs5kNJSjegqXIprhouGXlRdEnfybva7kqRGnB6mypA==", + "requires": { + "abab": "^1.0.4", + "whatwg-mimetype": "^2.0.0", + "whatwg-url": "^6.4.0" + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true + }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "requires": { + "type-detect": "^4.0.0" + } + }, + "deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" + }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "del": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", + "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", + "requires": { + "globby": "^5.0.0", + "is-path-cwd": "^1.0.0", + "is-path-in-cwd": "^1.0.0", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "rimraf": "^2.2.8" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "detect-indent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", + "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", + "requires": { + "repeating": "^2.0.0" + } + }, + "dev-null": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/dev-null/-/dev-null-0.1.1.tgz", + "integrity": "sha1-WiBc48Ky73e2I41roXnrdMag6Bg=" + }, + "diagnostics": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/diagnostics/-/diagnostics-1.1.1.tgz", + "integrity": "sha512-8wn1PmdunLJ9Tqbx+Fx/ZEuHfJf4NKSN2ZBj7SJC/OWRWha843+WsTjqMe1B5E3p28jqBlp+mJ2fPVxPyNgYKQ==", + "dev": true, + "requires": { + "colorspace": "1.1.x", + "enabled": "1.0.x", + "kuler": "1.0.x" + } + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==" + }, + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "requires": { + "esutils": "^2.0.2" + } + }, + "doctypes": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", + "integrity": "sha1-6oCxBqh1OHdOijpKWv4pPeSJ4Kk=" + }, + "domexception": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", + "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", + "requires": { + "webidl-conversions": "^4.0.2" + } + }, + "dot-prop": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", + "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", + "dev": true, + "requires": { + "is-obj": "^1.0.0" + } + }, + "double-ended-queue": { + "version": "2.1.0-0", + "resolved": "https://registry.npmjs.org/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz", + "integrity": "sha1-ED01J/0xUo9AGIEwyEHv3XgmTlw=" + }, + "duplexer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", + "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", + "dev": true + }, + "duplexer3": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", + "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", + "dev": true + }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "optional": true, + "requires": { + "jsbn": "~0.1.0" + } + }, + "ecdsa-sig-formatter": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.9.tgz", + "integrity": "sha1-S8kmJ07Dtau1AW5+HWCSGsJisqE=", + "requires": { + "base64url": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "electron-to-chromium": { + "version": "1.3.42", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.42.tgz", + "integrity": "sha1-lcM78B0MxAVVauyJn+Yf1NduoPk=" + }, + "enabled": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-1.0.2.tgz", + "integrity": "sha1-ll9lE9LC0cX0ZStkouM5ZGf8L5M=", + "dev": true, + "requires": { + "env-variable": "0.0.x" + } + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "requires": { + "once": "^1.4.0" + } + }, + "entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", + "dev": true + }, + "env-variable": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/env-variable/-/env-variable-0.0.5.tgz", + "integrity": "sha512-zoB603vQReOFvTg5xMl9I1P2PnHsHQQKTEowsKKD7nseUfJq6UWzK+4YtlWUO1nhiQUxe6XMkk+JleSZD1NZFA==", + "dev": true + }, + "error-ex": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", + "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "errors": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/errors/-/errors-0.2.0.tgz", + "integrity": "sha1-D1Hoidqj4RsZ5xhtEfEEqmbrJAM=" + }, + "es5-ext": { + "version": "0.10.42", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.42.tgz", + "integrity": "sha512-AJxO1rmPe1bDEfSR6TJ/FgMFYuTBhR5R57KW58iCkYACMyFbrkqVyzXSurYoScDGvgyMpk7uRF/lPUPPTmsRSA==", + "requires": { + "es6-iterator": "~2.0.3", + "es6-symbol": "~3.1.1", + "next-tick": "1" + } + }, + "es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "requires": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "es6-map": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz", + "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=", + "requires": { + "d": "1", + "es5-ext": "~0.10.14", + "es6-iterator": "~2.0.1", + "es6-set": "~0.1.5", + "es6-symbol": "~3.1.1", + "event-emitter": "~0.3.5" + } + }, + "es6-promise": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.2.1.tgz", + "integrity": "sha1-7FYjOGgDKQkgcXDDlEjiREndH8Q=" + }, + "es6-set": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", + "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=", + "requires": { + "d": "1", + "es5-ext": "~0.10.14", + "es6-iterator": "~2.0.1", + "es6-symbol": "3.1.1", + "event-emitter": "~0.3.5" + } + }, + "es6-symbol": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", + "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", + "requires": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "es6-weak-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.2.tgz", + "integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=", + "requires": { + "d": "1", + "es5-ext": "^0.10.14", + "es6-iterator": "^2.0.1", + "es6-symbol": "^3.1.1" + } + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "escodegen": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.9.1.tgz", + "integrity": "sha512-6hTjO1NAWkHnDk3OqQ4YrCuwwmGHL9S3nPlzBOUG/R44rda3wLNrfvQ5fkSGjyhHFKM7ALPKcKGrwvCLe0lC7Q==", + "requires": { + "esprima": "^3.1.3", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + }, + "dependencies": { + "esprima": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", + "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true + } + } + }, + "escope": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz", + "integrity": "sha1-4Bl16BJ4GhY6ba392AOY3GTIicM=", + "requires": { + "es6-map": "^0.1.3", + "es6-weak-map": "^2.0.1", + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "eslint": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-3.19.0.tgz", + "integrity": "sha1-yPxiAcf0DdCJQbh8CFdnOGpnmsw=", + "requires": { + "babel-code-frame": "^6.16.0", + "chalk": "^1.1.3", + "concat-stream": "^1.5.2", + "debug": "^2.1.1", + "doctrine": "^2.0.0", + "escope": "^3.6.0", + "espree": "^3.4.0", + "esquery": "^1.0.0", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "file-entry-cache": "^2.0.0", + "glob": "^7.0.3", + "globals": "^9.14.0", + "ignore": "^3.2.0", + "imurmurhash": "^0.1.4", + "inquirer": "^0.12.0", + "is-my-json-valid": "^2.10.0", + "is-resolvable": "^1.0.0", + "js-yaml": "^3.5.1", + "json-stable-stringify": "^1.0.0", + "levn": "^0.3.0", + "lodash": "^4.0.0", + "mkdirp": "^0.5.0", + "natural-compare": "^1.4.0", + "optionator": "^0.8.2", + "path-is-inside": "^1.0.1", + "pluralize": "^1.2.1", + "progress": "^1.1.8", + "require-uncached": "^1.0.2", + "shelljs": "^0.7.5", + "strip-bom": "^3.0.0", + "strip-json-comments": "~2.0.1", + "table": "^3.7.8", + "text-table": "~0.2.0", + "user-home": "^2.0.0" + } + }, + "eslint-config-zavatta": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/eslint-config-zavatta/-/eslint-config-zavatta-4.4.2.tgz", + "integrity": "sha1-EKM7xm7zxtT07SD1mD8iUCmTfAk=" + }, + "espree": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.4.tgz", + "integrity": "sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==", + "requires": { + "acorn": "^5.5.0", + "acorn-jsx": "^3.0.0" + } + }, + "esprima": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", + "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==" + }, + "esquery": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", + "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", + "requires": { + "estraverse": "^4.0.0" + } + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "requires": { + "estraverse": "^4.1.0" + } + }, + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=" + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", + "requires": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "event-stream": { + "version": "3.3.4", + "resolved": "http://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", + "integrity": "sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=", + "dev": true, + "requires": { + "duplexer": "~0.1.1", + "from": "~0", + "map-stream": "~0.1.0", + "pause-stream": "0.0.11", + "split": "0.3", + "stream-combiner": "~0.0.4", + "through": "~2.3.1" + } + }, + "events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" + }, + "execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "dev": true, + "requires": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "exit-hook": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", + "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=" + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "requires": { + "is-posix-bracket": "^0.1.0" + } + }, + "expand-range": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", + "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", + "requires": { + "fill-range": "^2.1.0" + } + }, + "expect.js": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/expect.js/-/expect.js-0.3.1.tgz", + "integrity": "sha1-sKWaDS7/VDdUTr8M6qYBWEHQm1s=", + "dev": true + }, + "express": { + "version": "4.16.3", + "resolved": "https://registry.npmjs.org/express/-/express-4.16.3.tgz", + "integrity": "sha1-avilAjUNsyRuzEvs9rWjTSL37VM=", + "requires": { + "accepts": "~1.3.5", + "array-flatten": "1.1.1", + "body-parser": "1.18.2", + "content-disposition": "0.5.2", + "content-type": "~1.0.4", + "cookie": "0.3.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.1.1", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.3", + "qs": "6.5.1", + "range-parser": "~1.2.0", + "safe-buffer": "5.1.1", + "send": "0.16.2", + "serve-static": "1.13.2", + "setprototypeof": "1.1.0", + "statuses": "~1.4.0", + "type-is": "~1.6.16", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "qs": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", + "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" + }, + "statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" + } + } + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "requires": { + "is-extglob": "^1.0.0" + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "eyes": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", + "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=" + }, + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" + }, + "fast-safe-stringify": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.6.tgz", + "integrity": "sha512-q8BZ89jjc+mz08rSxROs8VsrBBcn1SIw1kq9NjolL509tkABRk9io01RAjSaEv1Xb2uFLt8VtRiZbGp5H8iDtg==", + "dev": true + }, + "fbgraph": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/fbgraph/-/fbgraph-1.4.4.tgz", + "integrity": "sha1-m/RcZmYVKTjCWSF0OFxVQGCPzEo=", + "requires": { + "qs": "^6.5.0", + "request": "^2.79.0" + } + }, + "fecha": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-2.3.3.tgz", + "integrity": "sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg==", + "dev": true + }, + "figures": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", + "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", + "requires": { + "escape-string-regexp": "^1.0.5", + "object-assign": "^4.1.0" + } + }, + "file-entry-cache": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", + "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", + "requires": { + "flat-cache": "^1.2.1", + "object-assign": "^4.0.1" + } + }, + "file-type": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-4.4.0.tgz", + "integrity": "sha1-G2AOX8ofvcboDApwxxyNul95BsU=" + }, + "filename-regex": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", + "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=" + }, + "fill-range": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz", + "integrity": "sha1-ULd9/X5Gm8dJJHCWNpn+eoSFpyM=", + "requires": { + "is-number": "^2.1.0", + "isobject": "^2.0.0", + "randomatic": "^1.1.3", + "repeat-element": "^1.1.2", + "repeat-string": "^1.5.2" + } + }, + "filter-keys": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/filter-keys/-/filter-keys-1.1.0.tgz", + "integrity": "sha1-44UVQckkaVZG+MH8TcrJEZOy53s=", + "requires": { + "micromatch": "^2.2.0" + } + }, + "filter-object": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/filter-object/-/filter-object-2.1.0.tgz", + "integrity": "sha1-r5wK0LtAoAaUa4S02zPDrl6T34Y=", + "requires": { + "extend-shallow": "^2.0.1", + "filter-keys": "^1.0.2", + "filter-values": "^0.4.0", + "kind-of": "^2.0.1", + "object.pick": "^1.1.1" + } + }, + "filter-values": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/filter-values/-/filter-values-0.4.1.tgz", + "integrity": "sha1-Webb1dP9YwK9LbFcKOcbwWEO6Es=", + "requires": { + "for-own": "^0.1.3", + "is-match": "^0.4.0" + } + }, + "finalhandler": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", + "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "statuses": "~1.4.0", + "unpipe": "~1.0.0" + }, + "dependencies": { + "statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" + } + } + }, + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "flat-cache": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.0.tgz", + "integrity": "sha1-0wMLMrOBVPTjt+nHCfSQ9++XxIE=", + "requires": { + "circular-json": "^0.3.1", + "del": "^2.0.2", + "graceful-fs": "^4.1.2", + "write": "^0.2.1" + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" + }, + "for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "requires": { + "for-in": "^1.0.1" + } + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", + "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.5", + "mime-types": "^2.1.12" + } + }, + "form-data2": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/form-data2/-/form-data2-1.0.3.tgz", + "integrity": "sha1-y6XiNgGmlE2Vq31xEf+Tl6XLKk0=", + "requires": { + "bluebird": "^2.8.2", + "combined-stream2": "^1.0.2", + "debug": "^2.1.1", + "lodash": "^2.4.1", + "mime": "^1.2.11", + "uuid": "^2.0.1" + }, + "dependencies": { + "bluebird": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz", + "integrity": "sha1-U0uQM8AiyVecVro7Plpcqvu2UOE=" + }, + "lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=" + }, + "uuid": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz", + "integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=" + } + } + }, + "form-fix-array": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/form-fix-array/-/form-fix-array-1.0.0.tgz", + "integrity": "sha1-oTR6R+UxF6t7zb8+Lz7JHGZ2m8g=" + }, + "formidable": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.1.tgz", + "integrity": "sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg==" + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, + "requires": { + "map-cache": "^0.2.2" + } + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "from": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", + "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=", + "dev": true + }, + "fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "dependencies": { + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + } + } + }, + "fs-readdir-recursive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", + "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fsevents": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.3.tgz", + "integrity": "sha512-WIr7iDkdmdbxu/Gh6eKEZJL6KPE74/5MEsf2whTOFNxbIoIixogroLdKYqB6FDav4Wavh/lZdzzd3b2KxIXC5Q==", + "dev": true, + "optional": true, + "requires": { + "nan": "^2.3.0", + "node-pre-gyp": "^0.6.39" + }, + "dependencies": { + "abbrev": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "ajv": { + "version": "4.11.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "co": "^4.6.0", + "json-stable-stringify": "^1.0.1" + } + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "dev": true + }, + "aproba": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "asn1": { + "version": "0.2.3", + "bundled": true, + "dev": true, + "optional": true + }, + "assert-plus": { + "version": "0.2.0", + "bundled": true, + "dev": true, + "optional": true + }, + "asynckit": { + "version": "0.4.0", + "bundled": true, + "dev": true, + "optional": true + }, + "aws-sign2": { + "version": "0.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "aws4": { + "version": "1.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "balanced-match": { + "version": "0.4.2", + "bundled": true, + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "block-stream": { + "version": "0.0.9", + "bundled": true, + "dev": true, + "requires": { + "inherits": "~2.0.0" + } + }, + "boom": { + "version": "2.10.1", + "bundled": true, + "dev": true, + "requires": { + "hoek": "2.x.x" + } + }, + "brace-expansion": { + "version": "1.1.7", + "bundled": true, + "dev": true, + "requires": { + "balanced-match": "^0.4.1", + "concat-map": "0.0.1" + } + }, + "buffer-shims": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "caseless": { + "version": "0.12.0", + "bundled": true, + "dev": true, + "optional": true + }, + "co": { + "version": "4.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "combined-stream": { + "version": "1.0.5", + "bundled": true, + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "cryptiles": { + "version": "2.0.5", + "bundled": true, + "dev": true, + "requires": { + "boom": "2.x.x" + } + }, + "dashdash": { + "version": "1.14.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "assert-plus": "^1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "debug": { + "version": "2.6.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.4.2", + "bundled": true, + "dev": true, + "optional": true + }, + "delayed-stream": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "ecc-jsbn": { + "version": "0.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "jsbn": "~0.1.0" + } + }, + "extend": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "extsprintf": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "forever-agent": { + "version": "0.6.1", + "bundled": true, + "dev": true, + "optional": true + }, + "form-data": { + "version": "2.1.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.5", + "mime-types": "^2.1.12" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "fstream": { + "version": "1.0.11", + "bundled": true, + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + } + }, + "fstream-ignore": { + "version": "1.0.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "fstream": "^1.0.0", + "inherits": "2", + "minimatch": "^3.0.0" + } + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "getpass": { + "version": "0.1.7", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "assert-plus": "^1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "glob": { + "version": "7.1.2", + "bundled": true, + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "graceful-fs": { + "version": "4.1.11", + "bundled": true, + "dev": true + }, + "har-schema": { + "version": "1.0.5", + "bundled": true, + "dev": true, + "optional": true + }, + "har-validator": { + "version": "4.2.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ajv": "^4.9.1", + "har-schema": "^1.0.5" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "hawk": { + "version": "3.1.3", + "bundled": true, + "dev": true, + "requires": { + "boom": "2.x.x", + "cryptiles": "2.x.x", + "hoek": "2.x.x", + "sntp": "1.x.x" + } + }, + "hoek": { + "version": "2.16.3", + "bundled": true, + "dev": true + }, + "http-signature": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "assert-plus": "^0.2.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "dev": true + }, + "ini": { + "version": "1.3.4", + "bundled": true, + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-typedarray": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "isstream": { + "version": "0.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "jodid25519": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "jsbn": "~0.1.0" + } + }, + "jsbn": { + "version": "0.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "bundled": true, + "dev": true, + "optional": true + }, + "json-stable-stringify": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "jsonify": "~0.0.0" + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "jsonify": { + "version": "0.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "jsprim": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.0.2", + "json-schema": "0.2.3", + "verror": "1.3.6" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "mime-db": { + "version": "1.27.0", + "bundled": true, + "dev": true + }, + "mime-types": { + "version": "2.1.15", + "bundled": true, + "dev": true, + "requires": { + "mime-db": "~1.27.0" + } + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "node-pre-gyp": { + "version": "0.6.39", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "detect-libc": "^1.0.2", + "hawk": "3.1.3", + "mkdirp": "^0.5.1", + "nopt": "^4.0.1", + "npmlog": "^4.0.2", + "rc": "^1.1.7", + "request": "2.81.0", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^2.2.1", + "tar-pack": "^3.4.0" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npmlog": { + "version": "4.1.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "oauth-sign": { + "version": "0.8.2", + "bundled": true, + "dev": true, + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "osenv": { + "version": "0.1.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "performance-now": { + "version": "0.2.0", + "bundled": true, + "dev": true, + "optional": true + }, + "process-nextick-args": { + "version": "1.0.7", + "bundled": true, + "dev": true + }, + "punycode": { + "version": "1.4.1", + "bundled": true, + "dev": true, + "optional": true + }, + "qs": { + "version": "6.4.0", + "bundled": true, + "dev": true, + "optional": true + }, + "rc": { + "version": "1.2.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "deep-extend": "~0.4.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.2.9", + "bundled": true, + "dev": true, + "requires": { + "buffer-shims": "~1.0.0", + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "string_decoder": "~1.0.0", + "util-deprecate": "~1.0.1" + } + }, + "request": { + "version": "2.81.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aws-sign2": "~0.6.0", + "aws4": "^1.2.1", + "caseless": "~0.12.0", + "combined-stream": "~1.0.5", + "extend": "~3.0.0", + "forever-agent": "~0.6.1", + "form-data": "~2.1.1", + "har-validator": "~4.2.1", + "hawk": "~3.1.3", + "http-signature": "~1.1.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.7", + "oauth-sign": "~0.8.1", + "performance-now": "^0.2.0", + "qs": "~6.4.0", + "safe-buffer": "^5.0.1", + "stringstream": "~0.0.4", + "tough-cookie": "~2.3.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.0.0" + } + }, + "rimraf": { + "version": "2.6.1", + "bundled": true, + "dev": true, + "requires": { + "glob": "^7.0.5" + } + }, + "safe-buffer": { + "version": "5.0.1", + "bundled": true, + "dev": true + }, + "semver": { + "version": "5.3.0", + "bundled": true, + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "sntp": { + "version": "1.0.9", + "bundled": true, + "dev": true, + "requires": { + "hoek": "2.x.x" + } + }, + "sshpk": { + "version": "1.13.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jodid25519": "^1.0.0", + "jsbn": "~0.1.0", + "tweetnacl": "~0.14.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "stringstream": { + "version": "0.0.5", + "bundled": true, + "dev": true, + "optional": true + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "tar": { + "version": "2.2.1", + "bundled": true, + "dev": true, + "requires": { + "block-stream": "*", + "fstream": "^1.0.2", + "inherits": "2" + } + }, + "tar-pack": { + "version": "3.4.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "debug": "^2.2.0", + "fstream": "^1.0.10", + "fstream-ignore": "^1.0.5", + "once": "^1.3.3", + "readable-stream": "^2.1.4", + "rimraf": "^2.5.1", + "tar": "^2.2.1", + "uid-number": "^0.0.6" + } + }, + "tough-cookie": { + "version": "2.3.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "punycode": "^1.4.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "bundled": true, + "dev": true, + "optional": true + }, + "uid-number": { + "version": "0.0.6", + "bundled": true, + "dev": true, + "optional": true + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "uuid": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "verror": { + "version": "1.3.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "extsprintf": "1.0.2" + } + }, + "wide-align": { + "version": "1.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "string-width": "^1.0.2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true + } + } + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "generate-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", + "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=" + }, + "generate-object-property": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", + "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", + "requires": { + "is-property": "^1.0.0" + } + }, + "get-caller-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", + "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=" + }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=" + }, + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "dev": true + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "^1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-base": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", + "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "requires": { + "glob-parent": "^2.0.0", + "is-glob": "^2.0.0" + } + }, + "glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "requires": { + "is-glob": "^2.0.0" + } + }, + "global-dirs": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", + "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=", + "dev": true, + "requires": { + "ini": "^1.3.4" + } + }, + "globals": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", + "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==" + }, + "globby": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", + "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", + "requires": { + "array-union": "^1.0.1", + "arrify": "^1.0.0", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "got": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", + "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", + "dev": true, + "requires": { + "create-error-class": "^3.0.0", + "duplexer3": "^0.1.4", + "get-stream": "^3.0.0", + "is-redirect": "^1.0.0", + "is-retry-allowed": "^1.0.0", + "is-stream": "^1.0.0", + "lowercase-keys": "^1.0.0", + "safe-buffer": "^5.0.1", + "timed-out": "^4.0.0", + "unzip-response": "^2.0.1", + "url-parse-lax": "^1.0.0" + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" + }, + "growl": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.3.tgz", + "integrity": "sha512-hKlsbA5Vu3xsh1Cg3J7jSmX/WaW6A5oBeqzM88oNbCRQFz+zUaXm6yxS4RVytp1scBoJzSYl4YAEOQIt6O8V1Q==" + }, + "handlebars": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.11.tgz", + "integrity": "sha1-Ywo13+ApS8KB7a5v/F0yn8eYLcw=", + "dev": true, + "requires": { + "async": "^1.4.0", + "optimist": "^0.6.1", + "source-map": "^0.4.4", + "uglify-js": "^2.6" + }, + "dependencies": { + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "requires": { + "amdefine": ">=0.0.4" + } + } + } + }, + "har-schema": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz", + "integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=" + }, + "har-validator": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", + "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=", + "requires": { + "ajv": "^4.9.1", + "har-schema": "^1.0.5" + } + }, + "has": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", + "integrity": "sha1-hGFzP1OLCDfJNh45qauelwTcLyg=", + "requires": { + "function-bind": "^1.0.2" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=" + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "hawk": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", + "requires": { + "boom": "2.x.x", + "cryptiles": "2.x.x", + "hoek": "2.x.x", + "sntp": "1.x.x" + } + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=" + }, + "hoek": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" + }, + "home-or-tmp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", + "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=", + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.1" + } + }, + "hooks-fixed": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hooks-fixed/-/hooks-fixed-2.0.2.tgz", + "integrity": "sha512-YurCM4gQSetcrhwEtpQHhQ4M7Zo7poNGqY4kQGeBS6eZtOcT3tnNs01ThFa0jYBByAiYt1MjMjP/YApG0EnAvQ==" + }, + "hosted-git-info": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.6.0.tgz", + "integrity": "sha512-lIbgIIQA3lz5XaB6vxakj6sDHADJiZadYEJB+FgA+C4nubM1NwcuvUr9EJPmnH1skZqpqUzWborWo8EIUi0Sdw==" + }, + "html-encoding-sniffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", + "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", + "requires": { + "whatwg-encoding": "^1.0.1" + } + }, + "htmlencode": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/htmlencode/-/htmlencode-0.0.4.tgz", + "integrity": "sha1-9+LWr74YqHp45jujMI51N2Z0Dj8=" + }, + "http": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/http/-/http-0.0.0.tgz", + "integrity": "sha1-huYybSnF0Dnen6xYSkVon5KfT3I=" + }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "http-signature": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", + "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", + "requires": { + "assert-plus": "^0.2.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "iconv-lite": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", + "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" + }, + "ieee754": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", + "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=" + }, + "ignore": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.7.tgz", + "integrity": "sha512-YGG3ejvBNHRqu0559EOxxNFihD0AjpvHlC/pdGKd3X3ofe+CoJkYazwNJYTNebqpPKN+VVQbh4ZFn1DivMNuHA==" + }, + "ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=", + "dev": true + }, + "import-lazy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", + "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", + "dev": true + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "dev": true + }, + "inquirer": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-0.12.0.tgz", + "integrity": "sha1-HvK/1jUE3wvHV4X/+MLEHfEvB34=", + "requires": { + "ansi-escapes": "^1.1.0", + "ansi-regex": "^2.0.0", + "chalk": "^1.0.0", + "cli-cursor": "^1.0.1", + "cli-width": "^2.0.0", + "figures": "^1.3.5", + "lodash": "^4.3.0", + "readline2": "^1.0.1", + "run-async": "^0.1.0", + "rx-lite": "^3.1.2", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.0", + "through": "^2.3.6" + } + }, + "intercom-client": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/intercom-client/-/intercom-client-2.9.1.tgz", + "integrity": "sha1-QABM7wyClYPpvE4fg/1j8FYWrAY=", + "requires": { + "bluebird": "^3.3.4", + "htmlencode": "^0.0.4", + "request": "^2.83.0" + }, + "dependencies": { + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "requires": { + "co": "^4.6.0", + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "boom": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", + "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", + "requires": { + "hoek": "4.x.x" + } + }, + "cryptiles": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", + "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", + "requires": { + "boom": "5.x.x" + }, + "dependencies": { + "boom": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", + "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", + "requires": { + "hoek": "4.x.x" + } + } + } + }, + "form-data": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", + "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "1.0.6", + "mime-types": "^2.1.12" + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", + "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", + "requires": { + "ajv": "^5.1.0", + "har-schema": "^2.0.0" + } + }, + "hawk": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", + "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", + "requires": { + "boom": "4.x.x", + "cryptiles": "3.x.x", + "hoek": "4.x.x", + "sntp": "2.x.x" + } + }, + "hoek": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", + "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==" + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "request": { + "version": "2.85.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.85.0.tgz", + "integrity": "sha512-8H7Ehijd4js+s6wuVPLjwORxD4zeuyjYugprdOXlPSqaApmL/QOy+EB/beICHVCHkGMKNh5rvihb5ov+IDw4mg==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.6.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.5", + "extend": "~3.0.1", + "forever-agent": "~0.6.1", + "form-data": "~2.3.1", + "har-validator": "~5.0.3", + "hawk": "~6.0.2", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.17", + "oauth-sign": "~0.8.2", + "performance-now": "^2.1.0", + "qs": "~6.5.1", + "safe-buffer": "^5.1.1", + "stringstream": "~0.0.5", + "tough-cookie": "~2.3.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.1.0" + } + }, + "sntp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", + "integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==", + "requires": { + "hoek": "4.x.x" + } + } + } + }, + "interpret": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", + "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=" + }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "requires": { + "loose-envify": "^1.0.0" + } + }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" + }, + "ip-regex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=" + }, + "ipaddr.js": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.6.0.tgz", + "integrity": "sha1-4/o1e3c9phnybpXwSdBVxyeW+Gs=" + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dev": true, + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, + "is-builtin-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "requires": { + "builtin-modules": "^1.0.0" + } + }, + "is-ci": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.1.0.tgz", + "integrity": "sha512-c7TnwxLePuqIlxHgr7xtxzycJPegNHFuIrBkwbf8hc58//+Op1CqFkyS+xnIMkwn9UsJIwc174BIjkyBmSpjKg==", + "dev": true, + "requires": { + "ci-info": "^1.0.0" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "is-dotfile": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", + "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=" + }, + "is-equal-shallow": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", + "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", + "requires": { + "is-primitive": "^2.0.0" + } + }, + "is-expression": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-3.0.0.tgz", + "integrity": "sha1-Oayqa+f9HzRx3ELHQW5hwkMXrJ8=", + "requires": { + "acorn": "~4.0.2", + "object-assign": "^4.0.1" + }, + "dependencies": { + "acorn": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", + "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=" + } + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=" + }, + "is-finite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "requires": { + "is-extglob": "^1.0.0" + } + }, + "is-installed-globally": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.1.0.tgz", + "integrity": "sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=", + "dev": true, + "requires": { + "global-dirs": "^0.1.0", + "is-path-inside": "^1.0.0" + } + }, + "is-ip": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-2.0.0.tgz", + "integrity": "sha1-aO6gfooKCpTC0IDdZ0xzGrKkYas=", + "requires": { + "ip-regex": "^2.0.0" + } + }, + "is-match": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/is-match/-/is-match-0.4.1.tgz", + "integrity": "sha1-+19sZwmhVDt8fvp9lTDlt3b2H4M=", + "requires": { + "deep-equal": "^1.0.1", + "is-extendable": "^0.1.1", + "is-glob": "^2.0.1", + "micromatch": "^2.3.7" + } + }, + "is-my-ip-valid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz", + "integrity": "sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ==" + }, + "is-my-json-valid": { + "version": "2.17.2", + "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.17.2.tgz", + "integrity": "sha512-IBhBslgngMQN8DDSppmgDv7RNrlFotuuDsKcrCP3+HbFaVivIBU7u9oiiErw8sH4ynx3+gOGQ3q2otkgiSi6kg==", + "requires": { + "generate-function": "^2.0.0", + "generate-object-property": "^1.1.0", + "is-my-ip-valid": "^1.0.0", + "jsonpointer": "^4.0.0", + "xtend": "^4.0.0" + } + }, + "is-npm": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", + "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ=", + "dev": true + }, + "is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", + "dev": true + }, + "is-odd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-odd/-/is-odd-2.0.0.tgz", + "integrity": "sha512-OTiixgpZAT1M4NHgS5IguFp/Vz2VI3U7Goh4/HA1adtwyLtSBrxYlcSYkhpAE07s4fKEcjrFxyvtQBND4vFQyQ==", + "dev": true, + "requires": { + "is-number": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", + "dev": true + } + } + }, + "is-path-cwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", + "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=" + }, + "is-path-in-cwd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz", + "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==", + "requires": { + "is-path-inside": "^1.0.0" + } + }, + "is-path-inside": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", + "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", + "requires": { + "path-is-inside": "^1.0.1" + } + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "is-posix-bracket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", + "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=" + }, + "is-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", + "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=" + }, + "is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" + }, + "is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=" + }, + "is-redirect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", + "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=", + "dev": true + }, + "is-regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", + "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "requires": { + "has": "^1.0.1" + } + }, + "is-resolvable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", + "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==" + }, + "is-retry-allowed": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz", + "integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=", + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=" + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, + "is_js": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/is_js/-/is_js-0.9.0.tgz", + "integrity": "sha1-CrlFQFArp6+iTIVqqYVWFmnpxS0=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isemail": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/isemail/-/isemail-1.2.0.tgz", + "integrity": "sha1-vgPfjMPineTSxd9lASY/H6RZXpo=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "requires": { + "isarray": "1.0.0" + } + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "istanbul": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz", + "integrity": "sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs=", + "dev": true, + "requires": { + "abbrev": "1.0.x", + "async": "1.x", + "escodegen": "1.8.x", + "esprima": "2.7.x", + "glob": "^5.0.15", + "handlebars": "^4.0.1", + "js-yaml": "3.x", + "mkdirp": "0.5.x", + "nopt": "3.x", + "once": "1.x", + "resolve": "1.1.x", + "supports-color": "^3.1.0", + "which": "^1.1.1", + "wordwrap": "^1.0.0" + }, + "dependencies": { + "abbrev": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", + "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=", + "dev": true + }, + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "escodegen": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", + "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", + "dev": true, + "requires": { + "esprima": "^2.7.1", + "estraverse": "^1.9.1", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.2.0" + } + }, + "esprima": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", + "dev": true + }, + "estraverse": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", + "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=", + "dev": true + }, + "glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "dev": true, + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true + }, + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "dev": true, + "requires": { + "abbrev": "1" + } + }, + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + }, + "source-map": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", + "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", + "dev": true, + "optional": true, + "requires": { + "amdefine": ">=0.0.4" + } + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true, + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "istanbul-lib-coverage": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.0.tgz", + "integrity": "sha512-GvgM/uXRwm+gLlvkWHTjDAvwynZkL9ns15calTrmhGgowlwJBbWMYzWbKqE2DT6JDP1AFXKa+Zi0EkqNCUqY0A==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.1.tgz", + "integrity": "sha512-1dYuzkOCbuR5GRJqySuZdsmsNKPL3PTuyPevQfoCXJePT9C8y1ga75neU+Tuy9+yS3G/dgx8wgOmp2KLpgdoeQ==", + "dev": true, + "requires": { + "babel-generator": "^6.18.0", + "babel-template": "^6.16.0", + "babel-traverse": "^6.18.0", + "babel-types": "^6.18.0", + "babylon": "^6.18.0", + "istanbul-lib-coverage": "^1.2.0", + "semver": "^5.3.0" + } + }, + "jmespath": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", + "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" + }, + "joi": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/joi/-/joi-6.10.1.tgz", + "integrity": "sha1-TVDDGAeRIgAP5fFq8f+OGRe3fgY=", + "requires": { + "hoek": "2.x.x", + "isemail": "1.x.x", + "moment": "2.x.x", + "topo": "1.x.x" + } + }, + "js-stringify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", + "integrity": "sha1-Fzb939lyTyijaCrcYjCufk6Weds=" + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=" + }, + "js-yaml": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.11.0.tgz", + "integrity": "sha512-saJstZWv7oNeOyBh3+Dx1qWzhW0+e6/8eDzo7p5rDFqxntSztloLtuKu+Ejhtq82jsilwOIZYsCz+lIjthg1Hw==", + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "optional": true + }, + "jsdom": { + "version": "11.8.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-11.8.0.tgz", + "integrity": "sha512-fZZSH6P8tVqYIQl0WKpZuQljPu2cW41Uj/c9omtyGwjwZCB8c82UAi7BSQs/F1FgWovmZsoU02z3k28eHp0Cdw==", + "requires": { + "abab": "^1.0.4", + "acorn": "^5.3.0", + "acorn-globals": "^4.1.0", + "array-equal": "^1.0.0", + "cssom": ">= 0.3.2 < 0.4.0", + "cssstyle": ">= 0.2.37 < 0.3.0", + "data-urls": "^1.0.0", + "domexception": "^1.0.0", + "escodegen": "^1.9.0", + "html-encoding-sniffer": "^1.0.2", + "left-pad": "^1.2.0", + "nwmatcher": "^1.4.3", + "parse5": "4.0.0", + "pn": "^1.1.0", + "request": "^2.83.0", + "request-promise-native": "^1.0.5", + "sax": "^1.2.4", + "symbol-tree": "^3.2.2", + "tough-cookie": "^2.3.3", + "w3c-hr-time": "^1.0.1", + "webidl-conversions": "^4.0.2", + "whatwg-encoding": "^1.0.3", + "whatwg-mimetype": "^2.1.0", + "whatwg-url": "^6.4.0", + "ws": "^4.0.0", + "xml-name-validator": "^3.0.0" + }, + "dependencies": { + "acorn-globals": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.1.0.tgz", + "integrity": "sha512-KjZwU26uG3u6eZcfGbTULzFcsoz6pegNKtHPksZPOUsiKo5bUmiBPa38FuHZ/Eun+XYh/JCCkS9AS3Lu4McQOQ==", + "requires": { + "acorn": "^5.0.0" + } + }, + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "requires": { + "co": "^4.6.0", + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "boom": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", + "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", + "requires": { + "hoek": "4.x.x" + } + }, + "cryptiles": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", + "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", + "requires": { + "boom": "5.x.x" + }, + "dependencies": { + "boom": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", + "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", + "requires": { + "hoek": "4.x.x" + } + } + } + }, + "form-data": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", + "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "1.0.6", + "mime-types": "^2.1.12" + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", + "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", + "requires": { + "ajv": "^5.1.0", + "har-schema": "^2.0.0" + } + }, + "hawk": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", + "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", + "requires": { + "boom": "4.x.x", + "cryptiles": "3.x.x", + "hoek": "4.x.x", + "sntp": "2.x.x" + } + }, + "hoek": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", + "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==" + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "request": { + "version": "2.85.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.85.0.tgz", + "integrity": "sha512-8H7Ehijd4js+s6wuVPLjwORxD4zeuyjYugprdOXlPSqaApmL/QOy+EB/beICHVCHkGMKNh5rvihb5ov+IDw4mg==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.6.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.5", + "extend": "~3.0.1", + "forever-agent": "~0.6.1", + "form-data": "~2.3.1", + "har-validator": "~5.0.3", + "hawk": "~6.0.2", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.17", + "oauth-sign": "~0.8.2", + "performance-now": "^2.1.0", + "qs": "~6.5.1", + "safe-buffer": "^5.1.1", + "stringstream": "~0.0.5", + "tough-cookie": "~2.3.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.1.0" + } + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "sntp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", + "integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==", + "requires": { + "hoek": "4.x.x" + } + }, + "ws": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-4.1.0.tgz", + "integrity": "sha512-ZGh/8kF9rrRNffkLFV4AzhvooEclrOH0xaugmqGsIfFgOE/pIz4fMc4Ef+5HSQqTEug2S9JZIWDR47duDSLfaA==", + "requires": { + "async-limiter": "~1.0.0", + "safe-buffer": "~5.1.0" + } + } + } + }, + "jsesc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", + "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=" + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" + }, + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "requires": { + "jsonify": "~0.0.0" + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=" + }, + "jsonfile": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz", + "integrity": "sha1-pezG9l9T9mLEQVx2daAzHQmS7GY=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" + }, + "jsonpointer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", + "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=" + }, + "jsonschema": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.2.4.tgz", + "integrity": "sha512-lz1nOH69GbsVHeVgEdvyavc/33oymY1AZwtePMiMj4HZPMbP5OIKK3zT9INMWjwua/V4Z4yq7wSlBbSG+g4AEw==" + }, + "jsonwebtoken": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.2.1.tgz", + "integrity": "sha512-l8rUBr0fqYYwPc8/ZGrue7GiW7vWdZtZqelxo4Sd5lMvuEeCK8/wS54sEo6tJhdZ6hqfutsj6COgC0d1XdbHGw==", + "requires": { + "jws": "^3.1.4", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "xtend": "^4.0.1" + }, + "dependencies": { + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } + } + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "jstransformer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz", + "integrity": "sha1-7Yvwkh4vPx7U1cGkT2hwntJHIsM=", + "requires": { + "is-promise": "^2.0.0", + "promise": "^7.0.1" + } + }, + "jwa": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.1.5.tgz", + "integrity": "sha1-oFUs4CIHQs1S4VN3SjKQXDDnVuU=", + "requires": { + "base64url": "2.0.0", + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.9", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.1.4.tgz", + "integrity": "sha1-+ei5M46KhHJ31kRLFGT2GIDgUKI=", + "requires": { + "base64url": "^2.0.0", + "jwa": "^1.1.4", + "safe-buffer": "^5.0.1" + } + }, + "kareem": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-1.5.0.tgz", + "integrity": "sha1-4+QQHZ3P3imXadr0tNtk2JXRdEg=" + }, + "kind-of": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-2.0.1.tgz", + "integrity": "sha1-AY7HpM5+OobLkUG+UZ0kyPqpgbU=", + "requires": { + "is-buffer": "^1.0.2" + } + }, + "klaw-sync": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-2.1.0.tgz", + "integrity": "sha1-PTvNhgDnv971MjHHOf8FOu1WDkQ=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11" + } + }, + "kue": { + "version": "0.11.6", + "resolved": "https://registry.npmjs.org/kue/-/kue-0.11.6.tgz", + "integrity": "sha512-56Jic22qSqdJ3nNpkhVr6RUx/QKalfdBdU0m70hgBKEkhBAgdt6Qr74evec+bM+LGmNbEC6zGGDskX4mcgBYcQ==", + "requires": { + "body-parser": "^1.12.2", + "express": "^4.12.2", + "lodash": "^4.0.0", + "nib": "~1.1.2", + "node-redis-warlock": "~0.2.0", + "pug": "^2.0.0-beta3", + "redis": "~2.6.0-2", + "reds": "^0.2.5", + "stylus": "~0.54.5", + "yargs": "^4.0.0" + } + }, + "kuler": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-1.0.1.tgz", + "integrity": "sha512-J9nVUucG1p/skKul6DU3PUZrhs0LPulNaeUOox0IyXDi8S4CztTHs1gQphhuZmzXG7VOQSf6NJfKuzteQLv9gQ==", + "dev": true, + "requires": { + "colornames": "^1.1.1" + } + }, + "latest-version": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz", + "integrity": "sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=", + "dev": true, + "requires": { + "package-json": "^4.0.0" + } + }, + "lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=" + }, + "lazystream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", + "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", + "requires": { + "readable-stream": "^2.0.5" + } + }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "requires": { + "invert-kv": "^1.0.0" + } + }, + "left-pad": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz", + "integrity": "sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA==" + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "linkify-it": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.1.0.tgz", + "integrity": "sha512-4REs8/062kV2DSHxNfq5183zrqXMl7WP0WzABH9IeJI+NLm429FgE1PDecltYfnOoFDFlZGh2T8PfZn0r+GTRg==", + "dev": true, + "requires": { + "uc.micro": "^1.0.1" + } + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + }, + "dependencies": { + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "requires": { + "is-utf8": "^0.2.0" + } + } + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + }, + "dependencies": { + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + } + } + }, + "lodash": { + "version": "4.17.5", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz", + "integrity": "sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw==" + }, + "lodash.assign": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", + "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=" + }, + "lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", + "dev": true + }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" + }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" + }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" + }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=" + }, + "logform": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-1.10.0.tgz", + "integrity": "sha512-em5ojIhU18fIMOw/333mD+ZLE2fis0EzXl1ZwHx4iQzmpQi6odNiY/t+ITNr33JZhT9/KEaH+UPIipr6a9EjWg==", + "dev": true, + "requires": { + "colors": "^1.2.1", + "fast-safe-stringify": "^2.0.4", + "fecha": "^2.3.3", + "ms": "^2.1.1", + "triple-beam": "^1.2.0" + }, + "dependencies": { + "colors": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.3.3.tgz", + "integrity": "sha512-mmGt/1pZqYRjMxB1axhTo16/snVZ5krrKkcmMeVKxzECMMXoCgnvTPp10QgHfcbQZw8Dq2jMNG6je4JlWU0gWg==", + "dev": true + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + } + } + }, + "login-with-amazon": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/login-with-amazon/-/login-with-amazon-0.0.3.tgz", + "integrity": "sha1-raYDpq5s9vB/ReQMdmekR5KjVcI=", + "requires": { + "bhttp": "^1.2.4" + } + }, + "longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=" + }, + "loose-envify": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", + "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", + "requires": { + "js-tokens": "^3.0.0" + } + }, + "lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "dev": true + }, + "lru-cache": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=" + }, + "make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "requires": { + "pify": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true + }, + "map-stream": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", + "integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=", + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, + "requires": { + "object-visit": "^1.0.0" + } + }, + "markdown": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/markdown/-/markdown-0.5.0.tgz", + "integrity": "sha1-KCBbVlqK51kt4gdGPWY33BgnIrI=", + "requires": { + "nopt": "~2.1.1" + } + }, + "markdown-it": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-8.4.2.tgz", + "integrity": "sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "entities": "~1.1.1", + "linkify-it": "^2.0.0", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + } + }, + "md-recast": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/md-recast/-/md-recast-0.0.2.tgz", + "integrity": "sha512-5GeJBJA1gXZ7Hd2L6PKK++dijiBs75UTPcnzToIjCVFMA84B2J4rYzP4PTcovFsHfFChPopREW21TdgGPqL1aQ==", + "requires": { + "markdown": "0.5.0" + } + }, + "mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=", + "dev": true + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "requires": { + "arr-diff": "^2.0.0", + "array-unique": "^0.2.1", + "braces": "^1.8.2", + "expand-brackets": "^0.1.4", + "extglob": "^0.3.1", + "filename-regex": "^2.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.1", + "kind-of": "^3.0.2", + "normalize-path": "^2.0.1", + "object.omit": "^2.0.0", + "parse-glob": "^3.0.4", + "regex-cache": "^0.4.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" + }, + "mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "requires": { + "mime-db": "~1.33.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + }, + "mixin-deep": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", + "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", + "dev": true, + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + } + }, + "mocha": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.1.1.tgz", + "integrity": "sha512-kKKs/H1KrMMQIEsWNxGmb4/BGsmj0dkeyotEvbrAuQ01FcWRLssUNXCEUZk6SZtyJBi6EE7SL0zDDtItw1rGhw==", + "requires": { + "browser-stdout": "1.3.1", + "commander": "2.11.0", + "debug": "3.1.0", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.3", + "he": "1.1.1", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "supports-color": "4.4.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "supports-color": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz", + "integrity": "sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==", + "requires": { + "has-flag": "^2.0.0" + } + } + } + }, + "moment": { + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.1.tgz", + "integrity": "sha512-shJkRTSebXvsVqk56I+lkb2latjBs8I+pc2TzWc545y2iFnSjm7Wg0QMh+ZWcdSLQyGEau5jI8ocnmkyTgr9YQ==" + }, + "mongodb": { + "version": "2.2.34", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-2.2.34.tgz", + "integrity": "sha1-o09Zu+thdUrsQy3nLD/iFSakTBo=", + "requires": { + "es6-promise": "3.2.1", + "mongodb-core": "2.1.18", + "readable-stream": "2.2.7" + }, + "dependencies": { + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" + }, + "readable-stream": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.7.tgz", + "integrity": "sha1-BwV6y+JGeyIELTb5jFrVBwVOlbE=", + "requires": { + "buffer-shims": "~1.0.0", + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "string_decoder": "~1.0.0", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "mongodb-core": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-2.1.18.tgz", + "integrity": "sha1-TEYTm986HwMt7ZHbSfOO7AFlkFA=", + "requires": { + "bson": "~1.0.4", + "require_optional": "~1.0.0" + } + }, + "mongoose": { + "version": "4.13.12", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-4.13.12.tgz", + "integrity": "sha512-pH8NK5AYGbnPeEFFGs5ACk18vzzcy4DFT48U9kKvkfg6SI3nJZkzGfN7o1NDWjy+kP26hWyU/AMhYTfe5hSVnA==", + "requires": { + "async": "2.1.4", + "bson": "~1.0.4", + "hooks-fixed": "2.0.2", + "kareem": "1.5.0", + "lodash.get": "4.4.2", + "mongodb": "2.2.34", + "mpath": "0.3.0", + "mpromise": "0.5.5", + "mquery": "2.3.3", + "ms": "2.0.0", + "muri": "1.3.0", + "regexp-clone": "0.0.1", + "sliced": "1.0.1" + }, + "dependencies": { + "async": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.1.4.tgz", + "integrity": "sha1-LSFgx3iAMuTdbL4lAvH5osj2zeQ=", + "requires": { + "lodash": "^4.14.0" + } + } + } + }, + "morgan": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.0.tgz", + "integrity": "sha1-0B+mxlhZt2/PMbPLU6OCGjEdgFE=", + "requires": { + "basic-auth": "~2.0.0", + "debug": "2.6.9", + "depd": "~1.1.1", + "on-finished": "~2.3.0", + "on-headers": "~1.0.1" + } + }, + "mpath": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.3.0.tgz", + "integrity": "sha1-elj3iem1/TyUUgY0FXlg8mvV70Q=" + }, + "mpromise": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mpromise/-/mpromise-0.5.5.tgz", + "integrity": "sha1-9bJCWddjrMIlewoMjG2Gb9UXMuY=" + }, + "mquery": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-2.3.3.tgz", + "integrity": "sha512-NC8L14kn+qxJbbJ1gbcEMDxF0sC3sv+1cbRReXXwVvowcwY1y9KoVZFq0ebwARibsadu8lx8nWGvm3V0Pf0ZWQ==", + "requires": { + "bluebird": "3.5.0", + "debug": "2.6.9", + "regexp-clone": "0.0.1", + "sliced": "0.0.5" + }, + "dependencies": { + "bluebird": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.0.tgz", + "integrity": "sha1-eRQg1/VR7qKJdFOop3ZT+WYG1nw=" + }, + "sliced": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/sliced/-/sliced-0.0.5.tgz", + "integrity": "sha1-XtwETKTrb3gW1Qui/GPiXY/kcH8=" + } + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "muri": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/muri/-/muri-1.3.0.tgz", + "integrity": "sha512-FiaFwKl864onHFFUV/a2szAl7X0fxVlSKNdhTf+BM8i8goEgYut8u5P9MqQqIYwvaMxjzVESsoEm/2kfkFH1rg==" + }, + "mute-stream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz", + "integrity": "sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA=" + }, + "nan": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", + "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", + "dev": true, + "optional": true + }, + "nanomatch": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.9.tgz", + "integrity": "sha512-n8R9bS8yQ6eSXaV6jHUpKzD8gLsin02w1HSFiegwrs9E098Ylhw5jdyKPaYqvHknHaSCKTPp7C8dGCQ0q9koXA==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-odd": "^2.0.0", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } + }, + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "natural": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/natural/-/natural-0.2.1.tgz", + "integrity": "sha1-HrUVap2QtFkZSeIOlOvHe7Iznto=", + "optional": true, + "requires": { + "apparatus": ">= 0.0.9", + "sylvester": ">= 0.0.12", + "underscore": ">=1.3.1" + } + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=" + }, + "negotiator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" + }, + "next-tick": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", + "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" + }, + "nib": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/nib/-/nib-1.1.2.tgz", + "integrity": "sha1-amnt5AgblcDe+L4CSkyK4MLLtsc=", + "requires": { + "stylus": "0.54.5" + } + }, + "nock": { + "version": "9.2.5", + "resolved": "https://registry.npmjs.org/nock/-/nock-9.2.5.tgz", + "integrity": "sha512-ciCpyEq72Ws6/yhdayDfd0mAb3eQ7/533xKmFlBQZ5CDwrL0/bddtSicfL7R07oyvPAuegQrR+9ctrlPEp0EjQ==", + "dev": true, + "requires": { + "chai": "^4.1.2", + "debug": "^3.1.0", + "deep-equal": "^1.0.0", + "json-stringify-safe": "^5.0.1", + "lodash": "^4.17.5", + "mkdirp": "^0.5.0", + "propagate": "^1.0.0", + "qs": "^6.5.1", + "semver": "^5.5.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "node-forge": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.5.tgz", + "integrity": "sha512-MmbQJ2MTESTjt3Gi/3yG1wGpIMhUfcIypUCGtTizFR9IiccFwxSpfp0vtIZlkFclEqERemxfnSdZEMR9VqqEFQ==" + }, + "node-redis-scripty": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/node-redis-scripty/-/node-redis-scripty-0.0.5.tgz", + "integrity": "sha1-S/LTZattqyAswIt6xj+PVarcliU=", + "requires": { + "extend": "^1.2.1", + "lru-cache": "^2.5.0" + }, + "dependencies": { + "extend": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extend/-/extend-1.3.0.tgz", + "integrity": "sha1-0VFvsP9WJNLr+RI+odrFoZlABPg=" + } + } + }, + "node-redis-warlock": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/node-redis-warlock/-/node-redis-warlock-0.2.0.tgz", + "integrity": "sha1-VjlbmUyCjo4y9qrlO5O27fzZeZA=", + "requires": { + "node-redis-scripty": "0.0.5", + "uuid": "^2.0.1" + }, + "dependencies": { + "uuid": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz", + "integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=" + } + } + }, + "node-sparky": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/node-sparky/-/node-sparky-4.4.0.tgz", + "integrity": "sha512-gJZ4TtGMjLl6jziisaZNRkQc3roPeFnN6GIg2BbYC/20qE/h3gH/VokrG9lwzzlQUeRt6lwMo+/rxpIcEifESg==", + "requires": { + "lodash": "4.13.1", + "mime-types": "2.1.x", + "moment": "2.20.x", + "request": "2.79.x", + "validator": "9.1.x", + "when": "3.7.x" + }, + "dependencies": { + "caseless": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", + "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=" + }, + "har-validator": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz", + "integrity": "sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=", + "requires": { + "chalk": "^1.1.1", + "commander": "^2.9.0", + "is-my-json-valid": "^2.12.4", + "pinkie-promise": "^2.0.0" + } + }, + "lodash": { + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.13.1.tgz", + "integrity": "sha1-g+SxCRP0hJbU0W/sSlYK8u50S2g=" + }, + "moment": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.20.1.tgz", + "integrity": "sha512-Yh9y73JRljxW5QxN08Fner68eFLxM5ynNOAw2LbIB1YAGeQzZT8QFSUvkAz609Zf+IHhhaUxqZK8dG3W/+HEvg==" + }, + "qs": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.3.2.tgz", + "integrity": "sha1-51vV9uJoEioqDgvaYwslUMFmUCw=" + }, + "request": { + "version": "2.79.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.79.0.tgz", + "integrity": "sha1-Tf5b9r6LjNw3/Pk+BLZVd3InEN4=", + "requires": { + "aws-sign2": "~0.6.0", + "aws4": "^1.2.1", + "caseless": "~0.11.0", + "combined-stream": "~1.0.5", + "extend": "~3.0.0", + "forever-agent": "~0.6.1", + "form-data": "~2.1.1", + "har-validator": "~2.0.6", + "hawk": "~3.1.3", + "http-signature": "~1.1.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.7", + "oauth-sign": "~0.8.1", + "qs": "~6.3.0", + "stringstream": "~0.0.4", + "tough-cookie": "~2.3.0", + "tunnel-agent": "~0.4.1", + "uuid": "^3.0.0" + } + }, + "tunnel-agent": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz", + "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=" + }, + "validator": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/validator/-/validator-9.1.2.tgz", + "integrity": "sha512-1Tml6crNdsSC61jHssWksQxq6C7MmSFCCmf99Eb+l/V/cwVlw4/Pg3YXBP1WKcHLsyqe3E+iJXUZgoTTQFcqQg==" + } + } + }, + "nodemailer": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-4.6.4.tgz", + "integrity": "sha512-SD4uuX7NMzZ5f5m1XHDd13J4UC3SmdJk8DsmU1g6Nrs5h3x9LcXr6EBPZIqXRJ3LrF7RdklzGhZRF/TuylTcLg==" + }, + "nodemon": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-1.17.5.tgz", + "integrity": "sha512-FG2mWJU1Y58a9ktgMJ/RZpsiPz3b7ge77t/okZHEa4NbrlXGKZ8s1A6Q+C7+JPXohAfcPALRwvxcAn8S874pmw==", + "dev": true, + "requires": { + "chokidar": "^2.0.2", + "debug": "^3.1.0", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.0.4", + "pstree.remy": "^1.1.0", + "semver": "^5.5.0", + "supports-color": "^5.2.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.2", + "update-notifier": "^2.3.0" + }, + "dependencies": { + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "chokidar": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz", + "integrity": "sha512-z9n7yt9rOvIJrMhvDtDictKrkFHeihkNl6uWMmZlmL6tJtX9Cs+87oK+teBx+JIgzvbX3yZHT3eF8vpbDxHJXQ==", + "dev": true, + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.0", + "braces": "^2.3.0", + "fsevents": "^1.2.2", + "glob-parent": "^3.1.0", + "inherits": "^2.0.1", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "lodash.debounce": "^4.0.8", + "normalize-path": "^2.1.1", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.0.0", + "upath": "^1.0.5" + } + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fsevents": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", + "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", + "dev": true, + "optional": true, + "requires": { + "nan": "^2.9.2", + "node-pre-gyp": "^0.10.0" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "dev": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "debug": { + "version": "2.6.9", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "glob": { + "version": "7.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.21", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safer-buffer": "^2.1.0" + } + }, + "ignore-walk": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "dev": true + }, + "ini": { + "version": "1.3.5", + "bundled": true, + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "dev": true + }, + "minipass": { + "version": "2.2.4", + "bundled": true, + "dev": true, + "requires": { + "safe-buffer": "^5.1.1", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "needle": { + "version": "2.2.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "debug": "^2.1.2", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.10.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.0", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.1.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "npm-packlist": { + "version": "1.1.10", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "rc": { + "version": "1.2.7", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "deep-extend": "^0.5.1", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "rimraf": { + "version": "2.6.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "glob": "^7.0.5" + } + }, + "safe-buffer": { + "version": "5.1.1", + "bundled": true, + "dev": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "bundled": true, + "dev": true, + "optional": true + }, + "semver": { + "version": "5.5.0", + "bundled": true, + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "tar": { + "version": "4.4.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "chownr": "^1.0.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.2.4", + "minizlib": "^1.1.0", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.1", + "yallist": "^3.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "wide-align": { + "version": "1.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "string-width": "^1.0.2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "yallist": { + "version": "3.0.2", + "bundled": true, + "dev": true + } + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-glob": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", + "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "nopt": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-2.1.2.tgz", + "integrity": "sha1-bMzZd7gBMqB3MdbozljCyDA8+a8=", + "requires": { + "abbrev": "1" + } + }, + "normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", + "requires": { + "hosted-git-info": "^2.1.4", + "is-builtin-module": "^1.0.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "requires": { + "remove-trailing-separator": "^1.0.1" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "^2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + }, + "nwmatcher": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/nwmatcher/-/nwmatcher-1.4.4.tgz", + "integrity": "sha512-3iuY4N5dhgMpCUrOVnuAdGrgxVqV2cJpM+XNccjR2DKOB1RUP0aA+wGXEiNziG/UKboFyGBIoKOaNlJxx8bciQ==" + }, + "nyc": { + "version": "11.7.1", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-11.7.1.tgz", + "integrity": "sha512-EGePURSKUEpS1jWnEKAMhY+GWZzi7JC+f8iBDOATaOsLZW5hM/9eYx2dHGaEXa1ITvMm44CJugMksvP3NwMQMw==", + "dev": true, + "requires": { + "archy": "^1.0.0", + "arrify": "^1.0.1", + "caching-transform": "^1.0.0", + "convert-source-map": "^1.5.1", + "debug-log": "^1.0.1", + "default-require-extensions": "^1.0.0", + "find-cache-dir": "^0.1.1", + "find-up": "^2.1.0", + "foreground-child": "^1.5.3", + "glob": "^7.0.6", + "istanbul-lib-coverage": "^1.1.2", + "istanbul-lib-hook": "^1.1.0", + "istanbul-lib-instrument": "^1.10.0", + "istanbul-lib-report": "^1.1.3", + "istanbul-lib-source-maps": "^1.2.3", + "istanbul-reports": "^1.4.0", + "md5-hex": "^1.2.0", + "merge-source-map": "^1.0.2", + "micromatch": "^2.3.11", + "mkdirp": "^0.5.0", + "resolve-from": "^2.0.0", + "rimraf": "^2.5.4", + "signal-exit": "^3.0.1", + "spawn-wrap": "^1.4.2", + "test-exclude": "^4.2.0", + "yargs": "11.1.0", + "yargs-parser": "^8.0.0" + }, + "dependencies": { + "align-text": { + "version": "0.1.4", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "^3.0.2", + "longest": "^1.0.1", + "repeat-string": "^1.5.2" + } + }, + "amdefine": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "bundled": true, + "dev": true + }, + "append-transform": { + "version": "0.4.0", + "bundled": true, + "dev": true, + "requires": { + "default-require-extensions": "^1.0.0" + } + }, + "archy": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "arr-diff": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "arr-flatten": "^1.0.1" + } + }, + "arr-flatten": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "arr-union": { + "version": "3.1.0", + "bundled": true, + "dev": true + }, + "array-unique": { + "version": "0.2.1", + "bundled": true, + "dev": true + }, + "arrify": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "assign-symbols": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "async": { + "version": "1.5.2", + "bundled": true, + "dev": true + }, + "atob": { + "version": "2.1.0", + "bundled": true, + "dev": true + }, + "babel-code-frame": { + "version": "6.26.0", + "bundled": true, + "dev": true, + "requires": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + } + }, + "babel-generator": { + "version": "6.26.1", + "bundled": true, + "dev": true, + "requires": { + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "detect-indent": "^4.0.0", + "jsesc": "^1.3.0", + "lodash": "^4.17.4", + "source-map": "^0.5.7", + "trim-right": "^1.0.1" + } + }, + "babel-messages": { + "version": "6.23.0", + "bundled": true, + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-runtime": { + "version": "6.26.0", + "bundled": true, + "dev": true, + "requires": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + } + }, + "babel-template": { + "version": "6.26.0", + "bundled": true, + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "lodash": "^4.17.4" + } + }, + "babel-traverse": { + "version": "6.26.0", + "bundled": true, + "dev": true, + "requires": { + "babel-code-frame": "^6.26.0", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "debug": "^2.6.8", + "globals": "^9.18.0", + "invariant": "^2.2.2", + "lodash": "^4.17.4" + } + }, + "babel-types": { + "version": "6.26.0", + "bundled": true, + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + }, + "babylon": { + "version": "6.18.0", + "bundled": true, + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "base": { + "version": "0.11.2", + "bundled": true, + "dev": true, + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "bundled": true, + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "bundled": true, + "dev": true + } + } + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "1.8.5", + "bundled": true, + "dev": true, + "requires": { + "expand-range": "^1.8.1", + "preserve": "^0.2.0", + "repeat-element": "^1.1.2" + } + }, + "builtin-modules": { + "version": "1.1.1", + "bundled": true, + "dev": true + }, + "cache-base": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "bundled": true, + "dev": true + } + } + }, + "caching-transform": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "requires": { + "md5-hex": "^1.2.0", + "mkdirp": "^0.5.1", + "write-file-atomic": "^1.1.4" + } + }, + "camelcase": { + "version": "1.2.1", + "bundled": true, + "dev": true, + "optional": true + }, + "center-align": { + "version": "0.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "align-text": "^0.1.3", + "lazy-cache": "^1.0.3" + } + }, + "chalk": { + "version": "1.1.3", + "bundled": true, + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "class-utils": { + "version": "0.3.6", + "bundled": true, + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "bundled": true, + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "isobject": { + "version": "3.0.1", + "bundled": true, + "dev": true + } + } + }, + "cliui": { + "version": "2.1.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "center-align": "^0.1.1", + "right-align": "^0.1.1", + "wordwrap": "0.0.2" + }, + "dependencies": { + "wordwrap": { + "version": "0.0.2", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "collection-visit": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "commondir": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "component-emitter": { + "version": "1.2.1", + "bundled": true, + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true + }, + "convert-source-map": { + "version": "1.5.1", + "bundled": true, + "dev": true + }, + "copy-descriptor": { + "version": "0.1.1", + "bundled": true, + "dev": true + }, + "core-js": { + "version": "2.5.5", + "bundled": true, + "dev": true + }, + "cross-spawn": { + "version": "4.0.2", + "bundled": true, + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "which": "^1.2.9" + } + }, + "debug": { + "version": "2.6.9", + "bundled": true, + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "debug-log": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "decamelize": { + "version": "1.2.0", + "bundled": true, + "dev": true + }, + "decode-uri-component": { + "version": "0.2.0", + "bundled": true, + "dev": true + }, + "default-require-extensions": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "strip-bom": "^2.0.0" + } + }, + "define-property": { + "version": "2.0.2", + "bundled": true, + "dev": true, + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "bundled": true, + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "bundled": true, + "dev": true + } + } + }, + "detect-indent": { + "version": "4.0.0", + "bundled": true, + "dev": true, + "requires": { + "repeating": "^2.0.0" + } + }, + "error-ex": { + "version": "1.3.1", + "bundled": true, + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "bundled": true, + "dev": true + }, + "esutils": { + "version": "2.0.2", + "bundled": true, + "dev": true + }, + "execa": { + "version": "0.7.0", + "bundled": true, + "dev": true, + "requires": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "5.1.0", + "bundled": true, + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + } + } + }, + "expand-brackets": { + "version": "0.1.5", + "bundled": true, + "dev": true, + "requires": { + "is-posix-bracket": "^0.1.0" + } + }, + "expand-range": { + "version": "1.8.2", + "bundled": true, + "dev": true, + "requires": { + "fill-range": "^2.1.0" + } + }, + "extend-shallow": { + "version": "3.0.2", + "bundled": true, + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "extglob": { + "version": "0.3.2", + "bundled": true, + "dev": true, + "requires": { + "is-extglob": "^1.0.0" + } + }, + "filename-regex": { + "version": "2.0.1", + "bundled": true, + "dev": true + }, + "fill-range": { + "version": "2.2.3", + "bundled": true, + "dev": true, + "requires": { + "is-number": "^2.1.0", + "isobject": "^2.0.0", + "randomatic": "^1.1.3", + "repeat-element": "^1.1.2", + "repeat-string": "^1.5.2" + } + }, + "find-cache-dir": { + "version": "0.1.1", + "bundled": true, + "dev": true, + "requires": { + "commondir": "^1.0.1", + "mkdirp": "^0.5.1", + "pkg-dir": "^1.0.0" + } + }, + "find-up": { + "version": "2.1.0", + "bundled": true, + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "for-in": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "for-own": { + "version": "0.1.5", + "bundled": true, + "dev": true, + "requires": { + "for-in": "^1.0.1" + } + }, + "foreground-child": { + "version": "1.5.6", + "bundled": true, + "dev": true, + "requires": { + "cross-spawn": "^4", + "signal-exit": "^3.0.0" + } + }, + "fragment-cache": { + "version": "0.2.1", + "bundled": true, + "dev": true, + "requires": { + "map-cache": "^0.2.2" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "get-caller-file": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "get-stream": { + "version": "3.0.0", + "bundled": true, + "dev": true + }, + "get-value": { + "version": "2.0.6", + "bundled": true, + "dev": true + }, + "glob": { + "version": "7.1.2", + "bundled": true, + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-base": { + "version": "0.3.0", + "bundled": true, + "dev": true, + "requires": { + "glob-parent": "^2.0.0", + "is-glob": "^2.0.0" + } + }, + "glob-parent": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "is-glob": "^2.0.0" + } + }, + "globals": { + "version": "9.18.0", + "bundled": true, + "dev": true + }, + "graceful-fs": { + "version": "4.1.11", + "bundled": true, + "dev": true + }, + "handlebars": { + "version": "4.0.11", + "bundled": true, + "dev": true, + "requires": { + "async": "^1.4.0", + "optimist": "^0.6.1", + "source-map": "^0.4.4", + "uglify-js": "^2.6" + }, + "dependencies": { + "source-map": { + "version": "0.4.4", + "bundled": true, + "dev": true, + "requires": { + "amdefine": ">=0.0.4" + } + } + } + }, + "has-ansi": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "has-flag": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "has-value": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "bundled": true, + "dev": true + } + } + }, + "has-values": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "bundled": true, + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "bundled": true, + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "hosted-git-info": { + "version": "2.6.0", + "bundled": true, + "dev": true + }, + "imurmurhash": { + "version": "0.1.4", + "bundled": true, + "dev": true + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "dev": true + }, + "invariant": { + "version": "2.2.4", + "bundled": true, + "dev": true, + "requires": { + "loose-envify": "^1.0.0" + } + }, + "invert-kv": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-arrayish": { + "version": "0.2.1", + "bundled": true, + "dev": true + }, + "is-buffer": { + "version": "1.1.6", + "bundled": true, + "dev": true + }, + "is-builtin-module": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "builtin-modules": "^1.0.0" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-descriptor": { + "version": "0.1.6", + "bundled": true, + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "bundled": true, + "dev": true + } + } + }, + "is-dotfile": { + "version": "1.0.3", + "bundled": true, + "dev": true + }, + "is-equal-shallow": { + "version": "0.1.3", + "bundled": true, + "dev": true, + "requires": { + "is-primitive": "^2.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "bundled": true, + "dev": true + }, + "is-extglob": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "is-finite": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "is-extglob": "^1.0.0" + } + }, + "is-number": { + "version": "2.1.0", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-odd": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "is-number": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "4.0.0", + "bundled": true, + "dev": true + } + } + }, + "is-plain-object": { + "version": "2.0.4", + "bundled": true, + "dev": true, + "requires": { + "isobject": "^3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "bundled": true, + "dev": true + } + } + }, + "is-posix-bracket": { + "version": "0.1.1", + "bundled": true, + "dev": true + }, + "is-primitive": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "is-utf8": { + "version": "0.2.1", + "bundled": true, + "dev": true + }, + "is-windows": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "isexe": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "isobject": { + "version": "2.1.0", + "bundled": true, + "dev": true, + "requires": { + "isarray": "1.0.0" + } + }, + "istanbul-lib-coverage": { + "version": "1.2.0", + "bundled": true, + "dev": true + }, + "istanbul-lib-hook": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "requires": { + "append-transform": "^0.4.0" + } + }, + "istanbul-lib-instrument": { + "version": "1.10.1", + "bundled": true, + "dev": true, + "requires": { + "babel-generator": "^6.18.0", + "babel-template": "^6.16.0", + "babel-traverse": "^6.18.0", + "babel-types": "^6.18.0", + "babylon": "^6.18.0", + "istanbul-lib-coverage": "^1.2.0", + "semver": "^5.3.0" + } + }, + "istanbul-lib-report": { + "version": "1.1.3", + "bundled": true, + "dev": true, + "requires": { + "istanbul-lib-coverage": "^1.1.2", + "mkdirp": "^0.5.1", + "path-parse": "^1.0.5", + "supports-color": "^3.1.2" + }, + "dependencies": { + "supports-color": { + "version": "3.2.3", + "bundled": true, + "dev": true, + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "1.2.3", + "bundled": true, + "dev": true, + "requires": { + "debug": "^3.1.0", + "istanbul-lib-coverage": "^1.1.2", + "mkdirp": "^0.5.1", + "rimraf": "^2.6.1", + "source-map": "^0.5.3" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "bundled": true, + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "istanbul-reports": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "requires": { + "handlebars": "^4.0.3" + } + }, + "js-tokens": { + "version": "3.0.2", + "bundled": true, + "dev": true + }, + "jsesc": { + "version": "1.3.0", + "bundled": true, + "dev": true + }, + "kind-of": { + "version": "3.2.2", + "bundled": true, + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + }, + "lazy-cache": { + "version": "1.0.4", + "bundled": true, + "dev": true, + "optional": true + }, + "lcid": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "invert-kv": "^1.0.0" + } + }, + "load-json-file": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + }, + "dependencies": { + "path-exists": { + "version": "3.0.0", + "bundled": true, + "dev": true + } + } + }, + "lodash": { + "version": "4.17.5", + "bundled": true, + "dev": true + }, + "longest": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "loose-envify": { + "version": "1.3.1", + "bundled": true, + "dev": true, + "requires": { + "js-tokens": "^3.0.0" + } + }, + "lru-cache": { + "version": "4.1.2", + "bundled": true, + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "map-cache": { + "version": "0.2.2", + "bundled": true, + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "object-visit": "^1.0.0" + } + }, + "md5-hex": { + "version": "1.3.0", + "bundled": true, + "dev": true, + "requires": { + "md5-o-matic": "^0.1.1" + } + }, + "md5-o-matic": { + "version": "0.1.1", + "bundled": true, + "dev": true + }, + "mem": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "requires": { + "mimic-fn": "^1.0.0" + } + }, + "merge-source-map": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "requires": { + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "bundled": true, + "dev": true + } + } + }, + "micromatch": { + "version": "2.3.11", + "bundled": true, + "dev": true, + "requires": { + "arr-diff": "^2.0.0", + "array-unique": "^0.2.1", + "braces": "^1.8.2", + "expand-brackets": "^0.1.4", + "extglob": "^0.3.1", + "filename-regex": "^2.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.1", + "kind-of": "^3.0.2", + "normalize-path": "^2.0.1", + "object.omit": "^2.0.0", + "parse-glob": "^3.0.4", + "regex-cache": "^0.4.2" + } + }, + "mimic-fn": { + "version": "1.2.0", + "bundled": true, + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "dev": true + }, + "mixin-deep": { + "version": "1.3.1", + "bundled": true, + "dev": true, + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "nanomatch": { + "version": "1.2.9", + "bundled": true, + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-odd": "^2.0.0", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "bundled": true, + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "bundled": true, + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "bundled": true, + "dev": true + } + } + }, + "normalize-package-data": { + "version": "2.4.0", + "bundled": true, + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "is-builtin-module": "^1.0.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "normalize-path": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + }, + "npm-run-path": { + "version": "2.0.2", + "bundled": true, + "dev": true, + "requires": { + "path-key": "^2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "dev": true + }, + "object-copy": { + "version": "0.1.0", + "bundled": true, + "dev": true, + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "bundled": true, + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "object-visit": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "requires": { + "isobject": "^3.0.0" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "bundled": true, + "dev": true + } + } + }, + "object.omit": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "for-own": "^0.1.4", + "is-extendable": "^0.1.1" + } + }, + "object.pick": { + "version": "1.3.0", + "bundled": true, + "dev": true, + "requires": { + "isobject": "^3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "bundled": true, + "dev": true + } + } + }, + "once": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "optimist": { + "version": "0.6.1", + "bundled": true, + "dev": true, + "requires": { + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "os-locale": { + "version": "2.1.0", + "bundled": true, + "dev": true, + "requires": { + "execa": "^0.7.0", + "lcid": "^1.0.0", + "mem": "^1.1.0" + } + }, + "p-finally": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "p-limit": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "parse-glob": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "requires": { + "glob-base": "^0.3.0", + "is-dotfile": "^1.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.0" + } + }, + "parse-json": { + "version": "2.2.0", + "bundled": true, + "dev": true, + "requires": { + "error-ex": "^1.2.0" + } + }, + "pascalcase": { + "version": "0.1.1", + "bundled": true, + "dev": true + }, + "path-exists": { + "version": "2.1.0", + "bundled": true, + "dev": true, + "requires": { + "pinkie-promise": "^2.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "path-key": { + "version": "2.0.1", + "bundled": true, + "dev": true + }, + "path-parse": { + "version": "1.0.5", + "bundled": true, + "dev": true + }, + "path-type": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "pify": { + "version": "2.3.0", + "bundled": true, + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "bundled": true, + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "pinkie": "^2.0.0" + } + }, + "pkg-dir": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "find-up": "^1.0.0" + }, + "dependencies": { + "find-up": { + "version": "1.1.2", + "bundled": true, + "dev": true, + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + } + } + }, + "posix-character-classes": { + "version": "0.1.1", + "bundled": true, + "dev": true + }, + "preserve": { + "version": "0.2.0", + "bundled": true, + "dev": true + }, + "pseudomap": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "randomatic": { + "version": "1.1.7", + "bundled": true, + "dev": true, + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "bundled": true, + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "bundled": true, + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "read-pkg": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "requires": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "requires": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + }, + "dependencies": { + "find-up": { + "version": "1.1.2", + "bundled": true, + "dev": true, + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + } + } + }, + "regenerator-runtime": { + "version": "0.11.1", + "bundled": true, + "dev": true + }, + "regex-cache": { + "version": "0.4.4", + "bundled": true, + "dev": true, + "requires": { + "is-equal-shallow": "^0.1.3" + } + }, + "regex-not": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "repeat-element": { + "version": "1.1.2", + "bundled": true, + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "bundled": true, + "dev": true + }, + "repeating": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "is-finite": "^1.0.0" + } + }, + "require-directory": { + "version": "2.1.1", + "bundled": true, + "dev": true + }, + "require-main-filename": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "resolve-from": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "resolve-url": { + "version": "0.2.1", + "bundled": true, + "dev": true + }, + "ret": { + "version": "0.1.15", + "bundled": true, + "dev": true + }, + "right-align": { + "version": "0.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "align-text": "^0.1.1" + } + }, + "rimraf": { + "version": "2.6.2", + "bundled": true, + "dev": true, + "requires": { + "glob": "^7.0.5" + } + }, + "safe-regex": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "requires": { + "ret": "~0.1.10" + } + }, + "semver": { + "version": "5.5.0", + "bundled": true, + "dev": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "set-value": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "shebang-command": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true + }, + "slide": { + "version": "1.1.6", + "bundled": true, + "dev": true + }, + "snapdragon": { + "version": "0.8.2", + "bundled": true, + "dev": true, + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "bundled": true, + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "bundled": true, + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "bundled": true, + "dev": true + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "^3.2.0" + } + }, + "source-map": { + "version": "0.5.7", + "bundled": true, + "dev": true + }, + "source-map-resolve": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "requires": { + "atob": "^2.0.0", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-url": { + "version": "0.4.0", + "bundled": true, + "dev": true + }, + "spawn-wrap": { + "version": "1.4.2", + "bundled": true, + "dev": true, + "requires": { + "foreground-child": "^1.5.6", + "mkdirp": "^0.5.0", + "os-homedir": "^1.0.1", + "rimraf": "^2.6.2", + "signal-exit": "^3.0.2", + "which": "^1.3.0" + } + }, + "spdx-correct": { + "version": "3.0.0", + "bundled": true, + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.1.0", + "bundled": true, + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.0", + "bundled": true, + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.0", + "bundled": true, + "dev": true + }, + "split-string": { + "version": "3.1.0", + "bundled": true, + "dev": true, + "requires": { + "extend-shallow": "^3.0.0" + } + }, + "static-extend": { + "version": "0.1.2", + "bundled": true, + "dev": true, + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "bundled": true, + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "string-width": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "bundled": true, + "dev": true + }, + "strip-ansi": { + "version": "4.0.0", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-bom": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "is-utf8": "^0.2.0" + } + }, + "strip-eof": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "supports-color": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "test-exclude": { + "version": "4.2.1", + "bundled": true, + "dev": true, + "requires": { + "arrify": "^1.0.1", + "micromatch": "^3.1.8", + "object-assign": "^4.1.0", + "read-pkg-up": "^1.0.1", + "require-main-filename": "^1.0.1" + }, + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "bundled": true, + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "bundled": true, + "dev": true + }, + "braces": { + "version": "2.3.2", + "bundled": true, + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "expand-brackets": { + "version": "2.1.4", + "bundled": true, + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "bundled": true, + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "bundled": true, + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "bundled": true, + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "bundled": true, + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "kind-of": { + "version": "5.1.0", + "bundled": true, + "dev": true + } + } + }, + "extglob": { + "version": "2.0.4", + "bundled": true, + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "bundled": true, + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "is-number": { + "version": "3.0.0", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "bundled": true, + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "isobject": { + "version": "3.0.1", + "bundled": true, + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "bundled": true, + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "bundled": true, + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + } + } + }, + "to-fast-properties": { + "version": "1.0.3", + "bundled": true, + "dev": true + }, + "to-object-path": { + "version": "0.3.0", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "to-regex": { + "version": "3.0.2", + "bundled": true, + "dev": true, + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + } + } + }, + "trim-right": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "uglify-js": { + "version": "2.8.29", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "source-map": "~0.5.1", + "uglify-to-browserify": "~1.0.0", + "yargs": "~3.10.0" + }, + "dependencies": { + "yargs": { + "version": "3.10.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "camelcase": "^1.0.2", + "cliui": "^2.1.0", + "decamelize": "^1.0.0", + "window-size": "0.1.0" + } + } + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "union-value": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^0.4.3" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "set-value": { + "version": "0.4.3", + "bundled": true, + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.1", + "to-object-path": "^0.3.0" + } + } + } + }, + "unset-value": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "bundled": true, + "dev": true, + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "bundled": true, + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "bundled": true, + "dev": true + }, + "isobject": { + "version": "3.0.1", + "bundled": true, + "dev": true + } + } + }, + "urix": { + "version": "0.1.0", + "bundled": true, + "dev": true + }, + "use": { + "version": "3.1.0", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "^6.0.2" + }, + "dependencies": { + "kind-of": { + "version": "6.0.2", + "bundled": true, + "dev": true + } + } + }, + "validate-npm-package-license": { + "version": "3.0.3", + "bundled": true, + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "which": { + "version": "1.3.0", + "bundled": true, + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "window-size": { + "version": "0.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "wordwrap": { + "version": "0.0.3", + "bundled": true, + "dev": true + }, + "wrap-ansi": { + "version": "2.1.0", + "bundled": true, + "dev": true, + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "write-file-atomic": { + "version": "1.3.4", + "bundled": true, + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "slide": "^1.1.5" + } + }, + "y18n": { + "version": "3.2.1", + "bundled": true, + "dev": true + }, + "yallist": { + "version": "2.1.2", + "bundled": true, + "dev": true + }, + "yargs": { + "version": "11.1.0", + "bundled": true, + "dev": true, + "requires": { + "cliui": "^4.0.0", + "decamelize": "^1.1.1", + "find-up": "^2.1.0", + "get-caller-file": "^1.0.1", + "os-locale": "^2.0.0", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^2.0.0", + "which-module": "^2.0.0", + "y18n": "^3.2.1", + "yargs-parser": "^9.0.2" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "bundled": true, + "dev": true + }, + "camelcase": { + "version": "4.1.0", + "bundled": true, + "dev": true + }, + "cliui": { + "version": "4.0.0", + "bundled": true, + "dev": true, + "requires": { + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0", + "wrap-ansi": "^2.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "yargs-parser": { + "version": "9.0.2", + "bundled": true, + "dev": true, + "requires": { + "camelcase": "^4.1.0" + } + } + } + }, + "yargs-parser": { + "version": "8.1.0", + "bundled": true, + "dev": true, + "requires": { + "camelcase": "^4.1.0" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "bundled": true, + "dev": true + } + } + } + } + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, + "requires": { + "isobject": "^3.0.0" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "object.omit": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", + "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", + "requires": { + "for-own": "^0.1.4", + "is-extendable": "^0.1.1" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "requires": { + "isobject": "^3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + } + } + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz", + "integrity": "sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c=" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "one-time": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-0.0.4.tgz", + "integrity": "sha1-+M33eISCb+Tf+T46nMN7HkSAdC4=", + "dev": true + }, + "onetime": { + "version": "1.1.0", + "resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", + "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=" + }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "dev": true, + "requires": { + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" + }, + "dependencies": { + "wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", + "dev": true + } + } + }, + "optionator": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.4", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "wordwrap": "~1.0.0" + } + }, + "options": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/options/-/options-0.0.6.tgz", + "integrity": "sha1-7CLTEoBrtT5zF3Pnza788cZDEo8=" + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" + }, + "os-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", + "requires": { + "lcid": "^1.0.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" + }, + "output-file-sync": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/output-file-sync/-/output-file-sync-1.1.2.tgz", + "integrity": "sha1-0KM+7+YaIF+suQCS6CZZjVJFznY=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.4", + "mkdirp": "^0.5.1", + "object-assign": "^4.1.0" + } + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "p-limit": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.2.0.tgz", + "integrity": "sha512-Y/OtIaXtUPr4/YpMv1pCL5L5ed0rumAaAeBSj12F+bSlMdys7i8oQF/GUJmfpTS/QoaRrS/k6pma29haJpsMng==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "package-json": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-4.0.1.tgz", + "integrity": "sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=", + "dev": true, + "requires": { + "got": "^6.7.1", + "registry-auth-token": "^3.0.1", + "registry-url": "^3.0.3", + "semver": "^5.1.0" + } + }, + "parse-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", + "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "requires": { + "glob-base": "^0.3.0", + "is-dotfile": "^1.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.0" + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "requires": { + "error-ex": "^1.2.0" + } + }, + "parse5": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz", + "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==" + }, + "parseurl": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", + "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", + "dev": true + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "requires": { + "pinkie-promise": "^2.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=" + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "path-parse": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", + "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "requires": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=" + }, + "pause-stream": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", + "integrity": "sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=", + "dev": true, + "requires": { + "through": "~2.3" + } + }, + "performance-now": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", + "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=" + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "requires": { + "pinkie": "^2.0.0" + } + }, + "pluralize": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-1.2.1.tgz", + "integrity": "sha1-0aIUg/0iu0HlihL6NCGCMUCJfEU=" + }, + "pn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", + "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==" + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "dev": true + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" + }, + "prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", + "dev": true + }, + "preserve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", + "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=" + }, + "private": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", + "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==" + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "progress": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", + "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=" + }, + "promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "requires": { + "asap": "~2.0.3" + } + }, + "propagate": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-1.0.0.tgz", + "integrity": "sha1-AMLa7t2iDofjeCs0Stuhzd1q1wk=", + "dev": true + }, + "proxy-addr": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.3.tgz", + "integrity": "sha512-jQTChiCJteusULxjBp8+jftSQE5Obdl3k4cnmLA6WXtK6XFuWRnvVL7aCiBqaLPM8c4ph0S4tKna8XvmIwEnXQ==", + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.6.0" + } + }, + "ps-tree": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.1.0.tgz", + "integrity": "sha1-tCGyQUDWID8e08dplrRCewjowBQ=", + "dev": true, + "requires": { + "event-stream": "~3.3.0" + } + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "dev": true + }, + "pstree.remy": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.0.tgz", + "integrity": "sha512-q5I5vLRMVtdWa8n/3UEzZX7Lfghzrg9eG2IKk2ENLSofKRCXVqMvMUHxCKgXNaqH/8ebhBxrqftHWnyTFweJ5Q==", + "dev": true, + "requires": { + "ps-tree": "^1.1.0" + } + }, + "pug": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pug/-/pug-2.0.3.tgz", + "integrity": "sha1-ccuoJTfJWl6rftBGluQiH1Oqh44=", + "requires": { + "pug-code-gen": "^2.0.1", + "pug-filters": "^3.1.0", + "pug-lexer": "^4.0.0", + "pug-linker": "^3.0.5", + "pug-load": "^2.0.11", + "pug-parser": "^5.0.0", + "pug-runtime": "^2.0.4", + "pug-strip-comments": "^1.0.3" + } + }, + "pug-attrs": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-2.0.3.tgz", + "integrity": "sha1-owlflw5kFR972tlX7vVftdeQXRU=", + "requires": { + "constantinople": "^3.0.1", + "js-stringify": "^1.0.1", + "pug-runtime": "^2.0.4" + } + }, + "pug-code-gen": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-2.0.1.tgz", + "integrity": "sha1-CVHsgyJddNjPxHan+Zolm199BQw=", + "requires": { + "constantinople": "^3.0.1", + "doctypes": "^1.1.0", + "js-stringify": "^1.0.1", + "pug-attrs": "^2.0.3", + "pug-error": "^1.3.2", + "pug-runtime": "^2.0.4", + "void-elements": "^2.0.1", + "with": "^5.0.0" + } + }, + "pug-error": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-1.3.2.tgz", + "integrity": "sha1-U659nSm7A89WRJOgJhCfVMR/XyY=" + }, + "pug-filters": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-3.1.0.tgz", + "integrity": "sha1-JxZVVbwEwjbkqisDZiRt+gIbYm4=", + "requires": { + "clean-css": "^4.1.11", + "constantinople": "^3.0.1", + "jstransformer": "1.0.0", + "pug-error": "^1.3.2", + "pug-walk": "^1.1.7", + "resolve": "^1.1.6", + "uglify-js": "^2.6.1" + } + }, + "pug-lexer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-4.0.0.tgz", + "integrity": "sha1-IQwYRX7y4XYCQnQMXmR715TOwng=", + "requires": { + "character-parser": "^2.1.1", + "is-expression": "^3.0.0", + "pug-error": "^1.3.2" + } + }, + "pug-linker": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-3.0.5.tgz", + "integrity": "sha1-npp65ABWgtAn3uuWsAD4juuDoC8=", + "requires": { + "pug-error": "^1.3.2", + "pug-walk": "^1.1.7" + } + }, + "pug-load": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/pug-load/-/pug-load-2.0.11.tgz", + "integrity": "sha1-5kjlftET/iwfRdV4WOorrWvAFSc=", + "requires": { + "object-assign": "^4.1.0", + "pug-walk": "^1.1.7" + } + }, + "pug-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-5.0.0.tgz", + "integrity": "sha1-45Stmz/KkxI5QK/4hcBuRKt+aOQ=", + "requires": { + "pug-error": "^1.3.2", + "token-stream": "0.0.1" + } + }, + "pug-runtime": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-2.0.4.tgz", + "integrity": "sha1-4XjhvaaKsujArPybztLFT9iM61g=" + }, + "pug-strip-comments": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-1.0.3.tgz", + "integrity": "sha1-8VWVkiBu3G+FMQ2s9K+0igJa9Z8=", + "requires": { + "pug-error": "^1.3.2" + } + }, + "pug-walk": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-1.1.7.tgz", + "integrity": "sha1-wA1cUSi6xYBr7BXSt+fNq+QlMfM=" + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + }, + "random-words": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/random-words/-/random-words-0.0.1.tgz", + "integrity": "sha1-QOMAkgM62Ptg1mrRW+NiDTwlxB8=" + }, + "randomatic": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", + "integrity": "sha512-D5JUjPyJbaJDkuAazpVnSfVkLlpeO3wDlPROTMLGKG1zMFNFRgrciKo1ltz/AzNTkqE0HzDx655QOL51N06how==", + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" + }, + "raw-body": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", + "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", + "requires": { + "bytes": "3.0.0", + "http-errors": "1.6.2", + "iconv-lite": "0.4.19", + "unpipe": "1.0.0" + }, + "dependencies": { + "depd": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", + "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" + }, + "http-errors": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", + "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", + "requires": { + "depd": "1.1.1", + "inherits": "2.0.3", + "setprototypeof": "1.0.3", + "statuses": ">= 1.3.1 < 2" + } + }, + "setprototypeof": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", + "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" + } + } + }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "requires": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "requires": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + } + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "readdirp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz", + "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "minimatch": "^3.0.2", + "readable-stream": "^2.0.2", + "set-immediate-shim": "^1.0.1" + } + }, + "readline2": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/readline2/-/readline2-1.0.1.tgz", + "integrity": "sha1-QQWWCP/BVHV7cV2ZidGZ/783LjU=", + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "mute-stream": "0.0.5" + } + }, + "rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "requires": { + "resolve": "^1.1.6" + } + }, + "recursive-readdir": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.2.tgz", + "integrity": "sha512-nRCcW9Sj7NuZwa2XvH9co8NPeXUBhZP7CRKJtU+cS6PW9FpCIFoI5ib0NT1ZrbNuPoRy0ylyCaUL8Gih4LSyFg==", + "requires": { + "minimatch": "3.0.4" + } + }, + "redis": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/redis/-/redis-2.6.5.tgz", + "integrity": "sha1-h8Hv9KSJ+Utwhx89CLaYjyOpVoc=", + "requires": { + "double-ended-queue": "^2.1.0-0", + "redis-commands": "^1.2.0", + "redis-parser": "^2.0.0" + } + }, + "redis-commands": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.3.5.tgz", + "integrity": "sha512-foGF8u6MXGFF++1TZVC6icGXuMYPftKXt1FBT2vrfU9ZATNtZJ8duRC5d1lEfE8hyVe3jhelHGB91oB7I6qLsA==" + }, + "redis-parser": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-2.6.0.tgz", + "integrity": "sha1-Uu0J2srBCPGmMcB+m2mUHnoZUEs=" + }, + "reds": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/reds/-/reds-0.2.5.tgz", + "integrity": "sha1-OKdn92Y810kDaEhpfYLHT9KbwB8=", + "optional": true, + "requires": { + "natural": "^0.2.0", + "redis": "^0.12.1" + }, + "dependencies": { + "redis": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/redis/-/redis-0.12.1.tgz", + "integrity": "sha1-ZN92rQ/IrOuuvSoGReikj6xJGF4=", + "optional": true + } + } + }, + "regenerate": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.3.3.tgz", + "integrity": "sha512-jVpo1GadrDAK59t/0jRx5VxYWQEDkkEKi6+HjE3joFVLfDOh9Xrdh0dF1eSq+BI/SwvTQ44gSscJ8N5zYL61sg==" + }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + }, + "regenerator-transform": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.10.1.tgz", + "integrity": "sha512-PJepbvDbuK1xgIgnau7Y90cwaAmO/LCLMI2mPvaXq2heGMR3aWW5/BQvYrhJ8jgmQjXewXvBjzfqKcVOmhjZ6Q==", + "requires": { + "babel-runtime": "^6.18.0", + "babel-types": "^6.19.0", + "private": "^0.1.6" + }, + "dependencies": { + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + } + } + }, + "regex-cache": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", + "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", + "requires": { + "is-equal-shallow": "^0.1.3" + } + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } + }, + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "regexp-clone": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-0.0.1.tgz", + "integrity": "sha1-p8LgmJH9vzj7sQ03b7cwA+aKxYk=" + }, + "regexpu-core": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-2.0.0.tgz", + "integrity": "sha1-SdA4g3uNz4v6W5pCE5k45uoq4kA=", + "requires": { + "regenerate": "^1.2.1", + "regjsgen": "^0.2.0", + "regjsparser": "^0.1.4" + } + }, + "registry-auth-token": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz", + "integrity": "sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==", + "dev": true, + "requires": { + "rc": "^1.1.6", + "safe-buffer": "^5.0.1" + } + }, + "registry-url": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", + "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", + "dev": true, + "requires": { + "rc": "^1.0.1" + } + }, + "regjsgen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", + "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=" + }, + "regjsparser": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", + "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", + "requires": { + "jsesc": "~0.5.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=" + } + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" + }, + "repeat-element": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", + "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=" + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" + }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "requires": { + "is-finite": "^1.0.0" + } + }, + "request": { + "version": "2.81.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", + "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", + "requires": { + "aws-sign2": "~0.6.0", + "aws4": "^1.2.1", + "caseless": "~0.12.0", + "combined-stream": "~1.0.5", + "extend": "~3.0.0", + "forever-agent": "~0.6.1", + "form-data": "~2.1.1", + "har-validator": "~4.2.1", + "hawk": "~3.1.3", + "http-signature": "~1.1.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.7", + "oauth-sign": "~0.8.1", + "performance-now": "^0.2.0", + "qs": "~6.4.0", + "safe-buffer": "^5.0.1", + "stringstream": "~0.0.4", + "tough-cookie": "~2.3.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.0.0" + }, + "dependencies": { + "qs": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", + "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=" + } + } + }, + "request-promise-core": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.1.tgz", + "integrity": "sha1-Pu4AssWqgyOc+wTFcA2jb4HNCLY=", + "requires": { + "lodash": "^4.13.1" + } + }, + "request-promise-native": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.5.tgz", + "integrity": "sha1-UoF3D2jgyXGeUWP9P6tIIhX0/aU=", + "requires": { + "request-promise-core": "1.1.1", + "stealthy-require": "^1.1.0", + "tough-cookie": ">=2.3.3" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=" + }, + "require-uncached": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", + "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", + "requires": { + "caller-path": "^0.1.0", + "resolve-from": "^1.0.0" + } + }, + "require_optional": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", + "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==", + "requires": { + "resolve-from": "^2.0.0", + "semver": "^5.1.0" + }, + "dependencies": { + "resolve-from": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", + "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=" + } + } + }, + "resolve": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.7.1.tgz", + "integrity": "sha512-c7rwLofp8g1U+h1KNyHL/jicrKg1Ek4q+Lr33AL65uZTinUZHe30D5HlyN5V9NW0JX1D5dXQ4jqW5l7Sy/kGfw==", + "requires": { + "path-parse": "^1.0.5" + } + }, + "resolve-from": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", + "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=" + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "dev": true + }, + "restore-cursor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", + "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", + "requires": { + "exit-hook": "^1.0.0", + "onetime": "^1.0.0" + } + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true + }, + "right-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "requires": { + "align-text": "^0.1.1" + } + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "requires": { + "glob": "^7.0.5" + } + }, + "rsa-pem-from-mod-exp": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/rsa-pem-from-mod-exp/-/rsa-pem-from-mod-exp-0.8.4.tgz", + "integrity": "sha1-NipCxtMEBW1JOz8SvOq7LGV2ptQ=" + }, + "run-async": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-0.1.0.tgz", + "integrity": "sha1-yK1KXhEGYeQCp9IbUw4AnyX444k=", + "requires": { + "once": "^1.3.0" + } + }, + "rx-lite": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-3.1.2.tgz", + "integrity": "sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI=" + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, + "requires": { + "ret": "~0.1.10" + } + }, + "sax": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/sax/-/sax-0.5.8.tgz", + "integrity": "sha1-1HLbIo6zMcJQaw6MFVJK25OdEsE=" + }, + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" + }, + "semver-diff": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", + "integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=", + "dev": true, + "requires": { + "semver": "^5.0.3" + } + }, + "send": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", + "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.6.2", + "mime": "1.4.1", + "ms": "2.0.0", + "on-finished": "~2.3.0", + "range-parser": "~1.2.0", + "statuses": "~1.4.0" + }, + "dependencies": { + "mime": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", + "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" + }, + "statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" + } + } + }, + "serve-static": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", + "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.2", + "send": "0.16.2" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", + "dev": true + }, + "set-value": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", + "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + } + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "shelljs": { + "version": "0.7.8", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.7.8.tgz", + "integrity": "sha1-3svPh0sNHl+3LhSxZKloMEjprLM=", + "requires": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + } + }, + "should": { + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/should/-/should-11.2.1.tgz", + "integrity": "sha1-kPVRRVUtAc/CAGZuToGKHJZw7aI=", + "dev": true, + "requires": { + "should-equal": "^1.0.0", + "should-format": "^3.0.2", + "should-type": "^1.4.0", + "should-type-adaptors": "^1.0.1", + "should-util": "^1.0.0" + } + }, + "should-equal": { + "version": "1.0.1", + "resolved": "http://registry.npmjs.org/should-equal/-/should-equal-1.0.1.tgz", + "integrity": "sha1-C26VFvJgGp+wuy3MNpr6HH4gCvc=", + "dev": true, + "requires": { + "should-type": "^1.0.0" + } + }, + "should-format": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/should-format/-/should-format-3.0.3.tgz", + "integrity": "sha1-m/yPdPo5IFxT04w01xcwPidxJPE=", + "dev": true, + "requires": { + "should-type": "^1.3.0", + "should-type-adaptors": "^1.0.1" + } + }, + "should-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz", + "integrity": "sha1-B1bYzoRt/QmEOmlHcZ36DUz/XPM=", + "dev": true + }, + "should-type-adaptors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz", + "integrity": "sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA==", + "dev": true, + "requires": { + "should-type": "^1.3.0", + "should-util": "^1.0.0" + } + }, + "should-util": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/should-util/-/should-util-1.0.0.tgz", + "integrity": "sha1-yYzaN0qmsZDfi6h8mInCtNtiAGM=", + "dev": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "dev": true, + "requires": { + "is-arrayish": "^0.3.1" + }, + "dependencies": { + "is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "dev": true + } + } + }, + "slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=" + }, + "slice-ansi": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", + "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=" + }, + "sliced": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", + "integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E=" + }, + "slug": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/slug/-/slug-0.9.1.tgz", + "integrity": "sha1-rwj2CKfBFRa2F3iqgA3OhMUYz9o=", + "requires": { + "unicode": ">= 0.3.1" + } + }, + "slugify": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.2.9.tgz", + "integrity": "sha512-n0cdJ+kN3slJu8SbZXt/EHjljBqF6MxvMGSg/NPpBzoY7yyXoH38wp/ox20a1JaG1KgmdTN5Lf3aS9+xB2Y2aQ==" + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "requires": { + "kind-of": "^3.2.0" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "sntp": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", + "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", + "requires": { + "hoek": "2.x.x" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "source-map-resolve": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.1.tgz", + "integrity": "sha512-0KW2wvzfxm8NCTb30z0LMNyPqWCdDGE2viwzUaucqJdkTRXtZiSY3I+2A6nVAjmdOy0I4gU8DwnVVGsk9jvP2A==", + "dev": true, + "requires": { + "atob": "^2.0.0", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-support": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", + "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", + "requires": { + "source-map": "^0.5.6" + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "dev": true + }, + "spdx-correct": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz", + "integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==", + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz", + "integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg==" + }, + "spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz", + "integrity": "sha512-2+EPwgbnmOIl8HjGBXXMd9NAu02vLjOO1nWw4kmeRDFyHn+M/ETfHxQUK0oXg8ctgVnl9t3rosNVsZ1jG61nDA==" + }, + "split": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", + "integrity": "sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8=", + "dev": true, + "requires": { + "through": "2" + } + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.0" + }, + "dependencies": { + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } + }, + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "sprintf-js": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.1.tgz", + "integrity": "sha1-Nr54Mgr+WAH2zqPueLblqrlA6gw=" + }, + "sshpk": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.1.tgz", + "integrity": "sha1-Ew9Zde3a2WPx1W+SuaxsUfqfg+s=", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "tweetnacl": "~0.14.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=" + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=" + }, + "stream-combiner": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", + "integrity": "sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ=", + "dev": true, + "requires": { + "duplexer": "~0.1.1" + } + }, + "stream-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stream-length/-/stream-length-1.0.2.tgz", + "integrity": "sha1-gnfzy+5JpNqrz9tOL0qbXp8snwA=", + "requires": { + "bluebird": "^2.6.2" + }, + "dependencies": { + "bluebird": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz", + "integrity": "sha1-U0uQM8AiyVecVro7Plpcqvu2UOE=" + } + } + }, + "string": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/string/-/string-3.3.3.tgz", + "integrity": "sha1-XqIRzZLSKOGEKUmQpsyXs2anfLA=" + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "stringstream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=" + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + }, + "stylus": { + "version": "0.54.5", + "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.54.5.tgz", + "integrity": "sha1-QrlWCTHKcJDOhRWnmLqeaqPW3Hk=", + "requires": { + "css-parse": "1.7.x", + "debug": "*", + "glob": "7.0.x", + "mkdirp": "0.5.x", + "sax": "0.5.x", + "source-map": "0.1.x" + }, + "dependencies": { + "glob": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.6.tgz", + "integrity": "sha1-IRuvr0nlJbjNkyYNFKsTYVKz9Xo=", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.2", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "source-map": { + "version": "0.1.43", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", + "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", + "requires": { + "amdefine": ">=0.0.4" + } + } + } + }, + "superagent": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.2.tgz", + "integrity": "sha512-gVH4QfYHcY3P0f/BZzavLreHW3T1v7hG9B+hpMQotGQqurOvhv87GcMCd6LWySmBuf+BDR44TQd0aISjVHLeNQ==", + "requires": { + "component-emitter": "^1.2.0", + "cookiejar": "^2.1.0", + "debug": "^3.1.0", + "extend": "^3.0.0", + "form-data": "^2.3.1", + "formidable": "^1.1.1", + "methods": "^1.1.1", + "mime": "^1.4.1", + "qs": "^6.5.1", + "readable-stream": "^2.0.5" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "form-data": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", + "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "1.0.6", + "mime-types": "^2.1.12" + } + } + } + }, + "superagent-promise": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/superagent-promise/-/superagent-promise-1.1.0.tgz", + "integrity": "sha1-uvIti73UOamwfdEPjAj1T+JQNTM=" + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + }, + "sylvester": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/sylvester/-/sylvester-0.0.21.tgz", + "integrity": "sha1-KYexzivS84sNzio0OIiEv6RADqc=" + }, + "symbol-tree": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.2.tgz", + "integrity": "sha1-rifbOPZgp64uHDt9G8KQgZuFGeY=" + }, + "table": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/table/-/table-3.8.3.tgz", + "integrity": "sha1-K7xULw/amGGnVdOUf+/Ys/UThV8=", + "requires": { + "ajv": "^4.7.0", + "ajv-keywords": "^1.0.0", + "chalk": "^1.1.1", + "lodash": "^4.0.0", + "slice-ansi": "0.0.4", + "string-width": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "tar-stream": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.5.5.tgz", + "integrity": "sha512-mQdgLPc/Vjfr3VWqWbfxW8yQNiJCbAZ+Gf6GDu1Cy0bdb33ofyiNGBtAY96jHFhDuivCwgW1H9DgTON+INiXgg==", + "requires": { + "bl": "^1.0.0", + "end-of-stream": "^1.0.0", + "readable-stream": "^2.0.0", + "xtend": "^4.0.0" + } + }, + "term-size": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", + "integrity": "sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=", + "dev": true, + "requires": { + "execa": "^0.7.0" + } + }, + "text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "dev": true + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=" + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + }, + "through2": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.5.1.tgz", + "integrity": "sha1-390BLrnHAOIyP9M084rGIqs3Lac=", + "requires": { + "readable-stream": "~1.0.17", + "xtend": "~3.0.0" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + }, + "xtend": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-3.0.0.tgz", + "integrity": "sha1-XM50B7r2Qsunvs2laBEcST9ZZlo=" + } + } + }, + "through2-sink": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/through2-sink/-/through2-sink-1.0.0.tgz", + "integrity": "sha1-XxBruh1zMNrTy6XAqxhjkjJWw5k=", + "requires": { + "through2": "~0.5.1", + "xtend": "~3.0.0" + }, + "dependencies": { + "xtend": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-3.0.0.tgz", + "integrity": "sha1-XM50B7r2Qsunvs2laBEcST9ZZlo=" + } + } + }, + "through2-spy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/through2-spy/-/through2-spy-1.2.0.tgz", + "integrity": "sha1-nIkcqcpA4eHkzzHhrFf5TMnSSMs=", + "requires": { + "through2": "~0.5.1", + "xtend": "~3.0.0" + }, + "dependencies": { + "xtend": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-3.0.0.tgz", + "integrity": "sha1-XM50B7r2Qsunvs2laBEcST9ZZlo=" + } + } + }, + "timed-out": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", + "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", + "dev": true + }, + "tmi.js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/tmi.js/-/tmi.js-1.2.1.tgz", + "integrity": "sha1-a6NFOKAK3IKqbuSBqbNkn/YRGFM=", + "requires": { + "request": "2.74.0", + "ws": "1.0.1" + }, + "dependencies": { + "bl": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.1.2.tgz", + "integrity": "sha1-/cqHGplxOqANGeO7ukHER4emU5g=", + "requires": { + "readable-stream": "~2.0.5" + } + }, + "caseless": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", + "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=" + }, + "form-data": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-1.0.1.tgz", + "integrity": "sha1-rjFduaSQf6BlUCMEpm13M0de43w=", + "requires": { + "async": "^2.0.1", + "combined-stream": "^1.0.5", + "mime-types": "^2.1.11" + } + }, + "har-validator": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz", + "integrity": "sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=", + "requires": { + "chalk": "^1.1.1", + "commander": "^2.9.0", + "is-my-json-valid": "^2.12.4", + "pinkie-promise": "^2.0.0" + } + }, + "node-uuid": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz", + "integrity": "sha1-sEDrCSOWivq/jTL7HxfxFn/auQc=" + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" + }, + "qs": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.2.3.tgz", + "integrity": "sha1-HPyyXBCpsrSDBT/zn138kjOQjP4=" + }, + "readable-stream": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "string_decoder": "~0.10.x", + "util-deprecate": "~1.0.1" + } + }, + "request": { + "version": "2.74.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.74.0.tgz", + "integrity": "sha1-dpPKdou7DqXIzgjAhKRe+gW4kqs=", + "requires": { + "aws-sign2": "~0.6.0", + "aws4": "^1.2.1", + "bl": "~1.1.2", + "caseless": "~0.11.0", + "combined-stream": "~1.0.5", + "extend": "~3.0.0", + "forever-agent": "~0.6.1", + "form-data": "~1.0.0-rc4", + "har-validator": "~2.0.6", + "hawk": "~3.1.3", + "http-signature": "~1.1.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.7", + "node-uuid": "~1.4.7", + "oauth-sign": "~0.8.1", + "qs": "~6.2.0", + "stringstream": "~0.0.4", + "tough-cookie": "~2.3.0", + "tunnel-agent": "~0.4.1" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + }, + "tunnel-agent": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz", + "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=" + } + } + }, + "tmp": { + "version": "0.0.31", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.31.tgz", + "integrity": "sha1-jzirlDjhcxXl29izZX6L+yd65Kc=", + "requires": { + "os-tmpdir": "~1.0.1" + } + }, + "to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=" + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } + }, + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "token-stream": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-0.0.1.tgz", + "integrity": "sha1-zu78cXp2xDFvEm0LnbqlXX598Bo=" + }, + "topo": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/topo/-/topo-1.1.0.tgz", + "integrity": "sha1-6ddRYV0buH3IZdsYL6HKCl71NtU=", + "requires": { + "hoek": "2.x.x" + } + }, + "touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dev": true, + "requires": { + "nopt": "~1.0.10" + }, + "dependencies": { + "nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", + "dev": true, + "requires": { + "abbrev": "1" + } + } + } + }, + "tough-cookie": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", + "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", + "requires": { + "punycode": "^1.4.1" + } + }, + "tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", + "requires": { + "punycode": "^2.1.0" + }, + "dependencies": { + "punycode": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.0.tgz", + "integrity": "sha1-X4Y+3Im5bbCQdLrXlHvwkFbKTn0=" + } + } + }, + "trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=" + }, + "triple-beam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", + "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==", + "dev": true + }, + "tsscmp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.5.tgz", + "integrity": "sha1-fcSjOvcVgatDN9qR2FylQn69mpc=" + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "turndown": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/turndown/-/turndown-4.0.1.tgz", + "integrity": "sha512-xC83XzYm+yLuQWLBc87s63FLn4+ERdZOxDqlrlvKKWcyL9UFhwtR4hAqmFBKDUQyejRZWU9Fac4vMHomlFboyg==", + "requires": { + "jsdom": "^11.3.0" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "optional": true + }, + "twit": { + "version": "2.2.9", + "resolved": "https://registry.npmjs.org/twit/-/twit-2.2.9.tgz", + "integrity": "sha1-ZxBXT4FkHaoDeWobS457eNPXVnY=", + "requires": { + "bluebird": "^3.1.5", + "mime": "^1.3.4", + "request": "^2.68.0" + } + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==" + }, + "type-is": { + "version": "1.6.16", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", + "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.18" + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, + "uc.micro": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.5.tgz", + "integrity": "sha512-JoLI4g5zv5qNyT09f4YAvEZIIV1oOjqnewYg5D38dkQljIzpPT296dbIGvKro3digYI1bkb7W6EP1y4uDlmzLg==", + "dev": true + }, + "uglify-js": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "requires": { + "source-map": "~0.5.1", + "uglify-to-browserify": "~1.0.0", + "yargs": "~3.10.0" + }, + "dependencies": { + "yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "requires": { + "camelcase": "^1.0.2", + "cliui": "^2.1.0", + "decamelize": "^1.0.0", + "window-size": "0.1.0" + } + } + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", + "optional": true + }, + "ultron": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.0.2.tgz", + "integrity": "sha1-rOEWq1V80Zc4ak6I9GhTeMiy5Po=" + }, + "undefsafe": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.2.tgz", + "integrity": "sha1-Il9rngM3Zj4Njnz9aG/Cg2zKznY=", + "dev": true, + "requires": { + "debug": "^2.2.0" + } + }, + "underscore": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.0.tgz", + "integrity": "sha512-4IV1DSSxC1QK48j9ONFK1MoIAKKkbE8i7u55w2R6IqBqbT7A/iG7aZBCR2Bi8piF0Uz+i/MG1aeqLwl/5vqF+A==", + "optional": true + }, + "unicode": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/unicode/-/unicode-10.0.0.tgz", + "integrity": "sha1-5dUcHbk7bHGguHngsMSvfm/faI4=" + }, + "union-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", + "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^0.4.3" + }, + "dependencies": { + "set-value": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", + "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.1", + "to-object-path": "^0.3.0" + } + } + } + }, + "unique-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", + "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=", + "dev": true, + "requires": { + "crypto-random-string": "^1.0.0" + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "unzip-response": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", + "integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=", + "dev": true + }, + "upath": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.0.tgz", + "integrity": "sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw==", + "dev": true + }, + "update-notifier": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-2.5.0.tgz", + "integrity": "sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==", + "dev": true, + "requires": { + "boxen": "^1.2.1", + "chalk": "^2.0.1", + "configstore": "^3.0.0", + "import-lazy": "^2.1.0", + "is-ci": "^1.0.10", + "is-installed-globally": "^0.1.0", + "is-npm": "^1.0.0", + "latest-version": "^3.0.0", + "semver-diff": "^2.0.0", + "xdg-basedir": "^3.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "dev": true + }, + "url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" + } + } + }, + "url-join": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-1.1.0.tgz", + "integrity": "sha1-dBxsL0WWxIMNZxhGCSDQySIC3Hg=" + }, + "url-parse-lax": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", + "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", + "dev": true, + "requires": { + "prepend-http": "^1.0.1" + } + }, + "use": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.0.tgz", + "integrity": "sha512-6UJEQM/L+mzC3ZJNM56Q4DFGLX/evKGRg15UJHGB9X5j5Z3AFbgZvjUh2yq/UJUY4U5dh7Fal++XbNg1uzpRAw==", + "dev": true, + "requires": { + "kind-of": "^6.0.2" + }, + "dependencies": { + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "user-home": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz", + "integrity": "sha1-nHC/2Babwdy/SGBODwS4tJzenp8=", + "requires": { + "os-homedir": "^1.0.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "uuid": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", + "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==" + }, + "v8flags": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-2.1.1.tgz", + "integrity": "sha1-qrGh+jDUX4jdMhFIh1rALAtV5bQ=", + "dev": true, + "requires": { + "user-home": "^1.1.1" + }, + "dependencies": { + "user-home": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/user-home/-/user-home-1.1.1.tgz", + "integrity": "sha1-K1viOjK2Onyd640PKNSFcko98ZA=", + "dev": true + } + } + }, + "validate-npm-package-license": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz", + "integrity": "sha512-63ZOUnL4SIXj4L0NixR3L1lcjO38crAbgrTpl28t8jjrfuiOBL5Iygm+60qPs/KsZGzPNg6Smnc/oY16QTjF0g==", + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "validator": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/validator/-/validator-9.4.1.tgz", + "integrity": "sha512-YV5KjzvRmSyJ1ee/Dm5UED0G+1L4GZnLN3w6/T+zZm8scVua4sOhYKWTUrKa0H/tMiJyO9QLHMPN+9mB/aMunA==" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=" + }, + "w3c-hr-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz", + "integrity": "sha1-gqwr/2PZUOqeMYmlimViX+3xkEU=", + "requires": { + "browser-process-hrtime": "^0.1.2" + } + }, + "walkdir": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/walkdir/-/walkdir-0.0.11.tgz", + "integrity": "sha1-oW0CXrkxvQO1LzCMrtD0D86+lTI=" + }, + "webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==" + }, + "whatwg-encoding": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.3.tgz", + "integrity": "sha512-jLBwwKUhi8WtBfsMQlL4bUUcT8sMkAtQinscJAe/M4KHCkHuUJAF6vuB0tueNIw4c8ziO6AkRmgY+jL3a0iiPw==", + "requires": { + "iconv-lite": "0.4.19" + } + }, + "whatwg-mimetype": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.1.0.tgz", + "integrity": "sha512-FKxhYLytBQiUKjkYteN71fAUA3g6KpNXoho1isLiLSB3N1G4F35Q5vUxWfKFhBwi5IWF27VE6WxhrnnC+m0Mew==" + }, + "whatwg-url": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.4.0.tgz", + "integrity": "sha512-Z0CVh/YE217Foyb488eo+iBv+r7eAQ0wSTyApi9n06jhcA3z6Nidg/EGvl0UFkg7kMdKxfBzzr+o9JF+cevgMg==", + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.0", + "webidl-conversions": "^4.0.1" + } + }, + "when": { + "version": "3.7.8", + "resolved": "https://registry.npmjs.org/when/-/when-3.7.8.tgz", + "integrity": "sha1-xxMLan6gRpPoQs3J56Hyqjmjn4I=" + }, + "which": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", + "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", + "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=" + }, + "widest-line": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.0.tgz", + "integrity": "sha1-AUKk6KJD+IgsAjOqDgKBqnYVInM=", + "dev": true, + "requires": { + "string-width": "^2.1.1" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=" + }, + "winston": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/winston/-/winston-2.4.3.tgz", + "integrity": "sha512-GYKuysPz2pxYAVJD2NPsDLP5Z79SDEzPm9/j4tCjkF/n89iBNGBMJcR+dMUqxgPNgoSs6fVygPi+Vl2oxIpBuw==", + "requires": { + "async": "~1.0.0", + "colors": "1.0.x", + "cycle": "1.0.x", + "eyes": "0.1.x", + "isstream": "0.1.x", + "stack-trace": "0.0.x" + }, + "dependencies": { + "async": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async/-/async-1.0.0.tgz", + "integrity": "sha1-+PwEyjoTeErenhZBr5hXjPvWR6k=" + } + } + }, + "winston-transport": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.3.0.tgz", + "integrity": "sha512-B2wPuwUi3vhzn/51Uukcao4dIduEiPOcOt9HJ3QeaXgkJ5Z7UwpBzxS4ZGNHtrxrUvTwemsQiSys0ihOf8Mp1A==", + "dev": true, + "requires": { + "readable-stream": "^2.3.6", + "triple-beam": "^1.2.0" + } + }, + "with": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/with/-/with-5.1.1.tgz", + "integrity": "sha1-+k2qktrzLE6pTtRTyB8EaGtXXf4=", + "requires": { + "acorn": "^3.1.0", + "acorn-globals": "^3.0.0" + }, + "dependencies": { + "acorn": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", + "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=" + } + } + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "write": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", + "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", + "requires": { + "mkdirp": "^0.5.1" + } + }, + "write-file-atomic": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.3.0.tgz", + "integrity": "sha512-xuPeK4OdjWqtfi59ylvVL0Yn35SF3zgcAcv7rBPFHVaEapaDr4GdGgm3j7ckTwH9wHL7fGmgfAnb0+THrHb8tA==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + }, + "ws": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-1.0.1.tgz", + "integrity": "sha1-fQsqLljN3YGQOcKcneZQReGzEOk=", + "requires": { + "options": ">=0.0.5", + "ultron": "1.0.x" + } + }, + "xdg-basedir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", + "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=", + "dev": true + }, + "xml-name-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", + "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==" + }, + "xml2js": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.17.tgz", + "integrity": "sha1-F76T6q4/O3eTWceVtBlwWogX6Gg=", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "^4.1.0" + }, + "dependencies": { + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + } + } + }, + "xmlbuilder": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-4.2.1.tgz", + "integrity": "sha1-qlijBBoGb5DqoWwvU4n/GfP0YaU=", + "requires": { + "lodash": "^4.0.0" + } + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" + }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + }, + "yargs": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-4.8.1.tgz", + "integrity": "sha1-wMQpJMpKqmsObaFznfshZDn53cA=", + "requires": { + "cliui": "^3.2.0", + "decamelize": "^1.1.1", + "get-caller-file": "^1.0.1", + "lodash.assign": "^4.0.3", + "os-locale": "^1.4.0", + "read-pkg-up": "^1.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^1.0.1", + "which-module": "^1.0.0", + "window-size": "^0.2.0", + "y18n": "^3.2.1", + "yargs-parser": "^2.4.1" + }, + "dependencies": { + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wrap-ansi": "^2.0.0" + } + }, + "window-size": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.2.0.tgz", + "integrity": "sha1-tDFbtCFKPXBY6+7okuE/ok2YsHU=" + } + } + }, + "yargs-parser": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-2.4.1.tgz", + "integrity": "sha1-hVaN488VD/SfpRgl8DqMiA3cxcQ=", + "requires": { + "camelcase": "^3.0.0", + "lodash.assign": "^4.0.6" + }, + "dependencies": { + "camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=" + } + } + }, + "zip-stream": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-1.2.0.tgz", + "integrity": "sha1-qLxF9MG0lpnGuQGYuqyqzbzUugQ=", + "requires": { + "archiver-utils": "^1.3.0", + "compress-commons": "^1.2.0", + "lodash": "^4.8.0", + "readable-stream": "^2.0.0" + } + } + } +} diff --git a/package.json b/package.json index fbf2f9e..eb1173b 100644 --- a/package.json +++ b/package.json @@ -1,95 +1,162 @@ { - "name": "connector", - "version": "0.0.0", - "description": "Recast.AI Bot Connector", - "main": "src/index.js", + "name": "sap-botconnector", + "version": "1.0.0", + "main": "index.js", "scripts": { - "lint": "eslint src", - "start": "npm run build && cross-env NODE_ENV=production node dist/index.js", - "start-dev": "cross-env NODE_ENV=development nodemon src/index.js --exec babel-node", - "test": "cross-env NODE_ENV=test babel-node src/test.js", + "docs": "./node_modules/.bin/apidoc -i src -o docs", + "lint": "./node_modules/eslint/bin/eslint.js src", + "start": "npm run build && NODE_ENV=production node dist/index.js", + "start:dev": "NODE_ENV=development nodemon src/index.js --exec babel-node", + "start:dev:debug": "NODE_ENV=development nodemon src/index.js --inspect --exec babel-node", + "test": "NODE_ENV=test node ./node_modules/mocha/bin/mocha --recursive --require babel-core/register --exit --timeout 2500 'src/channel_integrations/**/test/**/*.js' 'test/**/*.js'", + "test:debug": "NODE_ENV=test node ./node_modules/mocha/bin/mocha --inspect-brk --recursive --require babel-core/register --exit 'src/channel_integrations/**/test/**/*.js' 'test/**/*.js'", + "test:coverage": "NODE_ENV=test ./node_modules/.bin/nyc ./node_modules/.bin/mocha --recursive --exit --timeout 10000 'src/channel_integrations/**/test/**/*.js' 'test/**/*.js'", "build": "babel src --out-dir dist", - "deploy": "./deploy.sh", - "doc": "apidoc -i src/ -o doc/" + "build:production": "NODE_ENV=production babel src --out-dir dist" }, - "repository": { - "type": "git", - "url": "git@git.recast.ai:recast/connector.git" + "license": "MIT", + "apidoc": { + "name": "SAP Bot Connector API Documentation", + "version": "1.0.0", + "title": "SAP Bot Connector API Documentation", + "description": "" }, - "author": "Recast (https://recast.ai)", - "license": "ISC", "eslintConfig": { "extends": [ "zavatta" ], + "env": { + "mocha": true + }, + "globals": { + "models": false, + "controllers": false, + "services": false, + "config": false, + "Map": false, + "Promise": false + }, "rules": { + "indent": [ + "error", + 2 + ], + "max-len": [ + "warn", + { + "code": 100 + } + ], + "no-case-declarations": 0, "no-sync": 0, - "no-undef": 0, - "id-length": 0, + "prefer-spread": 0, "camelcase": 0, + "id-length": 0, "no-inline-comments": 0, - "max-nested-callbacks": 0 + "guard-for-in": 0, + "no-useless-call": 0, + "require-await": 0, + "no-undefined": 0 } }, "babel": { "presets": [ - "es2015", - "stage-0" + "stage-0", + "env" ], + "env": { + "test": { + "plugins": [ + "istanbul" + ] + } + }, "plugins": [ - [ - "coverage", - { - "only": "src/*/" - } - ], + "transform-class-properties", "transform-runtime" ] }, - "apidoc": { - "name": "Bot Connector API Documentation", - "version": "0.0.0", - "title": "Bot Connector API Documentation", - "description": "" + "nyc": { + "reporter": [ + "html", + "text-summary" + ], + "require": [ + "babel-register" + ], + "sourceMap": false, + "instrument": false }, "dependencies": { - "@slack/client": "^3.6.0", - "babel-plugin-transform-runtime": "^6.15.0", + "alexa-verifier": "^1.0.0", + "archiver": "^1.3.0", + "ask-sdk": "^2.0.6", + "babel-core": "^6.23.1", + "babel-plugin-transform-class-properties": "^6.24.1", + "babel-preset-env": "^1.6.0", + "babel-preset-stage-0": "^6.24.1", + "bluebird": "^3.5.1", "blueimp-md5": "^2.5.0", - "body-parser": "^1.15.2", + "body-parser": "^1.18.2", + "botbuilder": "3.12.0", "callr": "^1.0.0", - "chai-spies": "^0.7.1", - "cors": "^2.8.1", - "cross-env": "3.1.3", + "chai": "^4.1.2", + "chai-http": "^4.0.0", + "content-type": "^1.0.2", "crypto": "^0.0.3", - "eslint": "^3.7.1", + "eslint": "^3.10.2", "eslint-config-zavatta": "^4.2.0", - "express": "^4.14.0", + "express": "^4.16.2", + "fbgraph": "^1.4.1", + "file-type": "^4.3.0", "filter-object": "^2.1.0", + "http": "^0.0.0", + "intercom-client": "^2.9.1", "is_js": "^0.9.0", - "istanbul": "^0.4.5", - "lodash": "^4.16.4", - "mocha": "^3.1.2", - "mongoose": "^4.6.3", + "jsonschema": "^1.1.1", + "jsonwebtoken": "^8.1.0", + "kue": "^0.11.6", + "lodash": "^4.17.2", + "login-with-amazon": "^0.0.3", + "md-recast": "^0.0.2", + "mocha": "^5.1.0", + "moment": "^2.18.1", + "mongoose": "^4.6.8", + "morgan": "^1.7.0", + "node-sparky": "^4.0.7", + "nodemailer": "^4.4.0", + "random-words": "^0.0.1", + "raw-body": "^2.2.0", "recursive-readdir": "^2.1.0", - "sinon": "^1.17.6", - "superagent": "^2.3.0", + "request": "2.81.0", + "slug": "^0.9.1", + "slugify": "^1.1.0", + "superagent": "^3.0.0", "superagent-promise": "^1.1.0", + "tmi.js": "^1.2.1", + "tmp": "^0.0.31", "tsscmp": "^1.0.5", - "uuid": "^3.0.1" + "turndown": "^4.0.1", + "twit": "^2.2.5", + "uuid": "^3.0.1", + "winston": "^2.4.2" }, "devDependencies": { - "apidoc": "^0.16.1", - "babel-cli": "^6.16.0", - "babel-eslint": "^7.0.0", - "babel-plugin-coverage": "^1.0.0", - "babel-preset-es2015": "^6.16.0", - "babel-preset-stage-0": "^6.16.0", - "chai": "^3.5.0", - "chai-http": "^3.0.0", - "mocha": "^3.1.1", - "mock-require": "^1.3.0", - "nock": "^8.1.0", - "nodemon": "^1.11.0" + "apidoc": "0.17.7", + "babel-cli": "6.18.0", + "babel-eslint": "^7.1.1", + "babel-generator": "6.19.0", + "babel-plugin-istanbul": "^4.1.6", + "babel-plugin-transform-runtime": "^6.15.0", + "babel-runtime": "^6.18.0", + "babel-traverse": "6.19.0", + "babel-types": "6.19.0", + "expect.js": "^0.3.1", + "istanbul": "^0.4.5", + "nock": "^9.0.6", + "nodemon": "^1.17.5", + "nyc": "^11.7.1", + "qs": "^6.5.2", + "should": "^11.2.0" } } diff --git a/resources/kik/kik.png b/resources/kik/kik.png deleted file mode 100644 index 6f3f027..0000000 Binary files a/resources/kik/kik.png and /dev/null differ diff --git a/resources/messenger/messenger.jpg b/resources/messenger/messenger.jpg deleted file mode 100644 index 5b16d35..0000000 Binary files a/resources/messenger/messenger.jpg and /dev/null differ diff --git a/resources/slack/slack.jpg b/resources/slack/slack.jpg deleted file mode 100644 index 9ace36b..0000000 Binary files a/resources/slack/slack.jpg and /dev/null differ diff --git a/src/app.js b/src/app.js new file mode 100644 index 0000000..7988e65 --- /dev/null +++ b/src/app.js @@ -0,0 +1,92 @@ +import express from 'express' +import contentType from 'content-type' +import bodyParser from 'body-parser' +import morgan from 'morgan' +import rawBody from 'raw-body' +import { logger } from './utils' +import mongoose from 'mongoose' +import { createRouter } from './routes' +import config from '../config' + +function createApplication () { + const app = express() + + // Enable Cross Origin Resource Sharing + app.use((req, res, next) => { + res.header('Access-Control-Allow-Origin', '*') + res.header('Access-Control-Allow-Headers', '*, Content-Type, Authorization') + res.header('Access-Control-Allow-Methods', 'GET, DELETE, POST, PUT, OPTIONS') + res.header('Access-Control-Max-Age', '86400') + res.header('Cache-Control', 'private, no-cache, no-store, must-revalidate') + res.header('Expires', '-1') + res.header('Pragma', 'no-cache') + next() + }) + + // stock a buffer containing the raw body in req.rawBody + app.use(async (req, res, next) => { + next() // eslint-disable-line callback-return + + if (req.method !== 'POST') { return } + + try { + let encoding = 'utf-8' + try { + encoding = contentType.parse(req).parameters.charset + } catch (e) {} // eslint-disable-line + req.rawBody = await rawBody(req, { + length: req.headers['content-length'], + limit: '1mb', + encoding, + }) + } catch (err) { + logger.error(`Error while getting raw body from request: ${err}`) + } + }) + + // Enable auto parsing of json content + app.use(bodyParser.json()) + app.use(bodyParser.urlencoded({ extended: true })) + + // Enable middleware logging + app.use(morgan('dev')) + return app +} + +function connectMongoDB () { + return new Promise((resolve, reject) => { + logger.info('Connecting to MongoDB') + // Use native Promise with mongoose + mongoose.Promise = global.Promise + + let dbUrl = 'mongodb://' + if (config.db.username) { + dbUrl = `${dbUrl}${config.db.username}:${config.db.password}@` + } + const path = `${config.db.dbName}?ssl=${config.db.ssl || 'false'}` + dbUrl = `${dbUrl}${config.db.host}:${config.db.port}/${path}` + mongoose.connect(dbUrl) + const db = mongoose.connection + db.on('error', reject) + db.once('open', () => { + resolve(db) + }) + }) +} + +function startExpressApp () { + return new Promise((resolve) => { + logger.info('Starting App') + const app = createApplication() + createRouter(app) + app.listen(config.server.port, () => { + app.emit('ready') + resolve(app) + }) + }) +} + +export async function startApplication () { + await connectMongoDB() + return startExpressApp() +} diff --git a/src/channel_integrations/abstract_channel_integration.js b/src/channel_integrations/abstract_channel_integration.js new file mode 100644 index 0000000..4c034b7 --- /dev/null +++ b/src/channel_integrations/abstract_channel_integration.js @@ -0,0 +1,337 @@ +import { noop } from '../utils' +import config from '../../config' + +export class NotImplementedError extends Error { + constructor (method) { + super(`Implement ${method} in class AbstractChannelIntegration`) + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, NotImplementedError) + } + + this.method = method + } +} + +/* eslint no-unused-vars: 0 */ // --> OFF for abstract methods + +/** + * Abstract base class for custom channel integrations. + */ +export class AbstractChannelIntegration { + + /** + * Can be overwritten by subclasses to customize the HTTP response that + * is sent for a request to the channel's webhook. + * @param {http.IncomingMessage} req HTTP request + * @param {http.ServerResponse} res HTTP response + * @param {Object} context Context from {@link populateMessageContext} + * @param {Object[]} botResponse The bot's response to the request's input message + */ + finalizeWebhookRequest (req, res, context, botResponse) { + res.status(200).json({ results: null, message: 'Message successfully received' }) + } + + /** + * Validates properties of the channel model created or updated using the JSON request body + * of the respective HTTP endpoint. + * @param {Channel} channel An instance of the channel model + * @throws {BadRequestError} Validation of channel object failed + */ + validateChannelObject (channel) { + // NOOP by default + } + + /** + * Gets called before the channel model gets persisted into the database. + * @param {Channel} channel An instance of the channel model + * @param {http.IncomingMessage} req HTTP request + * @return {Promise} Promise which resolves when all operations executed successfully + */ + beforeChannelCreated (channel, req) { return noop() } + + /** + * Is called after the channel model is persisted in the database. + * @param {Channel} channel An instance of the channel model + * @param {http.IncomingMessage} req HTTP request + * @return {Promise} Promise which resolves when all operations executed have been executed + */ + afterChannelCreated (channel, req) { return noop() } + + /** + * Gets called after the channel model was updated in the database. + * @param {Channel} channel The updated channel model + * @param {Channel} oldChannel The old channel model + * @return {Promise} Promise which resolves when all operations executed successfully + */ + afterChannelUpdated (channel, oldChannel) { return noop() } + + /** + * Gets called before the channel model get deleted in the database. + * @param {Channel} channel The deleted channel model + * @throws {ForbiddenError} Authentication failed + * @return {Promise} Promise which resolves when all operations executed successfully + */ + beforeChannelDeleted (channel) { return noop() } + + /** + * Gets called after the channel model was deleted in the database. + * @param {Channel} channel The deleted channel model + * @return {Promise} Promise which resolves when all operations executed successfully + */ + afterChannelDeleted (channel) { return noop() } + + /** + * Defines the HTTP methods that a caller can use to send messages to a channel's webhook. + * @returns {string[]} List of HTTP methods + */ + webhookHttpMethods () { + return ['POST'] + } + + /** + * Generates a URL for the channel's webhook. This is called during the creation of + * a new channel. Only override this method if you need to change your channel's webhook URL + * for a particular reason. + * @param channel An instance of the channel model before being persisted + * @return {string} A well-formed URL + */ + buildWebhookUrl (channel) { + return `${config.base_url}/v1/webhook/${channel._id}` + } + + /** + * Validates parameters of the request object for webhook subscriptions. + * @param {http.IncomingMessage} req HTTP request + * @param {http.ServerResponse} res HTTP response + * @param {Channel} channel An instance of the channel model + * @return {Promise} Promise which resolves when parameter validation succeeds + */ + validateWebhookSubscriptionRequest (req, res, channel) { + throw new NotImplementedError('validateWebhookSubscriptionRequest') + } + + /** + * Authenticates an HTTP request made against the channel's webhook. + * @param {http.IncomingMessage} req HTTP request + * @param {http.ServerResponse} res HTTP response + * @param {Channel} channel An instance of the channel model + * @throws {ForbiddenError} Authentication failed + * @return {Promise} Promise which resolves when request authentication succeeds + */ + authenticateWebhookRequest (req, res, channel) { return noop() } + + /** + * Gets called before an HTTP request to the channel's webhook is processed. + * @param {http.IncomingMessage} req HTTP request + * @param {http.ServerResponse} res HTTP response + * @param {Channel} channel An instance of the channel model + * @return {Promise} Promise which resolves when all operations succeed + */ + onWebhookCalled (req, res, channel) { + return channel + } + + /** + * Retrieves context information from the HTTP request, such as chatId, senderId, + * and channel-specific data. + * @param {http.IncomingMessage} req HTTP request + * @param {http.ServerResponse} res HTTP response + * @param {Channel} channel An instance of the channel model + * @return {Object} The context object for further message processing + */ + populateMessageContext (req, res, channel) { + throw new NotImplementedError('populateMessageContext') + } + + /** + * Reads the unparsed message from a request sent to the channel's webhook. + * @param {Channel} channel An instance of the channel model + * @param {http.IncomingMessage} req HTTP request + * @param {Object} context Context from {@link populateMessageContext} + * @return {Object} The raw message + */ + getRawMessage (channel, req, context) { + return req.body + } + + /** + * Gets called when the bot is ready to indicate that it is "typing". This hook can be + * used to activate "isTyping" indicators in any external services. + * @see populateMessageContext + * @param {Channel} channel An instance of the channel model + * @param {Object} context Context from {@link populateMessageContext} + * @return {Promise} Promise which resolves when "isTyping" request was successful + */ + onIsTyping (channel, context) { return noop() } + + /** + * Can be implemented to update the current conersation based on a received message. + * @param {Conversation} conversation The conversation to be updated + * @param {Message} message The received message + * @return {Conversation} The conversation with update properties + */ + updateConversationContextFromMessage (conversation, message) { + return conversation + } + + /** + * Parses an HTTP request sent to the channel's webhook and extracts a message object. + * The returned message object needs to be in the bot connector's format. + * @param {Conversation} conversation The message's conversation + * @param {Object} message The raw message returned from {@link getRawMessage} + * @param {Object} context Context from {@link populateMessageContext} + * @return {Object} The well-formatted message object + */ + parseIncomingMessage (conversation, message, context) { return noop() } + + /** + * Parses an HTTP request sent to the channel's webhook and extracts a memory object. + * The memory will be sent to the bot. + * @param {Object} message The raw message returned from {@link getRawMessage} + * @param {Object} context Context from {@link populateMessageContext} + * @return {Object} The well-formatted message object + */ + getMemoryOptions (message, context) { return { memory: {}, merge: true } } + + /** + * Transforms a bot's response into a format that can be understood by the channel' API. + * @param {Conversation} conversation The message's conversation + * @param {Object} message Message in bot builder's format + * @param {Object} context Context from {@link populateMessageContext} + * @return {Object} Message in channel API's format + */ + formatOutgoingMessage (conversation, message, context) { return message } + + /** + * Format Markdown the way it's supported by the external service + */ + formatMarkdown (message) { + const modifyTextLink = (result) => result.replace(/\[|\]/g, '') + const modifyEmptyLink = (result) => result.replace(/\(|\)|\[|\]/g, '') + const fixSingleStars = (result) => result.replace(/^\*|\*$/gm, '\'') + const fixSingleUnderscore = (result) => result.replace(/^_|_$/gm, '\'') + if (message.attachment.type === 'text') { + message.attachment.content = message.attachment.content + .replace(/_(.*?)_/gm, fixSingleUnderscore) + .replace(/\*(.*?)\*/gm, fixSingleStars) + .replace(/\[.+\]\(.*\)/gm, modifyTextLink) + .replace(/\[\]\(.*\)/g, modifyEmptyLink) + } + if (message.attachment.type === 'quickReplies') { + message.attachment.content.title = message.attachment.content.title + .replace(/_(.*?)_/gm, fixSingleUnderscore) + .replace(/\*(.*?)\*/gm, fixSingleStars) + .replace(/\[.+\]\(.*\)/gm, modifyTextLink) + .replace(/\[\]\(.*\)/g, modifyEmptyLink) + } + return message + } + + /** + * Sends response message to the channel. + * @param {Conversation} conversation The message's conversation + * @param {Object} message The pre-formatted message returned from {@link parseIncomingMessage} + * @param {Object} context Context from {@link populateMessageContext} + * @return {Promise} Promise which resolves when message was sent successfully + */ + sendMessage (conversation, message, context) { return noop() } + + /** + * Parses a participant's display name, which can be either a phone number or a user name. + * @example + * parseParticipantDisplayName (participant) { + * const externalParticipant = participant.data + * const displayName = `${externalParticipant.first} ${externalParticipant.last}` + * return { userName: displayName } + * } + * @example + * parseParticipantDisplayName (participant) { + * const externalParticipant = participant.data + * return { phoneNumber: externalParticipant.mobile } + * } + * @param {Participant} participant Complete Participant object + * saved in {@link populateParticipantData} + * @return {Object} An object containing either a `phoneNumber` or a `userName` field + */ + parseParticipantDisplayName (participant) { + return ({}) + } + + /** + * Downloads additional information about a participant and adds it to the database model. + * The downloaded information can be an arbitrary object and is expected to be saved + * as the `participant.data` property. + * @example + * const externalData = downloadParticipantFromAPI() + * participant.data = externalData + * participant.markModified('data') + * // Persist model and return Promise + * return participant.save() + * @param {Participant} participant Participant model to be used for persisting the data + * @param {Channel} channel An instance of the channel model + * @return {Promise} Promise which resolves if data has been downloaded and saved successfully + */ + populateParticipantData (participant, channel) { + return participant + } + + /* Call to send a message array + to a bot. If this method returns + true, `sendMessage` won't be called */ + sendMessages () { + return false + } + + /* Check shared webhook validity for certain channels (Messenger) */ + onSharedWebhookChecking () { + throw new NotImplementedError('onSharedWebhookChecking') + } + + /* Get unique model field / value pair to give a means to + identify a channel when a message is received by + a shared webhook endpoint */ + getIdPairsFromSharedWebhook () { + throw new NotImplementedError('getIdPairsFromSharedWebhook') + } + + /** + * set a get started button to the appropriate channel + * @param {Channel} an instance of the channel model + * @param {Value} the get started button's value + * @returns {Promise} a promise that resolves when the button is set + */ + setGetStartedButton (channel, value, connector) { + return noop() + } + + /** + * remove a get started button from a channel + * @param {Channel} an instance of the channel model + * @returns {Promise} a promise that resolves when the button is successfully removed + */ + deleteGetStartedButton (channel) { + return noop() + } + + /** + * Set a persistent menu to a specific channel + * @param {Channel} An instance of the Channel model + * @param {Menus} array of PersistentMenu instances + * @returns {Promise} a promise that resolves when the persistent menu is successfully set + */ + setPersistentMenu (channel, menus) { + return noop() + } + + /** + * Remove a peristent menu form a specific channel + * @param {Channel} An instance of the Channel model + * @returns {Promise} + */ + deletePersistentMenu (channel) { + return noop() + } +} + +export default AbstractChannelIntegration diff --git a/src/channel_integrations/amazon_alexa/channel.js b/src/channel_integrations/amazon_alexa/channel.js new file mode 100644 index 0000000..eab972e --- /dev/null +++ b/src/channel_integrations/amazon_alexa/channel.js @@ -0,0 +1,368 @@ +import lwa from 'login-with-amazon' +import _ from 'lodash' +import { SkillBuilders } from 'ask-sdk' + +import config from '../../../config' +import { logger, AppError, BadRequestError } from '../../utils' +import AbstractChannelIntegration from '../abstract_channel_integration' +import AlexaSMAPI from './sdk.js' + +/* + * getWebhookUrl: default + */ +export default class AmazonAlexa extends AbstractChannelIntegration { + + static REQUEST_LAUNCH = 'LaunchRequest' + static SESSION_ENDED_REQUEST = 'SessionEndedRequest' + static INTENT_REQUEST = 'IntentRequest' + static CATCH_ALL_INTENT = 'CATCH_ALL_INTENT' + static CATCH_ALL_SLOT = 'CATCH_ALL_SLOT' + + // Special Alexa Intent types + static CONVERSATION_START = 'CONVERSATION_START' + static CONVERSATION_END = 'CONVERSATION_END' + + // Special Memory flag to end conversation + static END_CONVERSATION = 'END_CONVERSATION' + + static supportedLocales = AlexaSMAPI.locales + + // [START] Inherited from AbstractChannelIntegration + + async beforeChannelCreated (channel) { + // Exchange the OAuth Code for Access Token and Refresh Token + const { access_token, refresh_token } = await lwa.getAccessTokens( + channel.oAuthCode, config.amazon_client_id, config.amazon_client_secret) + channel.oAuthTokens = { access_token, refresh_token } + await channel.save() + } + + async beforeChannelDeleted (channel) { + if (!channel.skillId) { + // Don't do anything if it has not been deployed to Amazon Alexa + return + } + + try { + await this.smapiCallWithAutoTokenRefresh(channel, 'deleteSkill', channel.skillId) + } catch (sdkError) { + const { message, status } = sdkError + if (status === 404) { + // If the Skill has been deleted in the Amazon Alexa Console, ignore the error + return + } + throw new AppError(message, null, status) + } + } + + async parseIncomingMessage (conversation, message, opts) { + try { + // Workaround for passing on the Response Builder since the ASK SDK wraps it into an object + // eslint-disable-next-line max-len + // https://github.com/alexa/alexa-skills-kit-sdk-for-nodejs/blob/2.0.x/ask-sdk-core/lib/skill/Skill.ts#L93 + const alexaResponse = await AmazonAlexa.skill.invoke(message) + + // Extract msg (for Bot Builder) and responseBuilder (for Alexa response) + const { response: { msg, responseBuilder } } = alexaResponse + // Delete the current response object + delete alexaResponse.response + _.set(opts, 'responseBuilder', responseBuilder) + _.set(opts, 'alexaResponseTemplate', alexaResponse) + return msg + } catch (err) { + logger.error(`[Amazon Alexa] Error invoking skill: ${err}`) + throw err + } + } + + populateMessageContext (req) { + const chatId = _.get(req, 'body.session.sessionId') + const senderId = _.get(req, 'body.session.user.userId') + if (_.isNil(chatId) || _.isNil(senderId)) { + throw new BadRequestError('Invalid sessionId or userId', { chatId, senderId }) + } + return { chatId, senderId } + } + + finalizeWebhookRequest (req, res, context, replies) { + if (!_.isEmpty(replies)) { + return res.status(200).json(_.head(replies)) + } + const emptyResponse = _.get(context, 'alexaResponseTemplate', {}) + _.set(emptyResponse, 'response', {}) + res.status(200).send(emptyResponse) + } + + formatOutgoingMessage (conversation, message, context) { + const { attachment: { type, content } } = message + + if (!_.includes(['text', 'card'], type)) { + throw Error(`[Amazon Alexa] Unsupported response type: ${type}`) + } + + // Special memory flag to end Alexa conversation (session) + const shouldEndSession = _.get(context, ['memory', AmazonAlexa.END_CONVERSATION], false) + + // Prepared Alexa response format from Skill invocation + const alexaResponseTemplate = _.get(context, 'alexaResponseTemplate', {}) + + let response = {} + try { + if (type === 'text') { + response = context.responseBuilder + .speak(content) + .withShouldEndSession(shouldEndSession) + .getResponse() + } + if (type === 'card') { + const { title: cardTitle, subtitle: cardContent, imageUrl: largeImageUrl } = content + + if (largeImageUrl) { + // 1. case: Standard Card with image + response = context.responseBuilder + .speak(content) + .withStandardCard(cardTitle, cardContent, largeImageUrl) + .withShouldEndSession(false) + .getResponse() + } else if (cardContent === 'account_linking') { + // 2. case: Account Linking + response = context.responseBuilder + .speak(content) + .withLinkAccountCard() + .withShouldEndSession(false) + .getResponse() + } + // Default case: Simple Card + response = context.responseBuilder + .speak(content) + .withSimpleCard(cardTitle, cardContent) + .withShouldEndSession(false) + .getResponse() + } + } catch (err) { + logger.error(`[Amazon Alexa] Error creating outgoing message: ${err}`) + throw err + } + _.set(alexaResponseTemplate, 'response', response) + return alexaResponseTemplate + } + + // [END] Inherited from AbstractChannelIntegration + + static LaunchRequestHandler = { + canHandle: (input) => input.requestEnvelope.request.type === AmazonAlexa.REQUEST_LAUNCH, + handle: (input) => { + return { + msg: { + attachment: { + content: AmazonAlexa.CONVERSATION_START, + type: AmazonAlexa.CONVERSATION_START, + }, + }, + responseBuilder: input.responseBuilder, + } + }, + } + + static SessionEndedRequestHandler = { + canHandle: (input) => input.requestEnvelope.request.type === AmazonAlexa.SESSION_ENDED_REQUEST, + handle: (input) => { + return { + msg: { + attachment: { + content: null, + type: AmazonAlexa.CONVERSATION_END, + }, + }, + responseBuilder: input.responseBuilder, + } + }, + } + + static CatchAllHandler = { + canHandle: (input) => input.requestEnvelope.request.type === AmazonAlexa.INTENT_REQUEST + && input.requestEnvelope.request.intent.name === AmazonAlexa.CATCH_ALL_INTENT, + handle: (input) => { + return { + msg: { + attachment: { + content: _.get(input, ['requestEnvelope', 'request', 'intent', 'slots', + AmazonAlexa.CATCH_ALL_SLOT, 'value']), + type: 'text', + }, + }, + responseBuilder: input.responseBuilder, + } + }, + } + + static skill = SkillBuilders.custom() + .addRequestHandlers(AmazonAlexa.LaunchRequestHandler, + AmazonAlexa.SessionEndedRequestHandler, + AmazonAlexa.CatchAllHandler) + .create() + + async refreshAccessToken (channel) { + const { refresh_token } = channel.oAuthTokens + try { + const response = await lwa.refreshAccessToken( + refresh_token, config.amazon_client_id, config.amazon_client_secret) + channel.oAuthTokens.access_token = response.access_token + await channel.save() + } catch (err) { + logger.error(`[Amazon Alexa] Error refreshing access token: ${err}`) + throw err + } + } + + async smapiCallWithAutoTokenRefresh (channel, func, ...params) { + let smapi = new AlexaSMAPI(channel.oAuthTokens.access_token) + try { + const response = await smapi[func](...params) + return response + } catch (err) { + if (err.status === 401) { + await this.refreshAccessToken(channel) + smapi = new AlexaSMAPI(channel.oAuthTokens.access_token) + return smapi[func](...params) + } + throw err + } + } + + async getVendors (channel) { + try { + const vendors = await this.smapiCallWithAutoTokenRefresh(channel, 'getVendors') + return vendors + } catch (sdkError) { + const { message, status } = sdkError + throw new AppError(message, null, status) + } + } + + async deploy (channel, { vendor = null, locales = [], isUpdate = false }) { + channel.invocationName = this.convertInvocationName(channel.invocationName) + channel.vendor = vendor + channel.locales = locales + + const skillManifest = this.createSkillManifest(channel) + const interactionModel = this.createInteractionModel(channel.invocationName) + + try { + if (isUpdate) { + await this.smapiCallWithAutoTokenRefresh( + channel, 'updateSkill', channel.skillId, skillManifest) + } else { + const { body: { skillId } } = await this.smapiCallWithAutoTokenRefresh( + channel, 'createSkill', channel.vendor, skillManifest) + channel.skillId = skillId + } + + for (const locale of channel.locales) { + await this.smapiCallWithAutoTokenRefresh( + channel, 'updateInteractionModel', channel.skillId, locale, interactionModel) + } + await channel.save() + } catch (sdkError) { + const { message, status } = sdkError + throw new AppError(message, null, status) + } + } + + createPublishingInformationByLocal (channel) { + const publishingInformation = {} + for (const locale of channel.locales) { + publishingInformation[locale] = { + name: channel.slug, + summary: 'This is an Alexa custom skill using SAP Conversational AI.', + description: 'This skill can leverage all the power of SAP Conversational AI.', + examplePhrases: [ + `Alexa, ask ${channel.invocationName}.`, + `Alexa, begin ${channel.invocationName}.`, + `Alexa, do ${channel.invocationName}.`, + ], + } + } + return publishingInformation + } + + createPrivacyAndComplianceByLocal (channel) { + const privacyAndCompliance = {} + for (const locale of channel.locales) { + privacyAndCompliance[locale] = { + privacyPolicyUrl: 'http://www.myprivacypolicy.sampleskill.com', + termsOfUseUrl: 'http://www.termsofuse.sampleskill.com', + } + } + return privacyAndCompliance + } + + createSkillManifest (channel) { + return { + publishingInformation: { + locales: this.createPublishingInformationByLocal(channel), + isAvailableWorldwide: false, + category: 'NOVELTY', + distributionCountries: ['US'], + }, + apis: { + custom: { + endpoint: { + sslCertificateType: 'Wildcard', + uri: channel.webhook, + }, + }, + }, + manifestVersion: '1.0', + permissions: [], + privacyAndCompliance: { + allowsPurchases: false, + usesPersonalInfo: false, + isChildDirected: false, + isExportCompliant: false, + containsAds: false, + locales: this.createPrivacyAndComplianceByLocal(channel), + }, + } + } + + createInteractionModel (invocationName) { + return { + languageModel: { + invocationName, + intents: [ + { + name: 'CATCH_ALL_INTENT', + slots: [ + { + name: 'CATCH_ALL_SLOT', + type: 'CATCH_ALL_SLOT_TYPE', + }, + ], + samples: [ + '{CATCH_ALL_SLOT}', + ], + }, + ], + types: [ + { + name: 'CATCH_ALL_SLOT_TYPE', + values: [ + { + name: { + value: 'CATCH_ALL_SLOT_VALUE', + }, + }, + ], + }, + ], + }, + } + } + + convertInvocationName (invocationName) { + // Invocation name must start with a letter and can only contain lower case + // letters, spaces, apostrophes, and periods. + return invocationName.replace(/[^a-zA-Z ]/g, '').toLowerCase() + } +} diff --git a/src/channel_integrations/amazon_alexa/controller.js b/src/channel_integrations/amazon_alexa/controller.js new file mode 100644 index 0000000..18e4185 --- /dev/null +++ b/src/channel_integrations/amazon_alexa/controller.js @@ -0,0 +1,66 @@ +import { Channel, Connector } from '../../models' +import { logger } from '../../utils' +import { NotFoundError } from '../../utils/errors' +import AmazonAlexaChannel from './channel.js' + +import { renderOk } from '../../utils/responses' + +export default class AmazonController { + + static async loadChannel (req, res) { + const connector = await Connector.findOne({ _id: req.params.connectorId, isActive: true }) + const channel_slug = req.params.channel_slug + + if (!connector) { + throw new NotFoundError('Connector') + } + + // Load the existing Channel model object + let channel + try { + channel = await Channel.findOne({ + slug: channel_slug, + connector: connector._id, + isActive: true, + }) + } catch (err) { + logger.error(`[Amazon Alexa] Error loading Channel model: ${err}`) + res.status(500).send(err) + } + + if (!channel) { + throw new NotFoundError('Channel') + } + return channel + } + + static async getVendors (req, res) { + const channel = await AmazonController.loadChannel(req, res) + try { + const channelIntegration = new AmazonAlexaChannel() + const vendors = await channelIntegration.getVendors(channel) + return renderOk(res, vendors.body) + } catch (err) { + logger.error(`[Amazon Alexa] Error retrieving Vendors: ${err} (${JSON.stringify(err.body)})`) + logger.error(err.stack) + res.status(500).send(err) + } + } + + static async deploy (req, res) { + const channel = await AmazonController.loadChannel(req, res) + try { + const channelIntegration = new AmazonAlexaChannel() + await channelIntegration.deploy(channel, req.body) + return renderOk(res) + } catch (err) { + logger.error(`[Amazon Alexa] Error deploying: ${err} (${JSON.stringify(err.body)})`) + logger.error(err.stack) + res.status(500).send(err) + } + } + + static getSupportedLocales (req, res) { + return renderOk(res, AmazonAlexaChannel.supportedLocales) + } +} diff --git a/src/channel_integrations/amazon_alexa/index.js b/src/channel_integrations/amazon_alexa/index.js new file mode 100644 index 0000000..303fbfb --- /dev/null +++ b/src/channel_integrations/amazon_alexa/index.js @@ -0,0 +1,4 @@ +import channel from './channel' +import routes from './routes' + +module.exports = { channel, routes, identifiers: ['amazonalexa'] } diff --git a/src/channel_integrations/amazon_alexa/routes.js b/src/channel_integrations/amazon_alexa/routes.js new file mode 100644 index 0000000..b8de320 --- /dev/null +++ b/src/channel_integrations/amazon_alexa/routes.js @@ -0,0 +1,25 @@ +import controller from './controller' + +export default [ + { + method: 'GET', + path: ['/connectors/:connectorId/channels/:channel_slug/amazon/vendors'], + validators: [], + authenticators: [], + handler: controller.getVendors, + }, + { + method: 'GET', + path: ['/connectors/:connectorId/channels/:channel_slug/amazon/locales'], + validators: [], + authenticators: [], + handler: controller.getSupportedLocales, + }, + { + method: 'POST', + path: ['/connectors/connectorId/channels/:channel_slug/amazon/deploy'], + validators: [], + authenticators: [], + handler: controller.deploy, + }, +] diff --git a/src/channel_integrations/amazon_alexa/sdk.js b/src/channel_integrations/amazon_alexa/sdk.js new file mode 100644 index 0000000..53d467d --- /dev/null +++ b/src/channel_integrations/amazon_alexa/sdk.js @@ -0,0 +1,129 @@ +import Promise from 'bluebird' +import superagent from 'superagent' +import superagentPromise from 'superagent-promise' +import _ from 'lodash' +import assert from 'assert' + +const agent = superagentPromise(superagent, Promise) + +export class AlexaSMAPIError extends Error { + constructor (message = '', body = {}, status = 500) { + super(message) + this.constructor = AlexaSMAPIError + // eslint-disable-next-line no-proto + this.__proto__ = AlexaSMAPIError.prototype + this.name = this.constructor.name + Error.captureStackTrace(this, this.constructor) + this.status = status + this.message = _.get(body, 'message', '') + } +} + +export default class AlexaSMAPI { + + static locales = ['en-US', 'en-GB', 'de-DE', 'en-CA', 'en-IN', 'en-AU', 'ja-JP'] + + constructor (accessToken, + api = 'https://api.amazonalexa.com') { + this.accessToken = accessToken + this.api = api + } + + async _callApi ({ url, method = 'GET', query = {}, payload = {} }) { + try { + const request = agent(method, url) + .set('Authorization', this.accessToken) + .query(query) + + if (!_.isEmpty(payload)) { + request.send(payload) + } + const { body, status } = (await request.end()) + return { body, status } + } catch (err) { + throw new AlexaSMAPIError(err.message, err.response.body, err.response.statusCode) + } + } + + async getSkill (skillId, stage = 'development') { + const url = `${this.api}/v1/skills/${skillId}/stages/${stage}/manifest` + return this._callApi({ url }) + } + + async createSkill (vendorId, manifest) { + const url = `${this.api}/v1/skills` + const method = 'POST' + const payload = { vendorId, manifest } + return this._callApi({ url, method, payload }) + } + + async updateSkill (skillId, manifest, stage = 'development') { + const url = `${this.api}/v1/skills/${skillId}/stages/${stage}/manifest` + const method = 'PUT' + const payload = { manifest } + return this._callApi({ url, method, payload }) + } + + async getSkillStatus (skillId, resource = []) { + const url = `${this.api}/v1/skills/${skillId}/status` + const query = { resource } + return this._callApi({ url, query }) + } + + async listSkills ({ vendorId, skillId = [], maxResults, nextToken }) { + try { + if (!_.isEmpty(skillId)) { + assert(_.isUndefined(maxResults)) + assert(_.isUndefined(nextToken)) + } + if (_.isArray(skillId)) { + assert(skillId.length <= 10) + } + if (!_.isUndefined(maxResults)) { + assert(maxResults <= 50) + } + } catch (err) { + throw new AlexaSMAPIError( + `Parameters don't fulfill requirements: + - 'maxResults' and 'nextToken' must not be used when 'skillId' is in use. + - One request can include up to 10 'skillId' values. + - The value of maxResults must not exceed 50.` + ) + } + const url = `${this.api}/v1/skills` + const query = { vendorId, skillId, maxResults, nextToken } + return this._callApi({ url, query }) + } + + async deleteSkill (skillId) { + const url = `${this.api}/v1/skills/${skillId}` + const method = 'DELETE' + return this._callApi({ url, method }) + } + + async getInteractionModel (skillId, locale, stage = 'development') { + const url = `${this.api}/v1/skills/${skillId}/stages/${stage}` + + `/interactionModel/locales/${locale}` + return this._callApi({ url }) + } + + async headInteractionModel (skillId, locale, stage = 'development') { + const url = `${this.api}/v1/skills/${skillId}/stages/${stage}` + + `/interactionModel/locales/${locale}` + const method = 'HEAD' + return this._callApi({ url, method }) + } + + async updateInteractionModel (skillId, locale, interactionModel, stage = 'development') { + const url = `${this.api}/v1/skills/${skillId}/stages/${stage}` + + `/interactionModel/locales/${locale}` + const method = 'PUT' + const payload = { interactionModel } + return this._callApi({ url, method, payload }) + } + + async getVendors () { + const url = `${this.api}/v1/vendors` + return this._callApi({ url }) + } +} diff --git a/src/channel_integrations/amazon_alexa/test/integration.js b/src/channel_integrations/amazon_alexa/test/integration.js new file mode 100644 index 0000000..5eaad11 --- /dev/null +++ b/src/channel_integrations/amazon_alexa/test/integration.js @@ -0,0 +1,149 @@ +import chai from 'chai' +import { setupChannelIntegrationTests } from '../../../../test/tools' +import nock from 'nock' + +const expect = chai.expect +const should = chai.should() + +const channelCreationParams = { + type: 'amazonalexa', + slug: 'my-awesome-channel', + invocationName: 'My Bot', + isActivated: true, +} + +describe('Amazon Alexa Channel', () => { + + const { createChannel, deleteChannel, + updateChannel, sendMessageToWebhook } = setupChannelIntegrationTests() + const amazonAPI = 'https://api.amazon.com' + + beforeEach(async () => { + nock(amazonAPI).post('/auth/o2/token').reply(200, { + access_token: '1337', + refresh_token: '1337', + }) + }) + + describe('creation', () => { + it('should be successful with valid parameters', async () => { + const response = await createChannel(channelCreationParams) + const { results: result, message } = response.body + + expect(response.status).to.equal(201) + expect(message).to.equal('Channel successfully created') + expect(result.type).to.equal(channelCreationParams.type) + expect(result.slug).to.equal(channelCreationParams.slug) + expect(result.invocationName).to.equal(channelCreationParams.invocationName) + expect(result.oAuthTokens).to.have.all.keys('access_token', 'refresh_token') + expect(result.oAuthTokens.access_token).to.be.a('string') + expect(result.oAuthTokens.refresh_token).to.be.a('string') + /* eslint no-unused-expressions: 0 */ // --> OFF + expect(result.isErrored).to.be.false + expect(result.isActivated).to.be.true + }) + it('should return 400 with invalid parameters', async () => { + try { + await createChannel({}) + should.fail() + } catch (error) { + expect(error.status).to.equal(400) + } + }) + }) + + describe('Update', () => { + it('should be successful if channel exists', async () => { + let response = await createChannel(channelCreationParams) + const channel = response.body.results + const newValues = Object.assign({}, channelCreationParams) + newValues.invocationName = 'Your Bot' + response = await updateChannel(channel, newValues) + expect(response.status).to.equal(200) + expect(response.body.results.invocationName).to.equal(newValues.invocationName) + }) + }) + + describe('Deletion', () => { + it('should be successful if channel exists', async () => { + let response = await createChannel(channelCreationParams) + const channel = response.body.results + response = await deleteChannel(channel) + expect(response.status).to.equal(200) + }) + }) + + describe('sending a message', () => { + let channel + const body = { + version: '1.0', + session: { + new: false, + sessionId: 'amzn1.echo-api.session.a313142e-bb3a-4cc1-8165-88ca4e1cf3fa', + application: { + applicationId: 'amzn1.ask.skill.0ef4a28b-1e00-49a9-b0f3-b67ea3364835', + }, + user: { + // eslint-disable-next-line max-len + userId: 'amzn1.ask.account.AEB564HYZGUIB3AFFXONLUR4FHMGPT2OOLUHC5C3BOQZCGTI6RRW2IQBUWIX57TMYQ64PNNIWYSPVADY6GQUMBFOUHRI6L2H6O6ZDN2KSOGGIUJJKEWCW4L23I2FN3FWHS5OCUV6EQEH2OCRAIX75XPLXVOFNHUMFCHWYM3YWVA6E4JTMMLT7RKFP3QRTB3DU44MD54ARFWRQHQ', + }, + }, + request: { + type: 'IntentRequest', + requestId: 'amzn1.echo-api.request.6013fceb-b7f9-4947-b796-b32afeb00580', + timestamp: '2018-07-02T21:33:55Z', + locale: 'en-US', + intent: { + name: 'CATCH_ALL_INTENT', + confirmationStatus: 'NONE', + slots: { + CATCH_ALL_SLOT: { + name: 'CATCH_ALL_SLOT', + value: 'hallo', + resolutions: { + resolutionsPerAuthority: [ + { + // eslint-disable-next-line max-len + authority: 'amzn1.er-authority.echo-sdk.amzn1.ask.skill.0ef4a28b-1e00-49a9-b0f3-b67ea3364835.CATCH_ALL_SLOT_TYPE', + status: { + code: 'ER_SUCCESS_NO_MATCH', + }, + }, + ], + }, + confirmationStatus: 'NONE', + }, + }, + }, + }, + } + beforeEach(async () => { + channel = (await createChannel(channelCreationParams)).body.results + }) + + it('should be successful with valid parameters', async () => { + // Hard-coded for testing purposes + const signature = 'sha1=a3a1f35918b7b8c8d187e75dfa793a843e5c2b3c' + const headers = { 'x-hub-signature': signature } + const response = await sendMessageToWebhook(channel, body, headers) + expect(response.status).to.equal(200) + expect(response.body).to.have.all.keys('version', 'response', 'sessionAttributes', + 'userAgent') + expect(response.body.version).to.equal('1.0') + expect(response.body.response).to.have.all.keys('outputSpeech', 'shouldEndSession') + expect(response.body.response.outputSpeech).to.have.all.keys('type', 'ssml') + expect(response.body.response.outputSpeech.type).to.equal('SSML') + expect(response.body.response.outputSpeech.ssml).to.equal('my message') + }) + + it('should return 400 with invalid parameters', async () => { + try { + await sendMessageToWebhook(channel, { version: '1337' }) + should.fail() + } catch (error) { + expect(error.status).to.equal(400) + } + }) + + }) +}) diff --git a/src/channel_integrations/callr/channel.js b/src/channel_integrations/callr/channel.js new file mode 100644 index 0000000..062064f --- /dev/null +++ b/src/channel_integrations/callr/channel.js @@ -0,0 +1,107 @@ +import _ from 'lodash' +import callr from 'callr' +import crypto from 'crypto' + +import { logger, BadRequestError, ForbiddenError, textFormatMessage } from '../../utils' +import AbstractChannelIntegration from '../abstract_channel_integration' + +export default class Callr extends AbstractChannelIntegration { + + validateChannelObject (channel) { + if (!channel.password) { + throw new BadRequestError('Parameter password is missing') + } else if (!channel.userName) { + throw new BadRequestError('Parameter userName is missing') + } + } + + async beforeChannelCreated (channel) { + const type = 'sms.mo' + const context = { hmac_secret: channel.password, hmac_algo: 'SHA256' } + const api = new callr.api(channel.userName, channel.password) + + try { + await new Promise((resolve, reject) => + (api.call('webhooks.subscribe', type, channel.webhook, context) + .success(async (res) => { + channel.webhookToken = res.hash + resolve() + }) + .error(reject))) + channel.isErrored = false + } catch (err) { + logger.error('[CallR] Error while setting webhook', err) + channel.isErrored = true + } + + return channel.save() + } + + async afterChannelUpdated (channel, oldChannel) { + await this.afterChannelDeleted(oldChannel) + await this.beforeChannelCreated(channel) + } + + async afterChannelDeleted (channel) { + try { + const api = new callr.api(channel.userName, channel.password) + await new Promise((resolve, reject) => (api.call('webhooks.unsubscribe', channel.webhookToken) + .success(resolve) + .error(reject))) + } catch (err) { + logger.error(`[CallR] Error while unsetting webhook: ${err}`) + } + } + + authenticateWebhookRequest (req, res, channel) { + const { password } = channel + const payload = JSON.stringify(req.body) + const webhookSig = req.headers['x-callr-hmacsignature'] + const hash = crypto.createHmac('SHA256', password).update(payload).digest('base64') + + if (hash !== webhookSig) { + throw new ForbiddenError() + } + } + + populateMessageContext (req) { + return { + chatId: _.get(req, 'body.data.from'), + senderId: _.get(req, 'body.data.to'), + } + } + + parseIncomingMessage (conversation, message) { + return { + attachment: { + type: 'text', + content: message.data.text, + }, + channelType: 'callr', + } + } + + formatOutgoingMessage (conversation, message) { + let body + try { + ({ body } = textFormatMessage(message)) + } catch (error) { + throw new BadRequestError('Message type is non-supported by Callr') + } + + return body + } + + sendMessage (conversation, message, opts) { + return new Promise(async (resolve, reject) => { + const { senderId } = opts + const { chatId, channel } = conversation + const api = new callr.api(channel.userName, channel.password) + + api.call('sms.send', senderId, chatId, message, null) + .success(resolve) + .error(reject) + }) + } + +} diff --git a/src/channel_integrations/callr/index.js b/src/channel_integrations/callr/index.js new file mode 100644 index 0000000..25943cd --- /dev/null +++ b/src/channel_integrations/callr/index.js @@ -0,0 +1,3 @@ +import channel from './channel' + +module.exports = { channel, identifiers: ['callr'] } diff --git a/src/channel_integrations/callr/test/integration.js b/src/channel_integrations/callr/test/integration.js new file mode 100644 index 0000000..6b5625a --- /dev/null +++ b/src/channel_integrations/callr/test/integration.js @@ -0,0 +1,109 @@ +import chai from 'chai' +import { setupChannelIntegrationTests } from '../../../../test/tools' +import nock from 'nock' + +const expect = chai.expect +const should = chai.should() + +const channelCreationParams = { + type: 'callr', + slug: 'my-awesome-channel', + password: 'password', + userName: 'user', +} + +describe('Callr channel', () => { + + const { createChannel, deleteChannel, + updateChannel, sendMessageToWebhook } = setupChannelIntegrationTests() + + beforeEach(() => { + nock(callrAPI).post('/json-rpc/v1.1/').reply(200, { result: { hash: 'webhook-hash' } }) + }) + const callrAPI = 'https://api.callr.com' + + describe('Creation', () => { + it('should be successful with valid parameters', async () => { + const response = await createChannel(channelCreationParams) + const { results: result, message } = response.body + + expect(response.status).to.equal(201) + expect(message).to.equal('Channel successfully created') + expect(result.type).to.equal(channelCreationParams.type) + expect(result.slug).to.equal(channelCreationParams.slug) + /* eslint no-unused-expressions: 0 */ // --> OFF + expect(result.isErrored).to.be.false + expect(result.isActivated).to.be.true + }) + + it('should return 400 with invalid parameters', async () => { + try { + await createChannel({}) + should.fail() + } catch (error) { + expect(error.status).to.equal(400) + } + }) + }) + + describe('Update', () => { + it('should be successful if channel exists', async () => { + let response = await createChannel(channelCreationParams) + const channel = response.body.results + const newValues = Object.assign({}, channelCreationParams) + newValues.password = 'newpassword' + nock(callrAPI).post('/json-rpc/v1.1/').times(2).reply(200, { result: { } }) + response = await updateChannel(channel, newValues) + expect(response.status).to.equal(200) + expect(response.body.results.password).to.equal(newValues.password) + }) + }) + + describe('Deletion', () => { + it('should be successful if channel exists', async () => { + let response = await createChannel(channelCreationParams) + const channel = response.body.results + response = await deleteChannel(channel) + expect(response.status).to.equal(200) + }) + }) + + describe('sending a message', () => { + + let channel + + beforeEach(async () => { + channel = (await createChannel(channelCreationParams)).body.results + }) + + it('should be successful with valid token', async () => { + const message = { data: { from: 'sender', to: 'receiver', text: 'a message' } } + const headers = { 'x-callr-hmacsignature': 'sHMMGdzGhfW2urhzjUwY578gZct/lVFlAP3qrHTaUig=' } + const outgoingMessageCall = nock(callrAPI).post('/json-rpc/v1.1/') + .reply(200, { result: { message: 'message' } }) + const response = await sendMessageToWebhook(channel, message, headers) + expect(response.status).to.equal(200) + expect(outgoingMessageCall.isDone()).to.be.true + }) + + it('should return 401 with invalid token', async () => { + try { + const headers = { 'x-callr-hmacsignature': 'invalid' } + await sendMessageToWebhook(channel, {}, headers) + should.fail() + } catch (error) { + expect(error.status).to.equal(401) + } + }) + + it('should return 401 without a token', async () => { + try { + await sendMessageToWebhook(channel, {}) + should.fail() + } catch (error) { + expect(error.status).to.equal(401) + } + }) + + }) +}) diff --git a/src/channel_integrations/cisco_spark/channel.js b/src/channel_integrations/cisco_spark/channel.js new file mode 100644 index 0000000..5e41681 --- /dev/null +++ b/src/channel_integrations/cisco_spark/channel.js @@ -0,0 +1,132 @@ +import _ from 'lodash' +import SparkClient from 'node-sparky' + +import { logger, arrayfy } from '../../utils' +import { BadRequestError, StopPipeline } from '../../utils/errors' +import AbstractChannelIntegration from '../abstract_channel_integration' + +export default class CiscoSpark extends AbstractChannelIntegration { + + validateChannelObject (channel) { + if (!channel.token) { + throw new BadRequestError('Parameter token is missing') + } + } + + async beforeChannelCreated (channel) { + try { + const spark = new SparkClient({ token: channel.token, webhookUrl: channel.webhook }) + const webhook = { + name: channel.slug, + targetUrl: channel.webhook, + resource: 'messages', + event: 'created', + } + + const [me] = await Promise.all([ + spark.personMe(), + spark.webhookAdd(webhook), + ]) + + channel.userName = me.id + channel.isErrored = false + } catch (err) { + logger.error(`[Cisco] Error while setting the webhook: ${err}`) + channel.isErrored = true + } + + return channel.save() + } + + async afterChannelUpdated (channel, oldChannel) { + await this.afterChannelDeleted(oldChannel) + await this.beforeChannelCreated(channel) + } + + async afterChannelDeleted (channel) { + try { + const spark = new SparkClient({ token: channel.token }) + const webhooks = await spark.webhooksGet() + const webhook = webhooks.find(w => w.name === channel.slug) + + if (!webhook) { return } + await spark.webhookRemove(webhook.id) + } catch (err) { + logger.error(`[Cisco] Error while unsetting the webhook: ${err}`) + } + } + + populateMessageContext (req) { + return { + chatId: _.get(req, 'body.data.roomId'), + senderId: _.get(req, 'body.data.personId'), + } + } + + async parseIncomingMessage (conversation, message) { + const spark = new SparkClient({ token: conversation.channel.token }) + + message = await spark.messageGet(message.data.id) // decrypt the message + if (message.personId === conversation.channel.userName) { + throw new StopPipeline() + } + + return { attachment: { type: 'text', content: message.text } } + } + + formatOutgoingMessage (conversation, message) { + const { type, content } = _.get(message, 'attachment', {}) + + switch (type) { + case 'text': + case 'video': + return { text: content } + case 'picture': + return { files: content } + case 'list': { + const payload = _.get(content, 'elements', []) + .map(e => `**- ${e.title}**\n\n${e.subtitle}\n\n${e.imageUrl || ''}`) + return { + markdown: _.reduce(payload, (acc, str) => `${acc}\n\n${str}`, ''), + } + } + case 'buttons': + case 'quickReplies': + return { + markdown: `**${_.get(content, 'title', '')}**\n\n` + .concat(_.get(content, 'buttons', []).map(b => `- ${b.title}`).join('\n\n')), + } + case 'card': + return { + files: _.get(content, 'imageUrl', ''), + markdown: `**${_.get(content, 'title', '')}**\n\n` + .concat(`*${_.get(content, 'subtitle', '')}*\n\n`) + .concat(_.get(content, 'buttons', []).map(b => `- ${b.title}`).join('\n\n')), + } + case 'carousel': + case 'carouselle': + return content.map(card => ({ + files: _.get(card, 'imageUrl', ''), + markdown: `**${_.get(card, 'title', '')}**\n\n` + .concat(`*${_.get(card, 'subtitle', '')}*\n\n`) + .concat(_.get(card, 'buttons', []).map(b => `- ${b.title}`).join('\n\n')), + })) + case 'custom': + return content + default: + throw new BadRequestError('Message type non-supported by CiscoSpark') + } + } + + async sendMessage (conversation, messages, opts) { + if (conversation.channel.userName !== opts.senderId) { + const spark = new SparkClient({ token: conversation.channel.token }) + + for (const message of arrayfy(messages)) { + message.roomId = conversation.chatId + await spark.messageSend(message) + } + } + } + +} diff --git a/src/channel_integrations/cisco_spark/index.js b/src/channel_integrations/cisco_spark/index.js new file mode 100644 index 0000000..483441e --- /dev/null +++ b/src/channel_integrations/cisco_spark/index.js @@ -0,0 +1,3 @@ +import channel from './channel' + +module.exports = { channel, identifiers: ['ciscospark'] } diff --git a/src/channel_integrations/cisco_spark/test/integration.js b/src/channel_integrations/cisco_spark/test/integration.js new file mode 100644 index 0000000..b0bd81f --- /dev/null +++ b/src/channel_integrations/cisco_spark/test/integration.js @@ -0,0 +1,219 @@ +import chai from 'chai' +import { setupChannelIntegrationTests } from '../../../../test/tools' +import nock from 'nock' + +/* eslint max-nested-callbacks: 0 */ // --> OFF + +const expect = chai.expect +const should = chai.should() + +const channelCreationParams = { + type: 'ciscospark', + slug: 'my-awesome-channel', + token: 'token', +} + +describe('Cisco Spark channel', () => { + + const { createChannel, updateChannel, + deleteChannel, sendMessageToWebhook } = setupChannelIntegrationTests() + const sparkAPI = 'https://api.ciscospark.com' + + beforeEach(() => { + nock(sparkAPI).get('/v1/people/me').reply(200, {}) + nock(sparkAPI).post('/v1/webhooks').reply(200, {}) + }) + + describe('Creation', () => { + it('should be successful with valid parameters', async () => { + const response = await createChannel(channelCreationParams) + const { results: result, message } = response.body + + expect(response.status).to.equal(201) + expect(message).to.equal('Channel successfully created') + expect(result.type).to.equal(channelCreationParams.type) + expect(result.slug).to.equal(channelCreationParams.slug) + /* eslint no-unused-expressions: 0 */ // --> OFF + expect(result.isErrored).to.be.false + expect(result.isActivated).to.be.true + }) + + it('should return 400 with invalid parameters', async () => { + try { + await createChannel({}) + should.fail() + } catch (error) { + expect(error.status).to.equal(400) + } + }) + }) + + describe('Update', () => { + it('should be successful if channel exists', async () => { + let response = await createChannel(channelCreationParams) + const channel = response.body.results + const newValues = Object.assign({}, channelCreationParams) + newValues.token = 'newtoken' + nock(sparkAPI).get('/v1/people/me').reply(200, {}) + nock(sparkAPI).get('/v1/webhooks').query(true).reply(200, {}) + nock(sparkAPI).post('/v1/webhooks').reply(200, {}) + response = await updateChannel(channel, newValues) + expect(response.status).to.equal(200) + expect(response.body.results.token).to.equal(newValues.token) + }) + }) + + describe('Deletion', () => { + it('should be successful if channel exists', async () => { + let response = await createChannel(channelCreationParams) + const channel = response.body.results + nock(sparkAPI).get('/v1/webhooks').query(true).reply(200, {}) + + response = await deleteChannel(channel) + expect(response.status).to.equal(200) + }) + }) + + describe('sending a message', () => { + + let channel + + beforeEach(async () => { + channel = (await createChannel(channelCreationParams)).body.results + }) + + it('should be successful with valid parameters', async () => { + nock(sparkAPI).get('/v1/messages/message-id').query(true).reply( + 200, { personId: 123, text: 'text' }) + const outgoingMessageCall = nock(sparkAPI).post('/v1/messages').reply(200, {}) + const response = await sendMessageToWebhook(channel, { + data: { roomId: 'room-id', personId: 'person-id', id: 'message-id' }, + }) + expect(response.status).to.equal(200) + // Assert that response was sent to Cisco API + expect(outgoingMessageCall.isDone()).to.be.true + }) + + it('should not send a response for an empty incoming message', async () => { + nock(sparkAPI).get('/v1/messages/message-id').query(true).reply( + 200, { personId: 123, text: '' }) + const outgoingMessageCall = nock(sparkAPI).post('/v1/messages').reply(200, {}) + const response = await sendMessageToWebhook(channel, { + data: { roomId: 'room-id', personId: 'person-id', id: 'message-id' }, + }) + expect(response.status).to.equal(200) + // Assert that response was sent to Cisco API + expect(outgoingMessageCall.isDone()).to.be.false + }) + + describe('should be successful', () => { + + beforeEach(async () => { + nock(sparkAPI).get('/v1/messages/message-id').query(true).reply( + 200, { personId: 123, text: 'text' }) + nock(sparkAPI).post('/v1/messages').reply(200, {}) + }) + + const body = { + data: { roomId: 'room-id', personId: 'person-id', id: 'message-id' }, + } + const headers = {} + + it('in list format', async () => { + const buttons = [ + { type: 'account_link', title: 'button title', value: 'https://link.com' }, + { type: 'web_url', title: 'button title', value: 'https://link.com' }, + { type: 'phone_number', title: 'button title', value: '0123554' }] + const listElement = { + title: 'title', + subtitle: 'subtitle', + imageUrl: 'https://img.url', + buttons, + } + const botResponse = { + results: {}, + messages: JSON.stringify([{ + type: 'list', + content: { elements: [listElement], buttons }, + }]), + } + const response = await sendMessageToWebhook(channel, body, headers, botResponse) + expect(response.status).to.equal(200) + }) + + it('in card format', async () => { + const cardElement = { + title: 'title', + subtitle: 'subtitle', + imageUrl: 'https://img.url', + buttons: [{ type: '', title: 'button title', value: 'abc' }], + } + const botResponse = { + results: {}, + messages: JSON.stringify([{ type: 'card', content: cardElement }]), + } + const response = await sendMessageToWebhook(channel, body, headers, botResponse) + expect(response.status).to.equal(200) + }) + + it('in carousel format', async () => { + const carouselElement = { + title: 'title', + imageUrl: 'https://img.url', + buttons: [{ type: '', title: 'button title', value: 'abc' }], + } + const botResponse = { + results: {}, + messages: JSON.stringify([{ type: 'carousel', content: [carouselElement] }]), + } + const response = await sendMessageToWebhook(channel, body, headers, botResponse) + expect(response.status).to.equal(200) + }) + + it('in quickReplies format', async () => { + const buttonsElement = { + title: 'title', + buttons: [{ type: '', title: 'button title', value: 'abc' }], + } + const botResponse = { + results: {}, + messages: JSON.stringify([{ type: 'quickReplies', content: buttonsElement }]), + } + const response = await sendMessageToWebhook(channel, body, headers, botResponse) + expect(response.status).to.equal(200) + }) + + it('in video format', async () => { + const botResponse = { + results: {}, + messages: JSON.stringify([{ type: 'video', content: 'https://link.com' }]), + } + const response = await sendMessageToWebhook(channel, body, headers, botResponse) + expect(response.status).to.equal(200) + }) + + it('in picture format', async () => { + const botResponse = { + results: {}, + messages: JSON.stringify([{ type: 'picture', content: 'https://link.com' }]), + } + const response = await sendMessageToWebhook(channel, body, headers, botResponse) + expect(response.status).to.equal(200) + }) + + it('in buttons format', async () => { + const buttonsElement = { + title: 'title', + buttons: [{ type: '', title: 'button title', value: 'abc' }], + } + const botResponse = { + results: {}, + messages: JSON.stringify([{ type: 'buttons', content: buttonsElement }]), + } + const response = await sendMessageToWebhook(channel, body, headers, botResponse) + expect(response.status).to.equal(200) + }) + }) + + }) +}) diff --git a/src/channel_integrations/facebook/channel.js b/src/channel_integrations/facebook/channel.js new file mode 100644 index 0000000..285dad1 --- /dev/null +++ b/src/channel_integrations/facebook/channel.js @@ -0,0 +1,452 @@ +/* eslint no-unused-vars: ["error", { "ignoreRestSiblings": true }] */ +import _ from 'lodash' +import config from '../../../config' +import AbstractChannelIntegration from '../abstract_channel_integration' +import { + BadRequestError, + ForbiddenError, + getWebhookToken, + logger, + StopPipeline, +} from '../../utils' +import { + facebookGetAppWebhookToken, + facebookGetUserData, + facebookSendMessage, + facebookSendIsTyping, + facebookAddAppToPage, + facebookRemoveAppFromPage, + facebookComputeSignature, + facebookAddProfileProperties, + facebookDelProfileProperties, +} from './sdk' +import { facebookCodesMap } from './constants' +import { GetStartedButton, PersistentMenu } from '../../models' +import { formatMarkdownHelper } from '../../utils/utils' + +export default class Messenger extends AbstractChannelIntegration { + + validateChannelObject (channel) { + if (!channel.token) { + throw new BadRequestError('Parameter token is missing') + } else if (!channel.apiKey && !channel.serviceId) { + /* 1-click messenger integration + serviceId is a facebook page id + the apiKey (app secret) is not needed anymore since we use + our app with its corresponding secret + We check for both, waiting for frontend implementation */ + throw new BadRequestError('Parameter apiKey or serviceId is missing') + } + } + + async beforeChannelCreated (channel) { + /* 1-click messenger integration + This subscribes SAP Conversational AI facebook App to the user page + so gromit can receive the messages */ + const { serviceId: pageId, token: pageToken } = channel + if (pageId) { + await facebookAddAppToPage(pageId, pageToken) + } + } + + async afterChannelUpdated (channel, oldChannel) { + await this.afterChannelDeleted(oldChannel) + await this.beforeChannelCreated(channel) + } + + async afterChannelDeleted (channel) { + /* 1-click messenger integration + This removes SAP Conversational AI facebook App subscription to the user page + so gromit doesn't receive the messages anymore */ + const { serviceId: pageId, token: pageToken } = channel + if (pageId) { + await facebookRemoveAppFromPage(pageId, pageToken) + } + } + + buildWebhookUrl (channel) { + const { serviceId: pageId } = channel + if (pageId) { + /* 1-click messenger integration + add the shared messenger webhook endpoint to the channel */ + return `${config.base_url}/v1/webhook/service/messenger` + } + return super.buildWebhookUrl(channel) + } + + static webHookCheck (req, res, token) { + if (req.query['hub.mode'] === 'subscribe' && req.query['hub.verify_token'] === token) { + res.status(200).send(req.query['hub.challenge']) + } else { + throw new BadRequestError('Error while checking the webhook validity') + } + } + + onSharedWebhookChecking (req, res) { + Messenger.webHookCheck(req, res, facebookGetAppWebhookToken()) + } + + getIdPairsFromSharedWebhook (req) { + const serviceId = _.get(req, 'body.entry[0].messaging[0].recipient.id', null) + if (serviceId) { + return { serviceId } + } + } + + validateWebhookSubscriptionRequest (req, res, channel) { + Messenger.webHookCheck(req, res, getWebhookToken(channel._id, channel.slug)) + } + + async authenticateWebhookRequest (req, res, channel) { + const rawBody = _.get(req, 'rawBody') + const signature = _.get(req, ['headers', 'x-hub-signature']) + + const serviceId = _.get(channel, 'serviceId') + /* 1-click messenger integration + If there is a pageId, we are in new mode + the secret will be directly fetched by the signature function */ + const appSecret = serviceId ? null : _.get(channel, 'apiKey') + const calculated = facebookComputeSignature(rawBody, appSecret) + if (calculated !== signature) { + throw new ForbiddenError() + } + } + + async onWebhookCalled (req, res, channel) { + // send 200 OK early so that Facebook doesn't retry our web hook after 20s + // because sometimes we take more than that to produce and send a bot response back to Slack + res.status(200).json({ results: null, message: 'Message successfully received' }) + return channel + } + + finalizeWebhookRequest () { + // do nothing as 200 OK has already been sent + } + + populateMessageContext (req) { + const recipientId = _.get(req, 'body.entry[0].messaging[0].recipient.id') + const senderId = _.get(req, 'body.entry[0].messaging[0].sender.id') + + return { + chatId: `${recipientId}-${senderId}`, + senderId, + } + } + + onIsTyping (channel, context) { + const { senderId } = context + const { token: pageToken, apiKey: appSecret } = channel + return facebookSendIsTyping(senderId, pageToken, appSecret) + } + + parseIncomingMessage (conversation, message) { + const msg = {} + message = _.get(message, 'entry[0].messaging[0]') + const type = _.get(message, 'message.attachments[0].type') + const quickReply = _.get(message, 'message.quick_reply.payload') + + if (message.account_linking) { + const { status, authorization_code } = _.get(message, 'account_linking') + msg.attachment = { type: 'account_linking', status, content: authorization_code } + } else if (message.postback) { + const content = _.get(message, 'postback.payload') + msg.attachment = { type: 'payload', content } + } else if (message.referral) { + msg.attachment = { type: 'referral', content: message.referral } + } else if (!message.message || (message.message.is_echo && message.message.app_id)) { + throw new StopPipeline() + } else if (type) { + const attachment = _.get(message, 'message.attachments[0]') + // fallback type for an attachment is a link fetched by facebook and + // displayed in a special fashion (can be a video, an image...) + const content = _.get(attachment, (type === 'fallback') ? 'url' : 'payload.url') + msg.attachment = { + type: type === 'image' ? 'picture' : type, + content, + } + } else if (quickReply) { + msg.attachment = { type: 'text', content: quickReply, is_button_click: true } + } else { + const content = _.get(message, 'message.text') + msg.attachment = { type: 'text', content } + } + + if (message.message && message.message.is_echo) { + _.set(msg, 'attachment.isEcho', true) + if (!message.message.app_id) { + _.set(msg, 'attachment.isAdminMessage', true) + } + } + + return msg + } + + static formatButtons (buttons) { + return buttons.map(button => { + const { title } = button + const type = button.type || 'text' + const value = button.value || button.url + + if (['account_linking', 'account_link'].indexOf(type) !== -1) { + return { type: 'account_link', title, url: value } + } else if (type === 'web_url') { + return { type, title, url: value } + } else if (type === 'phonenumber') { + return { type: 'phone_number', title, payload: value } + } else if (['postback', 'phone_number', 'element_share'].indexOf(type) !== -1) { + return { type, title, payload: value } + } + return { type } + }) + } + + formatMarkdown (message) { + return formatMarkdownHelper(message, true) + } + + formatOutgoingMessage (conversation, message, opts) { + // https://developers.facebook.com/docs/messenger-platform/send-messages + const { type, content } = _.get(message, 'attachment') + const msg = { + recipient: { id: opts.senderId }, + message: {}, + messaging_type: 'RESPONSE', + } + + switch (type) { + case 'text': + _.set(msg, 'message', { text: content }) + break + case 'video': + case 'picture': + case 'audio': // Special case needed for StarWars ? + _.set(msg, 'message.attachment.type', type === 'picture' ? 'image' : type) + _.set(msg, 'message.attachment.payload.url', content) + break + case 'card': + // FIXME: FB messenger only supports up to 3 buttons on a card + // https://developers.facebook.com/docs/messenger-platform/reference/template/generic#elements + const { + title, + itemUrl: item_url, + imageUrl: image_url, + subtitle } = _.get(message, 'attachment.content', {}) + const buttons = Messenger.formatButtons(_.get(message, 'attachment.content.buttons', [])) + + _.set(msg, 'message.attachment.type', 'template') + _.set(msg, 'message.attachment.payload.template_type', 'generic') + _.set(msg, + 'message.attachment.payload.elements', + [{ title, item_url, image_url, subtitle, buttons }]) + break + case 'quickReplies': + // FIXME: FB messenger only supports up to 11 quick reply buttons + // https://developers.facebook.com/docs/messenger-platform/reference/send-api/quick-replies + const text = _.get(message, 'attachment.content.title', '') + const quick_replies = _.get(message, 'attachment.content.buttons', []) + .map(b => ({ content_type: 'text', title: b.title, payload: b.value })) + + _.set(msg, 'message', { text, quick_replies }) + break + case 'list': { + const rawElements = _.get(message, 'attachment.content.elements', []) + const elements = rawElements.map(e => ({ + title: e.title, + image_url: e.imageUrl, + subtitle: e.subtitle, + buttons: e.buttons && Messenger.formatButtons(e.buttons), + })) + + // FB Messenger only supports lists with 2 - 4 elements + // workaround so this doesn't result in an error but we still are + // successful and not block following messages + // (FB would just retry the failed message over and over if status code != 200) + // https://developers.facebook.com/docs/messenger-platform/send-messages/template/list + if (elements.length < 2) { + // if list has only one element, send success instead of failing later + logger.error(`[Facebook Messenger] Channel ${conversation.channel.id} tried to send a list ` + + 'with less than 2 elements. List not sent.') + throw new StopPipeline() + } else if (elements.length > 4) { + // only take the first four elements of the list + elements.splice(4) + logger.error(`[Facebook Messenger] Channel ${conversation.channel.id} tried to send a list ` + + 'with more than 4 elements. Last elements omitted.') + } + + const payload = { template_type: 'list', elements } + + // In normal conditions, the first image must always have an image + if (rawElements.length > 0 && !('imageUrl' in rawElements[0])) { + payload.top_element_style = 'compact' + } + + const buttons = Messenger.formatButtons(_.get(message, 'attachment.content.buttons', [])) + if (buttons.length > 0) { + _.set(msg, 'message.attachment.payload.buttons', buttons) + } + + _.set(msg, 'message.attachment.type', 'template') + _.set(msg, 'message.attachment.payload', payload) + break + } + case 'carousel': + case 'carouselle': + // FIXME: FB messenger only supports up to 10 carousel elements + // https://developers.facebook.com/docs/messenger-platform/reference/template/generic#payload + const elements = _.get(message, 'attachment.content', []) + .map(content => { + const { title, itemUrl: item_url, imageUrl: image_url, subtitle } = content + const buttons = Messenger.formatButtons(_.get(content, 'buttons', [])) + const element = { title, subtitle, item_url, image_url } + + if (buttons.length > 0) { + _.set(element, 'buttons', buttons) + } + + return element + }) + + if (elements.splice(10).length !== 0) { + logger.error(`[Facebook Messenger] Channel ${conversation.channel.id} tried to + send a carousel with more than 10 elements. Last elements omitted.`) + } + + _.set(msg, 'message.attachment.type', 'template') + _.set(msg, 'message.attachment.payload.template_type', 'generic') + _.set(msg, 'message.attachment.payload.elements', elements) + break + case 'buttons': { + // FIXME: FB messenger only supports up to 3 buttons + // https://developers.facebook.com/docs/messenger-platform/reference/template/button#payload + const text = _.get(message, 'attachment.content.title', '') + const payload = { template_type: 'button', text } + + _.set(msg, 'message.attachment.type', 'template') + _.set(msg, 'message.attachment.payload', payload) + + const buttons = Messenger.formatButtons(_.get(message, 'attachment.content.buttons', [])) + if (buttons.length > 0) { + _.set(msg, 'message.attachment.payload.buttons', buttons) + } + break + } + case 'custom': + _.set(msg, 'message', content) + break + default: + throw new BadRequestError('Message type non-supported by Messenger') + } + + return msg + } + + async sendMessage (conversation, message) { + const { channel: { token: pageToken, apiKey: appSecret } } = conversation + await facebookSendMessage(message, pageToken, appSecret) + } + + /* + * Gromit methods + */ + + async populateParticipantData (participant, channel) { + const fields = 'first_name,last_name,profile_pic,locale,timezone,gender' + const { token: pageToken, apiKey: appSecret } = channel + const { id, ...participantData } + = await facebookGetUserData(participant.senderId, pageToken, fields, appSecret) + participant.data = participantData + participant.markModified('data') + return participant.save() + } + + parseParticipantDisplayName (participant) { + const informations = {} + + if (participant.data) { + const { first_name, last_name } = participant.data + informations.userName = `${first_name} ${last_name}` + } + + return informations + } + + formatItems (menu) { + menu.call_to_actions.map(item => { + if (item.type === 'Link') { + item.type = 'web_url' + item.url = item.payload + delete item.payload + } else if (item.type === 'nested') { + this.formatItems(item) + } + return item + }) + } + + formatPersistentMenu (menus) { + const persistent_menu = menus.reduce((formattedMenus, currentMenu) => { + // copying currentMenu.menu into currentFormattedMenu to not modify the currentMenu + const currentFormattedMenu = JSON.parse(JSON.stringify(currentMenu.menu)) + if (!facebookCodesMap[currentMenu.locale]) { + throw new BadRequestError('Language non-supported by Messenger') + } + if (currentMenu.default === true) { + currentFormattedMenu.locale = 'default' + } else { + currentFormattedMenu.locale = facebookCodesMap[currentMenu.locale] + } + currentFormattedMenu.composer_input_disabled = false + this.formatItems(currentFormattedMenu) + formattedMenus.push(currentFormattedMenu) + return formattedMenus + }, []) + return persistent_menu + } + + async setGetStartedButton (channel, value, connector = null) { + const pageToken = channel.token + const properties = { + get_started: { payload: value }, + persistent_menu: [], + } + if (connector) { + const menus = await PersistentMenu.find({ connector_id: connector._id }) + if (menus.length) { + properties.persistent_menu = this.formatPersistentMenu(menus) + } + } + await facebookAddProfileProperties(properties, pageToken) + } + + async deleteGetStartedButton (channel) { + const pageToken = channel.token + const property = ['get_started', 'persistent_menu'] + + await facebookDelProfileProperties(property, pageToken) + } + + /** + * format all the menus and send it to facebook + * Sets the locale to default or to the corresponding facebook code + * @param {channel} an instance of the channel model + * @param {menus} an array of PersistentMenu + * @returns undefined + */ + async setPersistentMenu (channel, menus) { + // setting persistent menu only if the channel has a get started button + if (!await GetStartedButton.findOne({ channel_id: channel._id })) { + return + } + const pageToken = channel.token + const property = { persistent_menu: [] } + property.persistent_menu = this.formatPersistentMenu(menus) + await facebookAddProfileProperties(property, pageToken) + } + + async deletePersistentMenu (channel) { + const pageToken = channel.token + const property = ['persistent_menu'] + await facebookDelProfileProperties(property, pageToken) + } +} diff --git a/src/channel_integrations/facebook/constants.js b/src/channel_integrations/facebook/constants.js new file mode 100644 index 0000000..1d14db5 --- /dev/null +++ b/src/channel_integrations/facebook/constants.js @@ -0,0 +1,46 @@ +export const facebookCodesMap = { + fr: 'fr_FR', + en: 'en_US', + es: 'es_ES', + ar: 'ar_AR', + ca: 'ca_ES', + da: 'da_DK', + de: 'de_DE', + fi: 'fi_FI', + hi: 'hi_IN', + it: 'it_IT', + ja: 'ja_JP', + ko: 'ko_KR', + no: 'nb_NO', + nl: 'nl_NL', + pl: 'pl_PL', + pt: 'pt_PT', + ru: 'ru_RU', + sv: 'sv_SE', + zh: 'zh_CN', + az: 'az_AZ', + be: 'be_BY', + bn: 'bn_IN', + cs: 'cs_CZ', + el: 'el_GR', + fa: 'fa_IR', + ha: 'ha_NG', + he: 'he_IL', + hu: 'hu_HU', + id: 'id_ID', + km: 'km_KH', + ms: 'ms_MY', + my: 'my_MM', + ne: 'ne_NP', + pa: 'pa_IN', + ro: 'ro_RO', + si: 'si_LK', + sr: 'sr_RS', + th: 'th_TH', + tl: 'tl_PH', + tr: 'tr_TR', + uk: 'uk_UA', + uz: 'uz_UZ', + vi: 'vi_VN', + ur: 'ur_PK', +} diff --git a/src/channel_integrations/facebook/controller.js b/src/channel_integrations/facebook/controller.js new file mode 100644 index 0000000..85c486f --- /dev/null +++ b/src/channel_integrations/facebook/controller.js @@ -0,0 +1,128 @@ +import { validateFacebookToken } from './middlewares' +import { renderOk, BadRequestError, ServiceError } from '../../utils' +import { + facebookGetAppToken, + facebookGetClientToken, + facebookGetExtendedClientToken, + facebookGetProfile, + facebookGetUserPages, + facebookGetPagesTokens, + facebookGetPagesPictures, +} from './sdk' + +const USER_PERM_ADMINISTER = 'ADMINISTER' +const USER_PERM_EDIT_PROFILE = 'EDIT_PROFILE' + +const checkClientToken = (clientToken, clientTokenType) => { + if (!clientToken || clientTokenType !== 'bearer') { + throw new ServiceError('Error while requesting facebook client token') + } +} + +export default class FacebookController { + static async getTokenFromCode (req, res) { + const { facebook_code: code, facebook_redirect: redirectUri } = req.query + + if (!code || !redirectUri) { + throw new BadRequestError('Missing facebook_code or facebook_redirect parameter') + } + + const appToken = await facebookGetAppToken() + + const { access_token: clientToken, token_type: clientTokenType, expires_in: clientTokenExpiry } + = await facebookGetClientToken(code, redirectUri, appToken) + + checkClientToken(clientToken, clientTokenType) + + const clientId = await validateFacebookToken(null, clientToken, appToken) + + return renderOk(res, { + results: { + facebook_token: clientToken, + facebook_user: clientId, + facebook_expiry: clientTokenExpiry, + }, + message: 'Facebook token successfully received', + }) + } + + static async refreshToken (req, res) { + const { clientId, clientToken, appToken } = req + + const { + access_token: newClientToken, + token_type: newClientTokenType, + expires_in: newClientTokenExpiry, + } + = await facebookGetExtendedClientToken(clientToken) + + checkClientToken(newClientToken, newClientTokenType) + + await validateFacebookToken(clientId, newClientToken, appToken) + + return renderOk(res, { + results: { + facebook_token: newClientToken, + facebook_user: clientId, + facebook_expiry: newClientTokenExpiry, + }, + message: 'Facebook token successfully refreshed', + }) + } + + static async getProfile (req, res) { + const { clientToken } = req + + const fields = 'name,picture' + const { name, picture: { data: { url: picture } } } + = await facebookGetProfile(clientToken, fields) + + return renderOk(res, { + results: { + name, + picture, + }, + message: 'Facebook profile successfully received', + }) + } + + static async getPages (req, res) { + const { clientToken, appToken } = req + + const pages = await facebookGetUserPages(clientToken) + const userAdministeredPages = pages + .filter( + page => + page.perms.includes(USER_PERM_ADMINISTER) + || page.perms.includes(USER_PERM_EDIT_PROFILE)) + .reduce((acc, page) => { + const { name, id } = page + return { + ...acc, + [id]: { name }, + } + }, {}) + + const pageIds = Object.keys(userAdministeredPages) + const [pagesPictures, pagesTokens] = await Promise.all([ + facebookGetPagesPictures(pageIds, appToken), + facebookGetPagesTokens(pageIds, clientToken), + ]) + + pagesTokens.forEach((tokenArray) => { + const pageId = tokenArray[0] + const pageToken = tokenArray[1] + userAdministeredPages[pageId].token = pageToken + }) + + pagesPictures.forEach((picture, index) => { + const picturePageId = pageIds[index] + userAdministeredPages[picturePageId].picture = picture.url + }) + + return renderOk(res, { + results: userAdministeredPages, + message: 'List of facebook pages successfully found', + }) + } +} diff --git a/src/channel_integrations/facebook/index.js b/src/channel_integrations/facebook/index.js new file mode 100644 index 0000000..dcc7219 --- /dev/null +++ b/src/channel_integrations/facebook/index.js @@ -0,0 +1,4 @@ +import channel from './channel' +import routes from './routes' + +module.exports = { channel, routes, identifiers: ['messenger'] } diff --git a/src/channel_integrations/facebook/middlewares.js b/src/channel_integrations/facebook/middlewares.js new file mode 100644 index 0000000..d5ab7f8 --- /dev/null +++ b/src/channel_integrations/facebook/middlewares.js @@ -0,0 +1,53 @@ +import { + facebookGetAppId, + facebookGetAppToken, + facebookGetClientTokenInformation, +} from './sdk' +import { BadRequestError, ForbiddenError } from '../../utils' + +const CLIENT_TOKEN_PERMS = [ + 'public_profile', + 'email', + 'manage_pages', + 'pages_messaging', + 'pages_messaging_subscriptions', +] + +export const validateFacebookParams = async (req) => { + const { facebook_token: clientToken, facebook_user: clientId } = req.query + if (!clientToken || !clientId) { + throw new BadRequestError('Missing facebook_token, facebook_expiry or facebook_user parameter') + } + + const appToken = await facebookGetAppToken() + + await validateFacebookToken(clientId, clientToken, appToken) + + req.clientId = clientId + req.clientToken = clientToken + req.appToken = appToken +} + +export const validateFacebookToken = async (clientId, clientToken, appToken) => { + const { + data: { + app_id: clientTokenAppId, + type: clientTokenType, + is_valid: clientTokenIsValid, + user_id: clientTokenUserId, + scopes: clientTokenScopes, + }, + } = await facebookGetClientTokenInformation(clientToken, appToken) + + if ( + !clientTokenIsValid + || clientTokenType !== 'USER' + || (clientId && clientTokenUserId !== clientId) + || clientTokenAppId !== facebookGetAppId() + || !CLIENT_TOKEN_PERMS.every((perm) => clientTokenScopes.includes(perm)) + ) { + throw new ForbiddenError() + } + + return clientTokenUserId +} diff --git a/src/channel_integrations/facebook/routes.js b/src/channel_integrations/facebook/routes.js new file mode 100644 index 0000000..df242b5 --- /dev/null +++ b/src/channel_integrations/facebook/routes.js @@ -0,0 +1,33 @@ +import { validateFacebookParams } from './middlewares' +import controller from './controller' + +export default [ + { + method: 'GET', + path: ['/facebook/token'], + validators: [], + authenticators: [], + handler: controller.getTokenFromCode, + }, + { + method: 'GET', + path: ['/facebook/refresh_token'], + validators: [], + authenticators: [validateFacebookParams], + handler: controller.refreshToken, + }, + { + method: 'GET', + path: ['/facebook/profile'], + validators: [], + authenticators: [validateFacebookParams], + handler: controller.getProfile, + }, + { + method: 'GET', + path: ['/facebook/pages'], + validators: [], + authenticators: [validateFacebookParams], + handler: controller.getPages, + }, +] diff --git a/src/channel_integrations/facebook/sdk.js b/src/channel_integrations/facebook/sdk.js new file mode 100644 index 0000000..b6cf816 --- /dev/null +++ b/src/channel_integrations/facebook/sdk.js @@ -0,0 +1,240 @@ +import _ from 'lodash' +import { createHmac } from 'crypto' +import Promise from 'bluebird' +import Graph from 'fbgraph' +import config from '../../../config' +import { ServiceError } from '../../utils' + +Graph.setVersion('2.11') +Promise.promisifyAll(Graph) + +export const facebookGetAppId = () => config.facebook_app_id +const getAppSecret = () => config.facebook_app_secret + +const getOAuthToken = async (appId, appSecret, params) => { + const response = await Graph.getAsync('/oauth/access_token', { + client_id: appId, + client_secret: appSecret, + ...params, + }) + return response +} + +export const facebookGetAppToken = async () => { + const appId = facebookGetAppId() + const appSecret = getAppSecret() + const response = await getOAuthToken(appId, appSecret, { grant_type: 'client_credentials' }) + + const { access_token: appToken, token_type: appTokenType } = response + if (!appToken || appTokenType !== 'bearer') { + throw new ServiceError('Error while requesting facebook application token') + } + return appToken +} + +export const facebookGetClientTokenInformation = async (clientToken, appToken) => { + const response = await Graph.getAsync('/debug_token', { + input_token: clientToken, + access_token: appToken, + }) + return response +} + +const getAuthSecureParams = (token, appSecret) => { + const hmac = createHmac('sha256', appSecret) + hmac.update(token) + return { + access_token: token, + appsecret_proof: hmac.digest('hex'), + } +} + +export const facebookGetClientToken = async (code, redirectUri, appToken) => { + const appId = facebookGetAppId() + const appSecret = getAppSecret() + const authSecureParams = getAuthSecureParams(appToken, appSecret) + + const response = await getOAuthToken(appId, appSecret, { + redirect_uri: redirectUri, + code, + ...authSecureParams, + }) + + return response +} + +export const facebookGetExtendedClientToken = async (clientToken) => { + const appId = facebookGetAppId() + const appSecret = getAppSecret() + const authSecureParams = getAuthSecureParams(clientToken, appSecret) + + const response = await getOAuthToken(appId, appSecret, { + grant_type: 'fb_exchange_token', + fb_exchange_token: clientToken, + ...authSecureParams, + }) + + return response +} + +export const facebookGetProfile = async (clientToken, fields) => { + const appSecret = getAppSecret() + const authSecureParams = getAuthSecureParams(clientToken, appSecret) + + const data = await Graph.getAsync('/me', { + fields, + ...authSecureParams, + }) + return data +} + +export const facebookGetUserData = async (userId, pageToken, fields, appSecret) => { + const properAppSecret = appSecret || getAppSecret() + const authSecureParams = getAuthSecureParams(pageToken, properAppSecret) + + const data = await Graph.getAsync(`/${userId}`, { + fields, + ...authSecureParams, + }) + return data +} + +const messageMethod = async (messageData, pageToken, appSecret) => { + const properAppSecret = appSecret || getAppSecret() + const authSecureParams = getAuthSecureParams(pageToken, properAppSecret) + + const data = await Graph.postAsync('/me/messages', { + ...authSecureParams, + ...messageData, + }) + return data +} + +export const facebookSendMessage = async (message, pageToken, appSecret) => { + await messageMethod(message, pageToken, appSecret) +} + +export const facebookSendIsTyping = async (recipientId, pageToken, appSecret) => { + const data = { + recipient: { id: recipientId }, + sender_action: 'typing_on', + } + await messageMethod(data, pageToken, appSecret) +} + +const getUserPagesRecursive = async (params, after = undefined) => { + const requestParams = _.omitBy({ + ...params, + after, + }, _.isNil) + + const { data, paging: { next, cursors: { after: afterCursor } } } + = await Graph.getAsync('/me/accounts', requestParams) + + if (next) { + return [ + ...data, + ...getUserPagesRecursive(params, afterCursor), + ] + } + return data +} + +export const facebookGetUserPages = async (clientToken) => { + const appSecret = getAppSecret() + const authSecureParams = getAuthSecureParams(clientToken, appSecret) + + const pages = await getUserPagesRecursive(authSecureParams) + return pages +} + +export const facebookGetPagesTokens = async (pageIds, clientToken) => { + const appSecret = getAppSecret() + const authSecureParams = getAuthSecureParams(clientToken, appSecret) + + const batchRequests = pageIds.reduce((acc, pageId) => { + return [ + ...acc, + { + method: 'GET', + relative_url: `${pageId}?fields=access_token`, + }, + ] + }, []) + + const response = await Graph.batchAsync(batchRequests, authSecureParams) + const filteredResponse = response.map(r => { + const body = r.body ? JSON.parse(r.body) : null + if (!body) { + throw new ServiceError('Error while getting facebook pages tokens') + } + return [body.id, body.access_token] + }) + return filteredResponse +} + +export const facebookGetPagesPictures = async (pageIds, appToken) => { + const appSecret = getAppSecret() + const authSecureParams = getAuthSecureParams(appToken, appSecret) + + const batchRequests = pageIds.reduce((acc, pageId) => { + return [ + ...acc, + { + method: 'GET', + relative_url: `${pageId}/picture?redirect=0`, + }, + ] + }, []) + + const response = await Graph.batchAsync(batchRequests, authSecureParams) + const filteredResponse = response.map(r => { + const body = r.body ? JSON.parse(r.body) : null + if (!body) { + throw new ServiceError('Error while getting facebook pages pictures') + } + return body.data + }) + return filteredResponse +} + +export const facebookAddAppToPage = async (pageId, pageToken) => { + const appSecret = getAppSecret() + const authSecureParams = getAuthSecureParams(pageToken, appSecret) + + const { success } = await Graph.postAsync(`/${pageId}/subscribed_apps`, { + ...authSecureParams, + }) + return success +} + +export const facebookRemoveAppFromPage = async (pageId, pageToken) => { + const appSecret = getAppSecret() + const authSecureParams = getAuthSecureParams(pageToken, appSecret) + + const { success } = await Graph.delAsync(`/${pageId}/subscribed_apps`, { + ...authSecureParams, + }) + return success +} + +export const facebookGetAppWebhookToken = () => config.facebook_app_webhook_token + +export const facebookComputeSignature = (rawBody, appSecret) => { + const properAppSecret = appSecret || getAppSecret() + + const hmac = createHmac('sha1', properAppSecret) + hmac.update(rawBody, 'utf-8') + const digest = hmac.digest('hex') + return `sha1=${digest}` +} + +export const facebookAddProfileProperties = async (properties, pageToken) => { + await Graph.postAsync(`/me/messenger_profile?access_token=${pageToken}`, { ...properties }) +} + +export const facebookDelProfileProperties = async (properties, pageToken) => { + const fields = { fields: properties } + await Graph.delAsync(`/me/messenger_profile?access_token=${pageToken}`, { ...fields }) + +} diff --git a/src/channel_integrations/facebook/test/integration.js b/src/channel_integrations/facebook/test/integration.js new file mode 100644 index 0000000..aa03ae7 --- /dev/null +++ b/src/channel_integrations/facebook/test/integration.js @@ -0,0 +1,504 @@ +import chai from 'chai' +import { setupChannelIntegrationTests } from '../../../../test/tools' +import nock from 'nock' +import _ from 'lodash' +import qs from 'qs' + +const expect = chai.expect +const should = chai.should() + +/* eslint max-nested-callbacks: 0 */ // --> OFF + +const channelCreationParams = { + type: 'messenger', + slug: 'my-awesome-channel', + apiKey: 'api-key', + serviceId: 'service-id', + token: 'token', +} + +describe('Facebook Messenger Channel', () => { + + const { createChannel, deleteChannel, + updateChannel, sendMessageToWebhook } = setupChannelIntegrationTests() + const facebookAPI = 'https://graph.facebook.com:443' + + beforeEach(async () => { + nock(facebookAPI).post('/v2.11/service-id/subscribed_apps').query(true).reply(200, {}) + }) + + describe('creation', () => { + it('should be successful with valid parameters', async () => { + const response = await createChannel(channelCreationParams) + const { results: result, message } = response.body + + expect(response.status).to.equal(201) + expect(message).to.equal('Channel successfully created') + expect(result.type).to.equal(channelCreationParams.type) + expect(result.slug).to.equal(channelCreationParams.slug) + /* eslint no-unused-expressions: 0 */ // --> OFF + expect(result.isErrored).to.be.false + expect(result.isActivated).to.be.true + }) + it('should return 400 with invalid parameters', async () => { + try { + await createChannel({}) + should.fail() + } catch (error) { + expect(error.status).to.equal(400) + } + }) + }) + + describe('Update', () => { + it('should be successful if channel exists', async () => { + let response = await createChannel(channelCreationParams) + const channel = response.body.results + const newValues = Object.assign({}, channelCreationParams) + newValues.token = 'newtoken' + nock(facebookAPI).post('/v2.11/service-id/subscribed_apps') + .times(2).query(true).reply(200, {}) + response = await updateChannel(channel, newValues) + expect(response.status).to.equal(200) + expect(response.body.results.token).to.equal(newValues.token) + }) + }) + + describe('Deletion', () => { + it('should be successful if channel exists', async () => { + let response = await createChannel(channelCreationParams) + const channel = response.body.results + response = await deleteChannel(channel) + expect(response.status).to.equal(200) + }) + }) + + describe('sending a message', () => { + + let channel + const senderId = 456 + const body = { + entry: [{ + messaging: [{ + message: { + text: 'my message', + }, + text: 'my message', + recipient: { id: channelCreationParams.serviceId }, + sender: { id: senderId }, + }], + }], + } + + beforeEach(async () => { + channel = (await createChannel(channelCreationParams)).body.results + }) + + // Hard-coded for testing purposes + const signature = 'sha1=a3a1f35918b7b8c8d187e75dfa793a843e5c2b3c' + const headers = { 'x-hub-signature': signature } + + it('should be successful with valid parameters and valid signature', async () => { + nock(facebookAPI).get('/v2.11/456').query(true).reply(200, { + id: senderId, + }) + const outgoingMessageCall = nock(facebookAPI).post('/v2.11/me/messages') + .times(2) + .reply(200, {}) + const outgoingMessageCallPromise = new Promise((resolve) => { + outgoingMessageCall.on('replied', () => { + if (outgoingMessageCall.isDone()) { + resolve() + } + }) + }) + const response = await sendMessageToWebhook(channel, body, headers) + expect(response.status).to.equal(200) + await outgoingMessageCallPromise + }) + + it('should return 401 with valid parameters, but invalid signature', async () => { + try { + const signature = 'sha1=invalid-signature' + const headers = { 'x-hub-signature': signature } + await sendMessageToWebhook(channel, body, headers) + should.fail() + } catch (error) { + expect(error.status).to.equal(401) + } + }) + + function mockIsTypingCall () { + const isTypingCall = nock(facebookAPI).post('/v2.11/me/messages', (requestBody) => { + const expectedQuery = { + access_token: channelCreationParams.token, + appsecret_proof: 'a0db4e58b0b2a5434a6f95e1ed797f311e42feee3c71f7c6f057a649ef1a8291', + recipient: { id: `${senderId}` }, + sender_action: 'typing_on', + } + return _.isEqual(expectedQuery, qs.parse(requestBody)) + }).query(true).reply(200, {}) + return isTypingCall + } + + describe('should be successful', () => { + + beforeEach(async () => { + nock(facebookAPI).get(`/v2.11/${senderId}`).query(true).reply(200, { + id: senderId, + }) + }) + + describe('in list format', () => { + // Facebook Messenger supports lists with 2 - 4 elements (2018-08-02) + + it('with too few elements') + + it('with supported length', async () => { + const listElement = { + title: 'title', + subtitle: 'subtitle', + imageUrl: 'https://img.url', + buttons: [ + { type: 'account_link', title: 'button title', value: 'https://link.com' }, + { type: 'web_url', title: 'button title', value: 'https://link.com' }, + { type: 'phone_number', title: 'button title', value: '0123554' }], + } + const isTypingCall = mockIsTypingCall() + const isTypingCallPromise = new Promise((resolve) => { + isTypingCall.on('replied', () => { + if (isTypingCall.isDone()) { + resolve() + } + }) + }) + const messageCall = nock(facebookAPI).post('/v2.11/me/messages', (requestBody) => { + const parsed = qs.parse(requestBody) + // https://developers.facebook.com/docs/messenger-platform/send-messages/template/list + const expectedQuery = { + access_token: channelCreationParams.token, + appsecret_proof: 'a0db4e58b0b2a5434a6f95e1ed797f311e42feee3c71f7c6f057a649ef1a8291', + messaging_type: 'RESPONSE', + recipient: { id: `${senderId}` }, + message: { + attachment: { + type: 'template', + payload: { + template_type: 'list', + elements: Array(2).fill({ + title: listElement.title, + subtitle: listElement.subtitle, + image_url: listElement.imageUrl, + buttons: { + // 'qs' module has trouble parsing FB's query body + '[0][title]': listElement.buttons[0].title, + '[0][type]': listElement.buttons[0].type, + '[0][url]': listElement.buttons[0].value, + '[1][title]': listElement.buttons[1].title, + '[1][type]': listElement.buttons[1].type, + '[1][url]': listElement.buttons[1].value, + '[2][title]': listElement.buttons[2].title, + '[2][type]': listElement.buttons[2].type, + '[2][payload]': listElement.buttons[2].value, + }, + }), + }, + }, + }, + } + return _.isEqual(expectedQuery, parsed) + }).query(true).reply(200, {}) + const messageCallPromise = new Promise((resolve) => { + messageCall.on('replied', () => { + if (messageCall.isDone()) { + resolve() + } + }) + }) + const botResponse = { + results: {}, + messages: JSON.stringify([{ + type: 'list', + content: { elements: Array(2).fill(listElement) }, + }]), + } + const response = await sendMessageToWebhook(channel, body, headers, botResponse) + expect(response.status).to.equal(200) + return Promise.all([isTypingCallPromise, messageCallPromise]) + }) + + it('with too many elements') + }) + + it('in card format', async () => { + const cardElement = { + title: 'title', + subtitle: 'subtitle', + imageUrl: 'https://img.url', + buttons: [{ type: '', title: 'button title', value: 'abc' }], + } + const isTypingCall = mockIsTypingCall() + const isTypingCallPromise = new Promise((resolve) => { + isTypingCall.on('replied', () => { + if (isTypingCall.isDone()) { + resolve() + } + }) + }) + const messageCall = nock(facebookAPI).post('/v2.11/me/messages', (requestBody) => { + const parsed = qs.parse(requestBody) + // https://developers.facebook.com/docs/messenger-platform/send-messages/template/list + const expectedQuery = { + access_token: channelCreationParams.token, + appsecret_proof: 'a0db4e58b0b2a5434a6f95e1ed797f311e42feee3c71f7c6f057a649ef1a8291', + messaging_type: 'RESPONSE', + recipient: { id: `${senderId}` }, + message: { + attachment: { + type: 'template', + payload: { + template_type: 'generic', + elements: [{ + title: cardElement.title, + subtitle: cardElement.subtitle, + image_url: cardElement.imageUrl, + buttons: { + // '[0][title]': cardElement.buttons[0].title, + '[0][type]': 'text', + }, + }], + }, + }, + }, + } + return _.isEqual(expectedQuery, parsed) + }).query(true).reply(200, {}) + const messageCallPromise = new Promise((resolve) => { + messageCall.on('replied', () => { + if (messageCall.isDone()) { + resolve() + } + }) + }) + const botResponse = { + results: {}, + messages: JSON.stringify([{ type: 'card', content: cardElement }]), + } + const response = await sendMessageToWebhook(channel, body, headers, botResponse) + expect(response.status).to.equal(200) + return Promise.all([isTypingCallPromise, messageCallPromise]) + }) + + it('in carousel format', async () => { + const carouselElement = { + title: 'title', + imageUrl: 'https://img.url', + buttons: [{ type: '', title: 'button title', value: 'abc' }], + } + const isTypingCall = mockIsTypingCall() + const isTypingCallPromise = new Promise((resolve) => { + isTypingCall.on('replied', () => { + if (isTypingCall.isDone()) { + resolve() + } + }) + }) + const messageCall = nock(facebookAPI).post('/v2.11/me/messages', (requestBody) => { + const parsed = qs.parse(requestBody) + // https://developers.facebook.com/docs/messenger-platform/send-messages/template/list + const expectedQuery = { + access_token: channelCreationParams.token, + appsecret_proof: 'a0db4e58b0b2a5434a6f95e1ed797f311e42feee3c71f7c6f057a649ef1a8291', + messaging_type: 'RESPONSE', + recipient: { id: `${senderId}` }, + message: { + attachment: { + type: 'template', + payload: { + template_type: 'generic', + elements: [{ + title: carouselElement.title, + image_url: carouselElement.imageUrl, + buttons: { + // '[0][title]': carouselElement.buttons[0].title, + '[0][type]': 'text', + }, + }], + }, + }, + }, + } + return _.isEqual(expectedQuery, parsed) + }).query(true).reply(200, {}) + const messageCallPromise = new Promise((resolve) => { + messageCall.on('replied', () => { + if (messageCall.isDone()) { + resolve() + } + }) + }) + const botResponse = { + results: {}, + messages: JSON.stringify([{ type: 'carousel', content: [carouselElement] }]), + } + const response = await sendMessageToWebhook(channel, body, headers, botResponse) + expect(response.status).to.equal(200) + return Promise.all([isTypingCallPromise, messageCallPromise]) + }) + + it('in quickReplies format', async () => { + const quickReplies = { + title: 'title', + buttons: [ + { type: '', title: 'button title', value: 'abc' }, + { type: 'postback', title: '1991 - 1996', value: '1991 - 1996' }, + ], + } + const isTypingCall = mockIsTypingCall() + const isTypingCallPromise = new Promise((resolve) => { + isTypingCall.on('replied', () => { + if (isTypingCall.isDone()) { + resolve() + } + }) + }) + const messageCall = nock(facebookAPI).post('/v2.11/me/messages', (requestBody) => { + const parsed = qs.parse(requestBody) + // https://developers.facebook.com/docs/messenger-platform/send-messages/template/list + const expectedQuery = { + access_token: channelCreationParams.token, + appsecret_proof: 'a0db4e58b0b2a5434a6f95e1ed797f311e42feee3c71f7c6f057a649ef1a8291', + messaging_type: 'RESPONSE', + recipient: { id: `${senderId}` }, + message: { + text: quickReplies.title, + quick_replies: [ + { + content_type: 'text', + title: quickReplies.buttons[0].title, + payload: quickReplies.buttons[0].value, + }, + { + content_type: 'text', + title: quickReplies.buttons[1].title, + payload: quickReplies.buttons[1].value, + }, + ], + }, + } + return _.isEqual(expectedQuery, parsed) + }).query(true).reply(200, {}) + const messageCallPromise = new Promise((resolve) => { + messageCall.on('replied', () => { + if (messageCall.isDone()) { + resolve() + } + }) + }) + const botResponse = { + results: {}, + messages: JSON.stringify([{ type: 'quickReplies', content: quickReplies }]), + } + const response = await sendMessageToWebhook(channel, body, headers, botResponse) + expect(response.status).to.equal(200) + return Promise.all([isTypingCallPromise, messageCallPromise]) + }) + + it('in video format', async () => { + const videoLink = 'https://link.com' + const isTypingCall = mockIsTypingCall() + const isTypingCallPromise = new Promise((resolve) => { + isTypingCall.on('replied', () => { + if (isTypingCall.isDone()) { + resolve() + } + }) + }) + const messageCall = nock(facebookAPI).post('/v2.11/me/messages', (requestBody) => { + const parsed = qs.parse(requestBody) + // https://developers.facebook.com/docs/messenger-platform/send-messages/template/list + const expectedQuery = { + access_token: channelCreationParams.token, + appsecret_proof: 'a0db4e58b0b2a5434a6f95e1ed797f311e42feee3c71f7c6f057a649ef1a8291', + messaging_type: 'RESPONSE', + recipient: { id: `${senderId}` }, + message: { + attachment: { + type: 'video', + payload: { url: videoLink }, + }, + }, + } + return _.isEqual(expectedQuery, parsed) + }).query(true).reply(200, {}) + const messageCallPromise = new Promise((resolve) => { + messageCall.on('replied', () => { + if (messageCall.isDone()) { + resolve() + } + }) + }) + const botResponse = { + results: {}, + messages: JSON.stringify([{ type: 'video', content: videoLink }]), + } + const response = await sendMessageToWebhook(channel, body, headers, botResponse) + expect(response.status).to.equal(200) + return Promise.all([isTypingCallPromise, messageCallPromise]) + }) + + it('in buttons format', async () => { + const buttonsElement = { + title: 'title', + buttons: [{ type: 'text', title: 'button title', value: 'abc' }], + } + const isTypingCall = mockIsTypingCall() + const isTypingCallPromise = new Promise((resolve) => { + isTypingCall.on('replied', () => { + if (isTypingCall.isDone()) { + resolve() + } + }) + }) + const messageCall = nock(facebookAPI).post('/v2.11/me/messages', (requestBody) => { + const parsed = qs.parse(requestBody) + // https://developers.facebook.com/docs/messenger-platform/send-messages/template/list + const expectedQuery = { + access_token: channelCreationParams.token, + appsecret_proof: 'a0db4e58b0b2a5434a6f95e1ed797f311e42feee3c71f7c6f057a649ef1a8291', + messaging_type: 'RESPONSE', + recipient: { id: `${senderId}` }, + message: { + attachment: { + type: 'template', + payload: { + template_type: 'button', + text: buttonsElement.title, + buttons: [{ + type: 'text', + }], + }, + }, + }, + } + return _.isEqual(expectedQuery, parsed) + }).query(true).reply(200, {}) + const messageCallPromise = new Promise((resolve) => { + messageCall.on('replied', () => { + if (messageCall.isDone()) { + resolve() + } + }) + }) + const botResponse = { + results: {}, + messages: JSON.stringify([{ type: 'buttons', content: buttonsElement }]), + } + const response = await sendMessageToWebhook(channel, body, headers, botResponse) + expect(response.status).to.equal(200) + return Promise.all([isTypingCallPromise, messageCallPromise]) + }) + }) + + }) +}) diff --git a/src/channel_integrations/index.js b/src/channel_integrations/index.js new file mode 100644 index 0000000..71f2864 --- /dev/null +++ b/src/channel_integrations/index.js @@ -0,0 +1,77 @@ +import AmazonAlexa from './amazon_alexa/channel' +import Callr from './callr/channel' +import CiscoSpark from './cisco_spark/channel' +import Kik from './kik/channel' +import Line from './line/channel' +import Messenger from './facebook/channel' +import Microsoft from './microsoft/channel' +import Slack from './slack/channel' +import SlackApp from './slack_app/channel' +import Telegram from './telegram/channel' +import Twilio from './twilio/channel' +import Twitter from './twitter/channel' +import Webchat from './webchat/channel' + +export default { + AmazonAlexa, + Callr, + CiscoSpark, + Kik, + Line, + Messenger, + Microsoft, + Slack, + SlackApp, + Telegram, + Twilio, + Twitter, + Webchat, +} +/** + * Lists the names of all available channel integration modules + * @type {string[]} + */ +export const MODULES = [ + 'amazon_alexa', + 'callr', + 'cisco_spark', + 'facebook', + 'kik', + 'line', + 'microsoft', + 'slack', + 'slack_app', + 'telegram', + 'twilio', + 'twitter', + 'webchat', +] + +function getChannelModules () { + return MODULES.map(moduleName => require(`./${moduleName}`)) +} + +/** + * Collects all custom routes defined in channel integrations. + * @return {Route[]} Array of route objects + */ +export function getChannelIntegrationRoutes () { + const routeLists = getChannelModules() + .map(module => module.routes || []) + .filter(routes => routes.length) + return [].concat(...routeLists) +} + +/** + * Retrieves an instance of a channel integration for a given identifier. + * @param {string} identifier One of the channel integration identifier + * @return {AbstractChannelIntegration} An instance of the channel integration matching + * the given identifier + */ +export function getChannelIntegrationByIdentifier (identifier) { + const module = getChannelModules().find(module => module.identifiers.includes(identifier)) + if (!module) { + return undefined + } + return new module.channel() +} diff --git a/src/channel_integrations/kik/channel.js b/src/channel_integrations/kik/channel.js new file mode 100644 index 0000000..6e5d6c7 --- /dev/null +++ b/src/channel_integrations/kik/channel.js @@ -0,0 +1,211 @@ +import _ from 'lodash' +import request from 'superagent' + +import { logger, arrayfy } from '../../utils' +import AbstractChannelIntegration from '../abstract_channel_integration' +import { BadRequestError, ForbiddenError } from '../../utils/errors' + +const agent = require('superagent-promise')(request, Promise) + +export default class Kik extends AbstractChannelIntegration { + + validateChannelObject (channel) { + if (!channel.apiKey) { + throw new BadRequestError('Parameter apiKey is missing') + } else if (!channel.userName) { + throw new BadRequestError('Parameter userName is missing') + } + } + + async beforeChannelCreated (channel) { + const data = { + webhook: channel.webhook, + features: { + receiveReadReceipts: false, + receiveIsTyping: false, + manuallySendReadReceipts: false, + receiveDeliveryReceipts: false, + }, + } + + try { + await agent.post('https://api.kik.com/v1/config') + .auth(channel.userName, channel.apiKey) + .send(data) + channel.isErrored = false + } catch (err) { + channel.isErrored = true + throw new BadRequestError('Invalid user name or API key') + } + } + + afterChannelUpdated (channel) { + return this.beforeChannelCreated(channel) + } + + authenticateWebhookRequest (req, res, channel) { + if (req.headers['x-kik-username'] !== channel.userName) { + throw new ForbiddenError() + } + } + + populateMessageContext (req) { + return { + chatId: _.get(req, 'body.messages[0].chatId'), + senderId: _.get(req, 'body.messages[0].participants[0]'), + } + } + + async onIsTyping (channel, context) { + const message = { + to: context.senderId, + chatId: context.chatId, + type: 'is-typing', + isTyping: true, + } + + return agent('POST', 'https://api.kik.com/v1/message') + .auth(channel.userName, channel.apiKey) + .send({ messages: [message] }) + } + + parseIncomingMessage (conversation, message) { + message = _.get(message, 'messages[0]', {}) + const msg = { attachment: {}, channelType: 'kik' } + + switch (message.type) { + case 'text': + msg.attachment = { type: 'text', content: message.body } + break + case 'link': + msg.attachment = { type: 'link', content: message.url } + break + case 'picture': + msg.attachment = { type: 'picture', content: message.picUrl } + break + case 'video': + msg.attachment = { type: 'video', content: message.videoUrl } + break + default: + throw new BadRequestError('Message non-supported by Kik') + } + + return msg + } + + static formatButtons (buttons = []) { + return { + type: 'suggested', + responses: buttons.map(b => ({ type: 'text', body: b.title })), + } + } + + formatOutgoingMessage (conversation, message, opts) { + const content = _.get(message, 'attachment.content') + const type = _.get(message, 'attachment.type') + const msg = { chatId: opts.chatId, to: opts.senderId, type } + + switch (type) { + case 'text': + return { ...msg, body: content } + case 'picture': + return { ...msg, picUrl: content } + case 'video': + return { ...msg, videoUrl: content } + case 'list': { + const replies = content.elements.map(elem => { + return { + ...msg, + type: 'text', + body: `\n${elem.title}\n${elem.subtitle}\n${elem.imageUrl}`, + } + }) + + const keyboard = Kik.formatButtons([].concat(...content.elements.map(elem => elem.buttons))) + replies[replies.length - 1].keyboards = [keyboard] + return replies + } + case 'buttons': + case 'quickReplies': { + const keyboard = Kik.formatButtons(content.buttons) + return { + ...msg, + type: 'text', + body: content.title, + keyboards: [keyboard], + } + } + case 'card': { + const replies = [] + replies.push({ ...msg, type: 'text', body: content.title }) + + if (content.imageUrl) { + replies.push({ ...msg, type: 'picture', picUrl: content.imageUrl }) + } + + const keyboard = Kik.formatButtons(content.buttons) + replies[replies.length - 1].keyboards = [keyboard] + return replies + } + case 'carousel': + case 'carouselle': { + const replies = [] + + for (const card of content) { + replies.push({ ...msg, type: 'text', body: card.title }) + + if (card.imageUrl) { + replies.push({ ...msg, type: 'picture', picUrl: card.imageUrl }) + } + } + + const buttons = [].concat.apply([], content.map(c => c.buttons)) + const keyboard = Kik.formatButtons(buttons) + replies[replies.length - 1].keyboards = [keyboard] + return replies + } + case 'custom': { + return _.map(content, ({ type, ...replyProps }) => ({ ...replyProps, ...msg, type })) + } + default: + throw new BadRequestError('Message type non-supported by Kik') + } + } + + async sendMessage (conversation, messages) { + for (const message of arrayfy(messages)) { + await agent('POST', 'https://api.kik.com/v1/message') + .auth(conversation.channel.userName, conversation.channel.apiKey) + .send({ messages: [message] }) + } + } + + populateParticipantData (participant, channel) { + return new Promise(async (resolve, reject) => { + request.get(`https://api.kik.com/v1/user/${participant.senderId}`) + .auth(channel.userName, channel.apiKey) + .end((err, result) => { + if (err) { + logger.error(`[Kik] Error when retrieving user info: ${err}`) + return reject(err) + } + + participant.data = result.body + participant.markModified('data') + + participant.save().then(resolve).catch(reject) + }) + }) + } + + parseParticipantDisplayName (participant) { + const informations = {} + + if (participant.data) { + const { firstName, lastName } = participant.data + informations.userName = `${firstName} ${lastName}` + } + + return informations + } +} diff --git a/src/channel_integrations/kik/index.js b/src/channel_integrations/kik/index.js new file mode 100644 index 0000000..ee1f28e --- /dev/null +++ b/src/channel_integrations/kik/index.js @@ -0,0 +1,3 @@ +import channel from './channel' + +module.exports = { channel, identifiers: ['kik'] } diff --git a/src/channel_integrations/kik/test/integration.js b/src/channel_integrations/kik/test/integration.js new file mode 100644 index 0000000..d855e74 --- /dev/null +++ b/src/channel_integrations/kik/test/integration.js @@ -0,0 +1,279 @@ +import chai from 'chai' +import { setupChannelIntegrationTests } from '../../../../test/tools' +import nock from 'nock' + +const expect = chai.expect +const should = chai.should() + +const channelCreationParams = { + type: 'kik', + slug: 'my-awesome-channel', + userName: 'abcdefg-alias-id', + apiKey: 'oauth-token', +} + +describe('Kik channel', () => { + + const { createChannel, updateChannel, + deleteChannel, sendMessageToWebhook } = setupChannelIntegrationTests() + const kikAPI = 'https://api.kik.com' + beforeEach(() => { + nock(kikAPI).post('/v1/config').reply(200, {}) + }) + + describe('Creation', () => { + it('should be successful with valid parameters', async () => { + const response = await createChannel(channelCreationParams) + const { results: result, message } = response.body + + expect(response.status).to.equal(201) + expect(message).to.equal('Channel successfully created') + expect(result.type).to.equal(channelCreationParams.type) + expect(result.slug).to.equal(channelCreationParams.slug) + /* eslint no-unused-expressions: 0 */ // --> OFF + expect(result.isErrored).to.be.false + expect(result.isActivated).to.be.true + }) + it('should return 400 with invalid parameters', async () => { + try { + await createChannel({}) + should.fail() + } catch (error) { + expect(error.status).to.equal(400) + } + }) + }) + + describe('Update', () => { + it('should be successful if channel exists', async () => { + let response = await createChannel(channelCreationParams) + const channel = response.body.results + const newValues = Object.assign({}, channelCreationParams) + newValues.token = 'newtoken' + nock(kikAPI).post('/v1/config').reply(200, {}) + response = await updateChannel(channel, newValues) + expect(response.status).to.equal(200) + expect(response.body.results.token).to.equal(newValues.token) + }) + }) + + describe('Deletion', () => { + it('should be successful if channel exists', async () => { + let response = await createChannel(channelCreationParams) + const channel = response.body.results + response = await deleteChannel(channel) + expect(response.status).to.equal(200) + }) + }) + + describe('sending a message', () => { + + let channel + + beforeEach(async () => { + channel = (await createChannel(channelCreationParams)).body.results + }) + + it('should return 401 with invalid token', async () => { + try { + const headers = { 'x-kik-username': 'invalid-token' } + await sendMessageToWebhook(channel, { message: 'message' }, headers) + should.fail() + } catch (error) { + expect(error.status).to.equal(401) + } + }) + + it('should return 401 without token', async () => { + try { + await sendMessageToWebhook(channel, {}) + should.fail() + } catch (error) { + expect(error.status).to.equal(401) + } + }) + + it('should be successful with valid parameters', async () => { + const senderId = 'sender-id' + const message = { + messages: [{ + type: 'text', + body: 'a message', + chatId: 123, + participants: [senderId] }], + } + // isTyping call + nock('https://api.kik.com').post('/v1/message').reply(200, {}) + nock('https://api.kik.com').get(`/v1/user/${senderId}`).times(2).reply(200, {}) + const outgoingMessageCall = nock('https://api.kik.com').post('/v1/message').reply(200, {}) + const headers = { 'x-kik-username': channelCreationParams.userName } + const response = await sendMessageToWebhook(channel, message, headers) + expect(response.status).to.equal(200) + expect(outgoingMessageCall.isDone()).to.be.true + }) + + it('should be successful for card message', async () => { + const senderId = 'sender-id' + const chatId = 123 + const message = { + messages: [{ + type: 'text', + body: 'a message', + chatId, + participants: [senderId], + }], + } + + // isTyping call + nock('https://api.kik.com').post('/v1/message').reply(200, {}) + nock('https://api.kik.com').get(`/v1/user/${senderId}`).times(2).reply(200, {}) + const firstOutgoingMessage = nock('https://api.kik.com') + .post('/v1/message', { + messages: [ + { + chatId, + to: senderId, + type: 'text', + body: 'title', + }, + ], + }) + .basicAuth({ + user: channel.userName, + pass: channel.apiKey, + }) + .reply(200, {}) + const secondOutgoingMessage = nock('https://api.kik.com') + .post('/v1/message', { + messages: [ + { + chatId, + type: 'picture', + to: senderId, + picUrl: 'https://img.url', + keyboards: [ + { + type: 'suggested', + responses: [ + { + type: 'text', + body: 'button title', + }, + ], + }, + ], + }, + ], + }) + .basicAuth({ + user: channel.userName, + pass: channel.apiKey, + }) + .reply(200, {}) + const headers = { 'x-kik-username': channelCreationParams.userName } + const botResponse = { + results: {}, + messages: JSON.stringify([{ + type: 'card', + content: { + title: 'title', + subtitle: 'subtitle', + imageUrl: 'https://img.url', + buttons: [{ type: '', title: 'button title', value: 'abc' }], + }, + }]), + } + const response = await sendMessageToWebhook(channel, message, headers, botResponse) + expect(response.status).to.equal(200) + expect(firstOutgoingMessage.isDone()).to.be.true + expect(secondOutgoingMessage.isDone()).to.be.true + }) + + it('should be successful for list message', async () => { + const senderId = 'sender-id' + const chatId = 123 + const message = { + messages: [{ + type: 'text', + body: 'a message', + chatId, + participants: [senderId], + }], + } + // isTyping call + nock('https://api.kik.com').post('/v1/message').reply(200, {}) + nock('https://api.kik.com').get(`/v1/user/${senderId}`).times(2).reply(200, {}) + const firstOutgoingMessage = nock('https://api.kik.com') + .post('/v1/message', { + messages: [ + { + chatId, + type: 'text', + to: senderId, + body: '\ntitle\nsubtitle\nhttps://img.url', + }, + ], + }) + .basicAuth({ + user: channel.userName, + pass: channel.apiKey, + }) + .reply(200, {}) + const secondOutgoingMessage = nock('https://api.kik.com') + .post('/v1/message', { + messages: [ + { + chatId, + type: 'text', + to: senderId, + body: '\nSecond title\nSecond subtitle\nhttps://img.url', + keyboards: [ + { + type: 'suggested', + responses: [ + { + type: 'text', + body: 'button title', + }, + ], + }, + ], + }, + ], + }) + .basicAuth({ + user: channel.userName, + pass: channel.apiKey, + }) + .reply(200, {}) + const headers = { 'x-kik-username': channelCreationParams.userName } + const botResponse = { + results: {}, + messages: JSON.stringify([{ + type: 'list', + content: { + elements: [ + { + title: 'title', + subtitle: 'subtitle', + imageUrl: 'https://img.url', + buttons: [{ type: '', title: 'button title', value: 'abc' }], + }, + { + title: 'Second title', + subtitle: 'Second subtitle', + imageUrl: 'https://img.url', + buttons: [], + }, + ], + }, + }]), + } + const response = await sendMessageToWebhook(channel, message, headers, botResponse) + expect(response.status).to.equal(200) + expect(firstOutgoingMessage.isDone()).to.be.true + expect(secondOutgoingMessage.isDone()).to.be.true + }) + + }) +}) diff --git a/src/channel_integrations/line/channel.js b/src/channel_integrations/line/channel.js new file mode 100644 index 0000000..521ba66 --- /dev/null +++ b/src/channel_integrations/line/channel.js @@ -0,0 +1,270 @@ +import _ from 'lodash' +import { createHmac } from 'crypto' + +import AbstractChannelIntegration from '../abstract_channel_integration' +import { + lineSendMessage, + lineGetUserProfile, + BadRequestError, + ForbiddenError, + StopPipeline, +} from '../../utils' + +const labelCharacterLimit = 20 +const carouselLabelCharacteLimit = 12 + +export default class Line extends AbstractChannelIntegration { + + validateChannelObject (channel) { + if (!channel.token || !channel.clientSecret) { + throw new BadRequestError('Parameter token or clientSecret is missing') + } + } + + authenticateWebhookRequest (req, res, channel) { + const signature = _.get(req, ['headers', 'x-line-signature']) + const rawBody = _.get(req, 'rawBody') + const channelSecret = _.get(channel, 'clientSecret') + + const computedSignature = createHmac('SHA256', channelSecret) + .update(rawBody) + .digest('base64') + + if (signature !== computedSignature) { + throw new ForbiddenError() + } + } + + onWebhookCalled (req, res, channel) { + if (_.get(req, 'body.events[0].replyToken') === '00000000000000000000000000000000') { + // webhook verification request, reply with success, don't send a message + throw new StopPipeline() + } + + return channel + } + + populateMessageContext (req) { + const sourceType = _.get(req, 'body.events[0].source.type') + const recipientId + = sourceType === 'user' ? '' : _.get(req, `body.events[0].source.${sourceType}Id`) + const senderId = _.get(req, 'body.events[0].source.userId', recipientId) + + return { + chatId: `${recipientId}-${senderId}`, + senderId, + } + } + + updateConversationContextFromMessage (conversation, message) { + const eventType = _.get(message, 'events[0].type') + if (eventType !== 'message' && eventType !== 'postback') { + throw new StopPipeline() + } + + const replyToken = _.get(message, 'events[0].replyToken') + conversation.replyToken = replyToken + conversation.markModified('replyToken') + + return conversation + } + + parseIncomingMessage (conversation, message) { + const msg = {} + const eventType = _.get(message, 'events[0].type') + + if (eventType === 'message') { + message = _.get(message, 'events[0].message') + const type = _.get(message, 'type') + + if (type === 'text') { + msg.attachment = { + type: 'text', + content: message.text, + } + } else if (type === 'image') { + msg.attachment = { + type: 'picture', + content: message.id, + } + } else if (type === 'video') { + msg.attachment = { + type: 'video', + content: message.id, + } + } else { + throw new BadRequestError('Message type non-supported by Line') + } + } else if (eventType === 'postback') { + const content = _.get(message, 'postback') + msg.attachment = { type: 'payload', content } + } + + return msg + } + + static formatButtons (buttons, characterLimit = labelCharacterLimit) { + return buttons.map(button => { + const { title: label } = button + const type = button.type || 'text' + const value = button.value || button.url + + // Line is restrictive in terms of label length. Different lengths + // are allowed for carousel (12 chars) vs. other templates (e.g. buttons) + // see https://developers.line.me/en/reference/messaging-api/#action-objects + if (['text', 'phonenumber', 'element_share'].indexOf(type) !== -1) { + return { type: 'message', label: label.slice(0, characterLimit), text: value } + } else if (type === 'web_url') { + return { type: 'uri', label: label.slice(0, characterLimit), uri: value } + } else if (type === 'postback') { + return { type, label: label.slice(0, characterLimit), data: value, text: value } + } + return { type } + }) + } + + formatOutgoingMessage (conversation, message) { + const { type, content } = _.get(message, 'attachment') + + if (type === 'text') { + return { + type, + text: content, + } + } else if (type === 'picture') { + return { + type: 'image', + originalContentUrl: content, + previewImageUrl: content, + } + } else if (type === 'video') { + return { + type: 'video', + originalContentUrl: content, + // needs preview image + previewImageUrl: + 'https://portfolium.cloudimg.io/s/crop/128x128/' + + 'https://cdn.portfolium.com/img%2Fdefaults%2Fdefault.jpg', + } + } else if (type === 'card') { + const { title, imageUrl: thumbnailImageUrl, subtitle: text, buttons } = content + const actions = Line.formatButtons(buttons) + + return { + type: 'template', + altText: title, + template: { + type: 'buttons', + thumbnailImageUrl, + title, + text, + actions, + }, + } + } else if (type === 'quickReplies' || type === 'buttons') { + const templateType = type === 'buttons' ? type : 'confirm' + const { title, buttons } = content + const actions = Line.formatButtons(buttons) + + return { + type: 'template', + altText: title, + template: { + type: templateType, + text: title, + actions, + }, + } + } else if (type === 'list') { + const { elements, buttons } = content + const actions = Line.formatButtons(buttons) + + return _.map(elements, ({ title, imageUrl: thumbnailImageUrl, subtitle: text, buttons }) => { + const actions = Line.formatButtons(buttons) + return { + type: 'template', + altText: title, + template: { + type: 'buttons', + thumbnailImageUrl, + title, + text, + actions, + }, + } + }) + .concat([{ + type: 'template', + altText: 'actions', + template: { + type: 'confirm', + text: 'Confirm', + actions, + }, + }]) + } else if (type === 'carousel' || type === 'carouselle') { + const elements = _.map(content, ({ title, + imageUrl: thumbnailImageUrl, + subtitle: text, + buttons }) => { + const actions = Line.formatButtons(buttons, carouselLabelCharacteLimit) + return { + thumbnailImageUrl, + title, + text, + actions, + } + }) + + return { + type: 'template', + altText: 'carousel', + template: { + type: 'carousel', + columns: elements, + }, + } + } else if (type === 'custom') { + return content + } + + throw new BadRequestError('Message type non-supported by Line') + } + + async sendMessages (conversation, messages) { + await lineSendMessage(conversation.channel.token, conversation.replyToken, messages) + return true + } + + async sendMessage (conversation, message) { + await lineSendMessage(conversation.channel.token, conversation.replyToken, [message]) + return true + } + + /* + * Gromit methods + */ + + async populateParticipantData (participant, channel) { + try { + const data = await lineGetUserProfile(channel.token, participant.senderId) + + participant.data = data + return participant.save() + } catch (error) { + return participant + } + } + + parseParticipantDisplayName (participant) { + const informations = {} + + if (participant.data) { + const { displayName } = participant.data + informations.userName = displayName + } + + return informations + } + +} diff --git a/src/channel_integrations/line/index.js b/src/channel_integrations/line/index.js new file mode 100644 index 0000000..2856d39 --- /dev/null +++ b/src/channel_integrations/line/index.js @@ -0,0 +1,3 @@ +import channel from './channel' + +module.exports = { channel, identifiers: ['line'] } diff --git a/src/channel_integrations/line/test/integration.js b/src/channel_integrations/line/test/integration.js new file mode 100644 index 0000000..0273173 --- /dev/null +++ b/src/channel_integrations/line/test/integration.js @@ -0,0 +1,255 @@ +import chai from 'chai' +import { setupChannelIntegrationTests } from '../../../../test/tools' +import nock from 'nock' + +/* eslint max-nested-callbacks: 0 */ // --> OFF + +const expect = chai.expect +const should = chai.should() + +const lineAPI = 'https://api.line.me' + +const channelCreationParams = { + type: 'line', + slug: 'my-awesome-channel', + clientSecret: 'client-secret', + token: 'token', +} + +describe('Line channel', () => { + + const { createChannel, updateChannel, + deleteChannel, sendMessageToWebhook } = setupChannelIntegrationTests() + + describe('Creation', () => { + it('should be successful with valid parameters', async () => { + const response = await createChannel(channelCreationParams) + const { results: result, message } = response.body + + expect(response.status).to.equal(201) + expect(message).to.equal('Channel successfully created') + expect(result.type).to.equal(channelCreationParams.type) + expect(result.slug).to.equal(channelCreationParams.slug) + /* eslint no-unused-expressions: 0 */ // --> OFF + expect(result.isErrored).to.be.false + expect(result.isActivated).to.be.true + }) + + it('should return 400 with invalid parameters', async () => { + try { + await createChannel({}) + should.fail() + } catch (error) { + expect(error.status).to.equal(400) + } + }) + }) + + describe('Update', () => { + it('should be successful if channel exists', async () => { + let response = await createChannel(channelCreationParams) + const channel = response.body.results + const newValues = Object.assign({}, channelCreationParams) + newValues.token = 'newtoken' + response = await updateChannel(channel, newValues) + expect(response.status).to.equal(200) + expect(response.body.results.token).to.equal(newValues.token) + }) + }) + + describe('Deletion', () => { + it('should be successful if channel exists', async () => { + let response = await createChannel(channelCreationParams) + const channel = response.body.results + response = await deleteChannel(channel) + expect(response.status).to.equal(200) + }) + }) + + describe('webhook url verification', () => { + let channel + + const body = { + events: [ + { + replyToken: '00000000000000000000000000000000', + type: 'message', + timestamp: 1533154101667, + source: { + type: 'user', + userId: 'Udeadbeefdeadbeefdeadbeefdeadbeef', + }, + message: { + id: '100001', + type: 'text', + text: 'Hello, world', + }, + }, + { + replyToken: 'ffffffffffffffffffffffffffffffff', + type: 'message', + timestamp: 1533154101667, + source: { + type: 'user', + userId: 'Udeadbeefdeadbeefdeadbeefdeadbeef', + }, + message: { + id: 100002, + type: 'sticker', + packageId: '1', + stickerId: '1', + }, + }, + ], + } + const headers = { 'x-line-signature': 'AziZnbmnjOTxmYKhlJrQLeFfmuybTQmywm7p5dT5UEc=' } + + beforeEach(async () => { + channel = (await createChannel(channelCreationParams)).body.results + }) + + it('should be successful and not send any message', async () => { + const response = await sendMessageToWebhook(channel, body, headers) + expect(response.status).to.equal(200) + }) + }) + + describe('sending a message', () => { + + let channel + + beforeEach(async () => { + channel = (await createChannel(channelCreationParams)).body.results + }) + + const body = { events: [{ + type: 'message', + source: { type: 'user', userId: 'userid' }, + message: { type: 'text', text: 'a test message' }, + }] } + const headers = { 'x-line-signature': 'syDEWaFckSt/T1qAy0R/WCVWygSSOWcJrPs61aGFdIg=' } + + it('should be successful with valid token', async () => { + nock(lineAPI).get('/v2/bot/profile/userid').reply(200, {}) + const outgoingMessageCall = nock(lineAPI).post('/v2/bot/message/reply').reply(200, {}) + const response = await sendMessageToWebhook(channel, body, headers) + expect(response.status).to.equal(200) + expect(outgoingMessageCall.isDone()).to.be.true + }) + + it('should return 401 with invalid token', async () => { + try { + const headers = { 'x-twilio-signature': 'invalid' } + await sendMessageToWebhook(channel, {}, headers) + should.fail() + } catch (error) { + expect(error.status).to.equal(401) + } + }) + + it('should return 401 without a token', async () => { + try { + await sendMessageToWebhook(channel, {}) + should.fail() + } catch (error) { + expect(error.status).to.equal(401) + } + }) + + describe('should be successful', () => { + + beforeEach(async () => { + nock(lineAPI).get('/v2/bot/profile/userid').reply(200, {}) + nock(lineAPI).post('/v2/bot/message/reply').reply(200, {}) + + }) + + it('in list format', async () => { + const buttons = [ + { type: 'account_link', title: 'button title', value: 'https://link.com' }, + { type: 'web_url', title: 'button title', value: 'https://link.com' }, + { type: 'phone_number', title: 'button title', value: '0123554' }] + const listElement = { + title: 'title', + subtitle: 'subtitle', + imageUrl: 'https://img.url', + buttons, + } + const botResponse = { + results: {}, + messages: JSON.stringify([{ + type: 'list', + content: { elements: [listElement], buttons }, + }]), + } + const response = await sendMessageToWebhook(channel, body, headers, botResponse) + expect(response.status).to.equal(200) + }) + + it('in card format', async () => { + const cardElement = { + title: 'title', + subtitle: 'subtitle', + imageUrl: 'https://img.url', + buttons: [{ type: '', title: 'button title', value: 'abc' }], + } + const botResponse = { + results: {}, + messages: JSON.stringify([{ type: 'card', content: cardElement }]), + } + const response = await sendMessageToWebhook(channel, body, headers, botResponse) + expect(response.status).to.equal(200) + }) + + it('in carousel format', async () => { + const carouselElement = { + title: 'title', + imageUrl: 'https://img.url', + buttons: [{ type: '', title: 'button title', value: 'abc' }], + } + const botResponse = { + results: {}, + messages: JSON.stringify([{ type: 'carousel', content: [carouselElement] }]), + } + const response = await sendMessageToWebhook(channel, body, headers, botResponse) + expect(response.status).to.equal(200) + }) + + it('in quickReplies format', async () => { + const buttonsElement = { + title: 'title', + buttons: [{ type: '', title: 'button title', value: 'abc' }], + } + const botResponse = { + results: {}, + messages: JSON.stringify([{ type: 'quickReplies', content: buttonsElement }]), + } + const response = await sendMessageToWebhook(channel, body, headers, botResponse) + expect(response.status).to.equal(200) + }) + + it('in video format', async () => { + const botResponse = { + results: {}, + messages: JSON.stringify([{ type: 'video', content: 'https://link.com' }]), + } + const response = await sendMessageToWebhook(channel, body, headers, botResponse) + expect(response.status).to.equal(200) + }) + + it('in buttons format', async () => { + const buttonsElement = { + title: 'title', + buttons: [{ type: '', title: 'button title', value: 'abc' }], + } + const botResponse = { + results: {}, + messages: JSON.stringify([{ type: 'buttons', content: buttonsElement }]), + } + const response = await sendMessageToWebhook(channel, body, headers, botResponse) + expect(response.status).to.equal(200) + }) + }) + + }) +}) diff --git a/src/channel_integrations/microsoft/channel.js b/src/channel_integrations/microsoft/channel.js new file mode 100644 index 0000000..766859c --- /dev/null +++ b/src/channel_integrations/microsoft/channel.js @@ -0,0 +1,182 @@ +import _ from 'lodash' +import url from 'url' +import { + Message, HeroCard, CardAction, + CardImage, ThumbnailCard, AttachmentLayout } from 'botbuilder' + +import AbstractChannelIntegration from '../abstract_channel_integration' +import { logger } from '../../utils' +import { BadRequestError, StopPipeline } from '../../utils/errors' +import { microsoftParseMessage, microsoftGetBot, microsoftMakeAttachement } from './utils' +import { formatMarkdownHelper } from '../../utils/utils' + +const VIDEO_AS_LINK_HOSTS = [ + 'youtube.com', + 'youtu.be', +] + +export default class MicrosoftTemplate extends AbstractChannelIntegration { + + validateChannelObject (channel) { + const params = ['clientId', 'clientSecret'] + params.forEach(param => { + if (!channel[param] || typeof channel[param] !== 'string') { + throw new BadRequestError('Bad parameter '.concat(param).concat(' : missing or not string')) + } + }) + } + + validateWebhookSubscriptionRequest (req, res) { + res.status(200).send() + } + + async populateMessageContext (req, res, channel) { + const { session, message } = await microsoftParseMessage(channel, req) + + return { + session, + message, + chatId: message.address.conversation.id, + senderId: message.user.id, + } + } + + onIsTyping (channel, context) { + context.session.sendTyping() + } + + getRawMessage (channel, req, context) { + return context.message + } + + updateConversationContextFromMessage (conversation, message) { + conversation.microsoftAddress = message.address + conversation.markModified('microsoftAddress') + return conversation + } + + parseIncomingMessage (conversation, message) { + const msg = {} + const attachment = _.get(message, 'attachments[0]') + if (attachment) { + if (attachment.contentType.startsWith('image')) { + msg.attachment = { type: 'picture', content: attachment.contentUrl } + } else if (attachment.contentType.startsWith('video')) { + msg.attachment = { type: 'video', content: attachment.contentUrl } + } else { + logger.warning('[Microsoft] No support for files of type: '.concat(attachment.contentType)) + logger.info('[Microsoft] Defaulting to text') + if (!message.text || message.text.length <= 0) { + logger.error('[Microsoft] No text') + throw new StopPipeline() + } + msg.attachment = { type: 'text', content: message.text } + } + } else { + msg.attachment = { type: 'text', content: message.text } + } + return msg + } + + formatMarkdown (message) { + return formatMarkdownHelper(message, false, false) + } + + async formatOutgoingMessage (conversation, message, opts) { + const { type, content } = _.get(message, 'attachment') + const msg = new Message() + const mType = _.get(conversation, 'microsoftAddress.channelId', '') + + if (mType === 'teams') { + opts.allVideosAsLink = true + opts.allVideosAsLink = true + } + + const makeCard = (constructor, e) => { + const res = new constructor() + .title(e.title) + .subtitle(e.subtitle) + .images([CardImage.create(undefined, e.imageUrl)]) + .buttons(e.buttons.map(button => { + let fun = CardAction.imBack + if (['web_url', 'account_linking'].indexOf(button.type) !== -1) { + fun = CardAction.openUrl + } + return fun(undefined, button.value, button.title) + })) + if (e.onClick) { + let fun = CardAction.imBack + if (['web_url', 'account_linking'].indexOf(e.onClick.type) !== -1) { + fun = CardAction.openUrl + } + res.tap(fun(undefined, e.onClick.value, e.onClick.title || '')) + } + return res + } + + if (type === 'text') { + msg.text(content) + } else if (type === 'picture' || type === 'video') { + let hostname = url.parse(content).hostname + if (hostname.startsWith('www.')) { + hostname = hostname.slice(4, hostname.length) + } + if (type === 'video' + && (VIDEO_AS_LINK_HOSTS.indexOf(hostname) !== -1 + || opts.allVideosAsLink)) { + msg.text(content) + } else { + const attachment = await microsoftMakeAttachement(content) + msg.addAttachment(attachment) + } + } else if (type === 'quickReplies' || type === 'buttons') { + const attachment = new HeroCard() + .title(content.title) + .buttons(content.buttons.map(button => { + return CardAction.imBack(undefined, button.value, button.title) + })) + msg.addAttachment(attachment) + } else if (type === 'card') { + const attachment = makeCard(HeroCard, content) + msg.addAttachment(attachment) + } else if (type === 'list') { + const attachments = content.elements.map(e => { + return makeCard(ThumbnailCard, e) + }) + attachments.push(new ThumbnailCard() + .buttons(content.buttons.map(button => { + let fun = CardAction.imBack + if (['web_url', 'account_linking'].indexOf(button.type) !== -1) { + fun = CardAction.openUrl + } + return fun(undefined, button.value, button.title) + })) + ) + msg.attachments(attachments) + } else if (type === 'carousel' || type === 'carouselle') { + const attachments = content.map(e => { + return makeCard(HeroCard, e) + }) + msg.attachments(attachments) + msg.attachmentLayout(AttachmentLayout.carousel) + } else { + throw new BadRequestError('Message type non-supported by Microsoft : '.concat(type)) + } + return msg + } + + sendMessage (conversation, message) { + return new Promise((resolve, reject) => { + const channel = conversation.channel + const bot = microsoftGetBot(channel) + const address = conversation.microsoftAddress + bot.send(message.address(address), (err) => { + if (err) { + return reject(err) + } + return resolve() + }) + }) + } + +} diff --git a/src/channel_integrations/microsoft/index.js b/src/channel_integrations/microsoft/index.js new file mode 100644 index 0000000..d46c5ca --- /dev/null +++ b/src/channel_integrations/microsoft/index.js @@ -0,0 +1,3 @@ +import channel from './channel' + +module.exports = { channel, identifiers: ['microsoft'] } diff --git a/src/channel_integrations/microsoft/test/integration.js b/src/channel_integrations/microsoft/test/integration.js new file mode 100644 index 0000000..3509150 --- /dev/null +++ b/src/channel_integrations/microsoft/test/integration.js @@ -0,0 +1,59 @@ +import chai from 'chai' +import superagent from 'superagent' +import superagentPromise from 'superagent-promise' +import { setupChannelIntegrationTests } from '../../../../test/tools' + +const agent = superagentPromise(superagent, Promise) +const expect = chai.expect +const should = chai.should() +const channelCreationParams = { + type: 'microsoft', + slug: 'my-awesome-channel', + clientId: 'client-id', + clientSecret: 'client-secret', +} + +describe('Microsoft channel', () => { + + const { createChannel } = setupChannelIntegrationTests() + + describe('Creation', () => { + it('should be successful with valid parameters', async () => { + const response = await createChannel(channelCreationParams) + const { results: result, message } = response.body + + expect(response.status).to.equal(201) + expect(message).to.equal('Channel successfully created') + expect(result.type).to.equal(channelCreationParams.type) + expect(result.slug).to.equal(channelCreationParams.slug) + }) + + it('should return 400 for valid parameters', async () => { + try { + await createChannel({ type: 'microsoft', slug: 'my-awesome-channel' }) + should.fail() + } catch (error) { + expect(error.status).to.equal(400) + } + }) + }) + + describe('test webchat in Azure portal', () => { + let channel + + beforeEach(async () => { + channel = (await createChannel(channelCreationParams)).body.results + }) + + it('should be successful for pulse check', async () => { + const response = await agent.get(channel.webhook) + expect(response.status).to.equal(200) + }) + }) + + describe('sending a message', () => { + + it('should be successful with valid parameters') + + }) +}) diff --git a/src/channel_integrations/microsoft/utils.js b/src/channel_integrations/microsoft/utils.js new file mode 100644 index 0000000..e3b7760 --- /dev/null +++ b/src/channel_integrations/microsoft/utils.js @@ -0,0 +1,77 @@ +// @flow + +import { ChatConnector, UniversalBot, MemoryBotStorage } from 'botbuilder' +import fileType from 'file-type' +import http from 'http' +import https from 'https' + +export function microsoftParseMessage (channel, req) { + return new Promise((resolve, reject) => { + const connector = new ChatConnector({ + appId: channel.clientId, + appPassword: channel.clientSecret, + }) + const res = { + rejectIfInvalidStatus () { + if (!this.rejected && this.stat !== undefined && (this.stat < 200 || this.stat >= 300)) { + this.rejected = true + reject(new Error('error while receiving message, status : '.concat(this.stat))) + } + }, + status (status) { + this.rejectIfInvalidStatus() + this.stat = status + this.rejectIfInvalidStatus() + }, + send (status) { + this.rejectIfInvalidStatus() + this.stat = status + this.rejectIfInvalidStatus() + }, + end () { + this.rejectIfInvalidStatus() + }, + } + connector.listen()(req, res) + const bot = new UniversalBot(connector, (session) => { + resolve({ session, message: session.message }) + }) + .set('storage', new MemoryBotStorage()) + bot.linterPleaseLeaveMeAlone = 1 + }) +} + +export function microsoftGetBot (channel) { + const connector = new ChatConnector({ + appId: channel.clientId, + appPassword: channel.clientSecret, + }) + const bot = new UniversalBot(connector).set('storage', new MemoryBotStorage()) + return bot +} + +export function getFileType (url) { + return new Promise((resolve, reject) => { + let module = http + if (url.startsWith('https')) { + module = https + } + module.get(url, res => { + res.once('data', chunk => { + res.destroy() + resolve(fileType(chunk)) + }) + res.once('error', () => { reject(new Error('could not get file type')) }) + }) + }) +} + +export async function microsoftMakeAttachement (url) { + const { mime } = await getFileType(url) + const name = url.split('/').reverse().filter(e => e.length > 0)[0] + return { + contentUrl: url, + contentType: mime, + name, + } +} diff --git a/src/channel_integrations/slack/channel.js b/src/channel_integrations/slack/channel.js new file mode 100644 index 0000000..34e16cb --- /dev/null +++ b/src/channel_integrations/slack/channel.js @@ -0,0 +1,193 @@ +import _ from 'lodash' +import request from 'superagent' + +import AbstractChannelIntegration from '../abstract_channel_integration' +import { logger } from '../../utils' +import { BadRequestError, UnauthorizedError } from '../../utils/errors' +import { formatMarkdownHelper } from '../../utils/utils' + +export default class Slack extends AbstractChannelIntegration { + + populateMessageContext (req) { + return { + chatId: _.get(req, 'body.event.channel'), + senderId: _.get(req, 'body.event.user'), + } + } + + validateChannelObject (channel) { + if (!channel.token) { + throw new BadRequestError('Parameter token is missing') + } + } + + parseIncomingMessage (conversation, message, opts) { + const msg = { attachment: {} } + const file = _.get(message, 'event.file', { mimetype: '' }) + + opts.mentioned = _.get(message, 'event.channel', '').startsWith('D') + || _.get(message, 'event.text', '').includes(`<@${conversation.channel.botuser}>`) + + if (file.mimetype.startsWith('image')) { + _.set(msg, 'attachment', { type: 'picture', content: file.url_private }) + } else if (file.mimetype.startsWith('video')) { + _.set(msg, 'attachment', { type: 'picture', content: file.url_private }) + } else if (message.event && message.event.text) { + _.set(msg, 'attachment', { + type: 'text', + content: message.event.text.replace(`<@${conversation.channel.botuser}>`, ''), + }) + } else { + throw new BadRequestError('Message type non-supported by Slack') + } + + return msg + } + + formatMarkdown (message) { + return formatMarkdownHelper(message, true) + } + + formatOutgoingMessage (conversation, message) { + const type = _.get(message, 'attachment.type') + const content = _.get(message, 'attachment.content') + const makeButton = ({ type, title, value }) => { + const button = { name: title, text: title, type: 'button' } + if (type === 'web_url') { + button.url = value + } else { + button.value = value + } + return button + } + + switch (type) { + case 'text': + case 'video': { + return { text: content } + } + case 'picture': { + return { + attachments: [ + { + fallback: content, + image_url: content, + }, + ], + } + } + case 'list': { + return { + attachments: content.elements.map(e => ({ + color: '#3AA3E3', + title: e.title, + text: e.subtitle, + image_url: e.imageUrl, + attachment_type: 'default', + callback_id: 'callback_id', + actions: e.buttons.map(makeButton), + })), + } + } + case 'buttons': + case 'quickReplies': { + const { title, buttons } = content + return { + text: title, + attachments: [{ + fallback: title, + color: '#3AA3E3', + attachment_type: 'default', + callback_id: 'callback_id', + actions: buttons.map(makeButton), + }], + } + } + case 'card': { + return { + attachments: [{ + color: '#7CD197', + title: content.title, + text: content.subtitle, + image_url: content.imageUrl, + fallback: content.title, + attachment_type: 'default', + callback_id: 'callback_id', + actions: content.buttons.map(makeButton), + }], + } + } + case 'carousel': + case 'carouselle': + return { + attachments: content.map(card => ({ + color: '#F35A00', + title: card.title, + image_url: card.imageUrl, + attachment_type: 'default', + callback_id: 'callback_id', + actions: card.buttons.map(makeButton), + })), + } + case 'custom': + return content + default: + throw new BadRequestError('Message type non-supported by Slack') + } + } + + sendMessage (conversation, message) { + return new Promise((resolve, reject) => { + const req = request.post('https://slack.com/api/chat.postMessage') + .query({ token: conversation.channel.token, channel: conversation.chatId, as_user: true }) + + if (message.text) { + req.query({ text: message.text }) + } + + if (message.attachments) { + req.query({ attachments: JSON.stringify(message.attachments) }) + } + + req.end((err, res) => { + if (err) { + logger.error(`[Slack] Error sending message: ${err}`) + reject(err) + } else if (!res.body.ok) { + // might come back with { ok: false, error: 'invalid_auth' } + logger.error('[Slack] Error sending message: ', res.body) + reject(new UnauthorizedError('Invalid authentication information for Slack')) + } else { + resolve('Message sent') + } + }) + }) + } + + populateParticipantData (participant, channel) { + return new Promise((resolve, reject) => { + const token = channel.token + const senderId = participant.senderId + + request.get(`http://slack.com/api/users.info?token=${token}&user=${senderId}`) + .end((err, res) => { + if (err) { + logger.error(`Error when retrieving Slack user info: ${err}`) + return reject(err) + } + + participant.data = res.body && res.body.user + participant.markModified('data') + + participant.save().then(resolve).catch(reject) + }) + }) + } + + parseParticipantDisplayName (participant) { + return participant.data + ? { userName: participant.data.real_name } + : {} + } + +} diff --git a/src/channel_integrations/slack/index.js b/src/channel_integrations/slack/index.js new file mode 100644 index 0000000..535f5e7 --- /dev/null +++ b/src/channel_integrations/slack/index.js @@ -0,0 +1,3 @@ +import channel from './channel' + +module.exports = { channel, identifiers: ['slack'] } diff --git a/src/channel_integrations/slack/test/integration.js b/src/channel_integrations/slack/test/integration.js new file mode 100644 index 0000000..7ece523 --- /dev/null +++ b/src/channel_integrations/slack/test/integration.js @@ -0,0 +1,348 @@ +import _ from 'lodash' +import chai from 'chai' +import { setupChannelIntegrationTests } from '../../../../test/tools' +import superagentPromise from 'superagent-promise' +import superagent from 'superagent' +import nock from 'nock' + +/* eslint max-nested-callbacks: 0 */ // --> OFF +/* eslint no-unused-expressions: 0 */ // --> OFF + +const agent = superagentPromise(superagent, Promise) +const expect = chai.expect + +describe('Slack channel', () => { + + const { createChannel, sendMessageToWebhook } = setupChannelIntegrationTests() + let slackAppChannel + const teamId = 'slack-team' + + beforeEach(async () => { + const response = await createChannel({ + type: 'slackapp', + slug: 'my-awesome-channel', + clientId: 'client-id', + clientSecret: 'client-secret', + }) + slackAppChannel = response.body.results + }) + + describe('Creation', () => { + it('should be successful with valid parameters', async () => { + const slackOauthResponse = { + ok: true, + team_id: teamId, + bot: { + bot_user_id: 'bot-user-id', + bot_access_token: 'bot-access-token', + }, + } + nock('https://slack.com').post('/api/oauth.access').query(true).reply(200, slackOauthResponse) + + // Call oauth endpoint on Slack App channel to create Slack channel + const oauthResponse = await agent.get(slackAppChannel.oAuthUrl) + .query({ code: 'activation-code' }) + .send({}) + expect(oauthResponse.status).to.equal(200) + // Get Slack App channel and check for child + const createChannelResponse + = await agent.get(`${process.env.ROUTETEST}/v1/connectors/${slackAppChannel.connector}/channels/${slackAppChannel.slug}`) + .send() + expect(createChannelResponse.status).to.equal(200) + expect(createChannelResponse.body.results.children).to.have.lengthOf(1) + const slackChannel = createChannelResponse.body.results.children[0] + expect(slackChannel.botuser).to.equal(slackOauthResponse.bot.bot_user_id) + expect(slackChannel.token).to.equal(slackOauthResponse.bot.bot_access_token) + }) + }) + + describe('sending a message', () => { + + const botUser = 'W965F80RL' + const botAccessToken = 'bot-access-token' + const teamId = 'T78N3DCEN' + + beforeEach(async () => { + nock('https://slack.com').post('/api/oauth.access').query(true).reply(200, { + ok: true, + team_id: teamId, + bot: { + bot_user_id: botUser, + bot_access_token: botAccessToken, + }, + }) + nock('http://slack.com').get('/api/users.info').query(true).reply(200, {}) + + // Call oauth endpoint on Slack App channel to create Slack channel + await agent.get(slackAppChannel.oAuthUrl) + .query({ code: 'activation-code' }) + .send({ }) + }) + + const body = { + token: 'NIQiZjUhWLD9dAw54YcZbf9X', + team_id: teamId, + enterprise_id: 'A1RABCDXHA', + api_app_id: 'BABTW7MNU', + event: { + type: 'message', + user: 'another-user', + text: 'This is a test message', + client_msg_id: '2ca5568c-1ae6-4446-9419-a1f388d3289b', + team: teamId, + source_team: teamId, + user_team: teamId, + user_profile: { + avatar_hash: '5ab7b65c4c14', + image_72: 'https://avatars.slack-edge.com/2018-03-13' + + '/335522214727_5ab6b74c4c132402f9ba_72.jpg', + first_name: 'John', + real_name: 'John Doe', + display_name: 'John Doe', + team: 'E7RBBBXHB', + name: 'johndoe', + is_restricted: false, + is_ultra_restricted: false, + }, + ts: '1531523342.000005', + channel: 'DAPTXDG92', + event_ts: '1531523342.000005', + channel_type: 'im', + }, + type: 'event_callback', + authed_teams: [teamId], + event_id: 'EvBRPDDKU6', + event_time: 1531523342, + authed_users: ['WAPTX8GTA'], + } + + describe('should be successful', () => { + it('in text format', async () => { + const message = 'my text message' + const apiCall = nock('https://slack.com').post('/api/chat.postMessage') + .query((actualQuery) => { + const expectedQuery = { + token: botAccessToken, + as_user: 'true', + channel: body.event.channel, + text: message, + } + return _.isEqual(expectedQuery, actualQuery) + }) + .reply(200, { + ok: true, + }) + const botResponse = { + messages: JSON.stringify([{ type: 'text', content: message }]), + } + const response = await sendMessageToWebhook(slackAppChannel, body, {}, botResponse) + expect(response.status).to.equal(200) + expect(apiCall.isDone()).to.be.true + }) + + it('in list format', async () => { + const listElement = { + title: 'title', + subtitle: 'subtitle', + imageUrl: 'https://img.url', + buttons: [{ type: '', title: 'button title', value: 'abc' }], + } + const apiCall = nock('https://slack.com').post('/api/chat.postMessage') + .query((actualQuery) => { + actualQuery.attachments = JSON.parse(actualQuery.attachments) + const expectedQuery = { + token: botAccessToken, + as_user: 'true', + channel: body.event.channel, + attachments: [{ + color: '#3AA3E3', + attachment_type: 'default', + callback_id: 'callback_id', + title: listElement.title, + image_url: listElement.imageUrl, + text: listElement.subtitle, + actions: [{ + name: listElement.buttons[0].title, + text: listElement.buttons[0].title, + type: 'button', + value: listElement.buttons[0].value, + }], + }], + } + return _.isEqual(expectedQuery, actualQuery) + }) + .reply(200, { + ok: true, + }) + const botResponse = { + results: {}, + messages: JSON.stringify([{ type: 'list', content: { elements: [listElement] } }]), + } + const response = await sendMessageToWebhook(slackAppChannel, body, {}, botResponse) + expect(response.status).to.equal(200) + expect(apiCall.isDone()).to.be.true + }) + + it('in card format', async () => { + const cardElement = { + title: 'title', + subtitle: 'subtitle', + imageUrl: 'https://img.url', + buttons: [{ type: '', title: 'button title', value: 'abc' }], + } + const apiCall = nock('https://slack.com').post('/api/chat.postMessage') + .query((actualQuery) => { + actualQuery.attachments = JSON.parse(actualQuery.attachments) + const expectedQuery = { + token: botAccessToken, + as_user: 'true', + channel: body.event.channel, + attachments: [{ + color: '#7CD197', + fallback: cardElement.title, + attachment_type: 'default', + callback_id: 'callback_id', + title: cardElement.title, + image_url: cardElement.imageUrl, + text: cardElement.subtitle, + actions: [{ + name: cardElement.buttons[0].title, + text: cardElement.buttons[0].title, + type: 'button', + value: cardElement.buttons[0].value, + }], + }], + } + return _.isEqual(expectedQuery, actualQuery) + }) + .reply(200, { + ok: true, + }) + const botResponse = { + results: {}, + messages: JSON.stringify([{ type: 'card', content: cardElement }]), + } + const response = await sendMessageToWebhook(slackAppChannel, body, {}, botResponse) + expect(response.status).to.equal(200) + expect(apiCall.isDone()).to.be.true + }) + + it('in carousel format', async () => { + const carouselElement = { + title: 'title', + imageUrl: 'https://img.url', + buttons: [{ type: '', title: 'button title', value: 'abc' }], + } + const apiCall = nock('https://slack.com').post('/api/chat.postMessage') + .query((actualQuery) => { + actualQuery.attachments = JSON.parse(actualQuery.attachments) + const expectedQuery = { + token: botAccessToken, + as_user: 'true', + channel: body.event.channel, + attachments: [{ + color: '#F35A00', + attachment_type: 'default', + callback_id: 'callback_id', + title: carouselElement.title, + image_url: carouselElement.imageUrl, + actions: [{ + name: carouselElement.buttons[0].title, + text: carouselElement.buttons[0].title, + type: 'button', + value: carouselElement.buttons[0].value, + }], + }], + } + return _.isEqual(expectedQuery, actualQuery) + }) + .reply(200, { + ok: true, + }) + const botResponse = { + results: {}, + messages: JSON.stringify([{ type: 'carousel', content: [carouselElement] }]), + } + const response = await sendMessageToWebhook(slackAppChannel, body, {}, botResponse) + expect(response.status).to.equal(200) + expect(apiCall.isDone()).to.be.true + }) + + it('in buttons', async () => { + const buttonsElement = { + title: 'title', + buttons: [{ type: '', title: 'button title', value: 'abc' }], + } + const apiCall = nock('https://slack.com').post('/api/chat.postMessage') + .query((actualQuery) => { + actualQuery.attachments = JSON.parse(actualQuery.attachments) + const expectedQuery = { + token: botAccessToken, + as_user: 'true', + channel: body.event.channel, + text: buttonsElement.title, + attachments: [{ + fallback: buttonsElement.title, + color: '#3AA3E3', + attachment_type: 'default', + callback_id: 'callback_id', + actions: [{ + name: buttonsElement.buttons[0].title, + text: buttonsElement.buttons[0].title, + type: 'button', + value: buttonsElement.buttons[0].value, + }], + }], + } + return _.isEqual(expectedQuery, actualQuery) + }) + .reply(200, { + ok: true, + }) + const botResponse = { + results: {}, + messages: JSON.stringify([{ type: 'buttons', content: buttonsElement }]), + } + const response = await sendMessageToWebhook(slackAppChannel, body, {}, botResponse) + expect(response.status).to.equal(200) + expect(apiCall.isDone()).to.be.true + }) + + it('with response message type picture', async () => { + const imageUrl = 'https://url.to/image.png' + const apiCall = nock('https://slack.com') + .post('/api/chat.postMessage') + .query((actualQuery) => { + actualQuery.attachments = JSON.parse(actualQuery.attachments) + const expectedQuery = { + token: botAccessToken, + as_user: 'true', + channel: body.event.channel, + attachments: [{ + fallback: imageUrl, + image_url: imageUrl, + }], + } + return _.isEqual(expectedQuery, actualQuery) + }) + .reply(200, { + ok: true, + }) + + const botResponse = { + results: {}, + messages: JSON.stringify([ + { + type: 'picture', + content: imageUrl, + }, + ]), + } + const response = await sendMessageToWebhook(slackAppChannel, body, {}, botResponse) + expect(response.status).to.equal(200) + expect(apiCall.isDone()).to.be.true + }) + }) + + }) +}) diff --git a/src/channel_integrations/slack_app/channel.js b/src/channel_integrations/slack_app/channel.js new file mode 100644 index 0000000..1aaeeec --- /dev/null +++ b/src/channel_integrations/slack_app/channel.js @@ -0,0 +1,161 @@ +import _ from 'lodash' +import superagent from 'superagent' +import superagentPromise from 'superagent-promise' + +import AbstractChannelIntegration from '../abstract_channel_integration' +import { Channel } from '../../models' +import { slugify } from '../../models/channel' +import { StopPipeline, NotFoundError, BadRequestError } from '../../utils/errors' +import * as config from '../../../config' + +const agent = superagentPromise(superagent, Promise) + +export default class SlackAppChannel extends AbstractChannelIntegration { + + populateMessageContext (req) { + return { + chatId: _.get(req, 'body.event.channel'), + senderId: _.get(req, 'body.event.user'), + } + } + + validateChannelObject (channel) { + if (!channel.clientId) { + throw new BadRequestError('Parameter clientId is missing') + } else if (!channel.clientSecret) { + throw new BadRequestError('Parameter clientSecret is missing') + } + } + + async beforeChannelCreated (channel) { + channel.oAuthUrl = `${config.base_url}/v1/oauth/slack/${channel._id}` + return channel.save() + } + + async afterChannelDeleted (channel) { + for (let child of channel.children) { + child = await Channel.findById(child) + if (child) { await child.remove() } + } + } + + authenticateWebhookRequest (req) { + if (req.body && req.body.type === 'url_verification') { + throw new StopPipeline(req.body.challenge) + } + } + + async onWebhookCalled (req, res, channel) { + // send 200 OK early so that Slack doesn't retry our web hook after 3s + // because sometimes we take more than 3s to produce and send a bot response back to Slack + res.status(200).send() + + /* handle action (buttons) to format them */ + if (req.body.payload) { + req.body = SlackAppChannel.parsePayload(req.body) + } + + /* Search for the App children */ + // slugify the slug as well to support channels created before slug was slugified on save + channel = _.find( + channel.children, child => slugify(child.slug) === slugify(req.body.team_id) + ) + if (!channel) { throw new NotFoundError('Channel') } + + /* check if event is only message */ + if (channel.type === 'slack' + && req.body + && req.body.event + && req.body.event.type !== 'message') { + throw new StopPipeline() + } + + /* check if sender is the bot */ + if (req.body.event.user === channel.botuser) { + throw new StopPipeline() + } + + return channel + } + + finalizeWebhookRequest (req, res) { + res.status(200).send() + } + + /* + * SlackApp specific methods + */ + + static parsePayload (body) { + const parsedBody = JSON.parse(body.payload) + + return ({ + team_id: parsedBody.team.id, + token: parsedBody.token, + event: { + type: 'message', + is_button_click: parsedBody.actions[0].type === 'button', + user: parsedBody.user.id, + text: parsedBody.actions[0].value, + ts: parsedBody.action_ts, + channel: parsedBody.channel.id, + event_ts: parsedBody.action_ts, + }, + type: 'event_callback', + }) + } + + static async receiveOauth (req, res) { + const { channel_id } = req.params + const { code } = req.query + const channel = await Channel.findById(channel_id) + + if (!channel) { + throw new NotFoundError('Channel') + } + + let response + try { + response = await agent.post('https://slack.com/api/oauth.access') + .query({ client_id: channel.clientId }) + .query({ client_secret: channel.clientSecret }) + .query({ code }) + } catch (err) { + throw new Error(`[Slack] Failed oAuth subscription: ${err.message}`) + } + + const { body } = response + if (!body.ok) { + throw new Error(`[Slack] Failed oAuth subscription: ${body.error}`) + } + + try { + const channelChild = await new Channel({ + type: 'slack', + app: channel_id, + slug: body.team_id, + connector: channel.connector, + botuser: body.bot.bot_user_id, + token: body.bot.bot_access_token, + }) + channel.children.push(channelChild._id) + + await Promise.all([ + channelChild.save(), + channel.save(), + ]) + } catch (err) { + throw new Error(`Error storing Mongoose model for Slack channel child: ${err.message}`) + } + + let url = `${config.cody_base_url}/` + + if (req.query.state) { + const infosSlugEncoded = Buffer.from(req.query.state, 'base64') + const infosSlugDecoded = JSON.parse(infosSlugEncoded.toString('utf8')) + const { userSlug, botSlug } = infosSlugDecoded + url = `${config.cody_base_url}/${userSlug}/${botSlug}/connect/?slack=success` + } + res.redirect(url) + } +} diff --git a/src/channel_integrations/slack_app/index.js b/src/channel_integrations/slack_app/index.js new file mode 100644 index 0000000..f03b8ef --- /dev/null +++ b/src/channel_integrations/slack_app/index.js @@ -0,0 +1,4 @@ +import channel from './channel' +import routes from './routes' + +module.exports = { channel, routes, identifiers: ['slackapp'] } diff --git a/src/channel_integrations/slack_app/routes.js b/src/channel_integrations/slack_app/routes.js new file mode 100644 index 0000000..ad80636 --- /dev/null +++ b/src/channel_integrations/slack_app/routes.js @@ -0,0 +1,11 @@ +import SlackAppChannel from './channel' + +export default [ + { + method: 'GET', + path: ['/oauth/slack/:channel_id'], + validators: [], + authenticators: [], + handler: SlackAppChannel.receiveOauth, + }, +] diff --git a/src/channel_integrations/slack_app/test/integration.js b/src/channel_integrations/slack_app/test/integration.js new file mode 100644 index 0000000..8db4e09 --- /dev/null +++ b/src/channel_integrations/slack_app/test/integration.js @@ -0,0 +1,64 @@ +import chai from 'chai' +import { setupChannelIntegrationTests } from '../../../../test/tools' + +const expect = chai.expect +const should = chai.should() +const channelCreationParams = { + type: 'slackapp', + slug: 'my-awesome-channel', + clientId: 'client-id', + clientSecret: 'client-secret', +} + +/* eslint no-unused-expressions: 0 */ // --> OFF + +describe('Slack App channel', () => { + + const { createChannel, updateChannel, deleteChannel } = setupChannelIntegrationTests() + + describe('Creation', () => { + it('should be successful with valid parameters', async () => { + const response = await createChannel(channelCreationParams) + const { results: result, message } = response.body + + expect(response.status).to.equal(201) + expect(message).to.equal('Channel successfully created') + expect(result.children).to.be.empty + expect(result.type).to.equal(channelCreationParams.type) + expect(result.slug).to.equal(channelCreationParams.slug) + expect(result.isErrored).to.be.false + expect(result.isActivated).to.be.true + expect(result.oAuthUrl.startsWith(`${process.env.ROUTETEST}/v1/oauth/slack/`)).to.be.true + expect(result.webhook.startsWith(`${process.env.ROUTETEST}/v1/webhook/`)).to.be.true + }) + it('should return 400 with invalid parameters', async () => { + try { + await createChannel({}) + should.fail() + } catch (error) { + expect(error.status).to.equal(400) + } + }) + }) + + describe('Update', () => { + it('should be successful if channel exists', async () => { + let response = await createChannel(channelCreationParams) + const channel = response.body.results + const newValues = JSON.parse(JSON.stringify(channelCreationParams)) + newValues.clientSecret = 'newsecret' + response = await updateChannel(channel, newValues) + expect(response.status).to.equal(200) + expect(response.body.results.clientSecret).to.equal(newValues.clientSecret) + }) + }) + + describe('Deletion', () => { + it('should be successful if channel exists', async () => { + let response = await createChannel(channelCreationParams) + const channel = response.body.results + response = await deleteChannel(channel) + expect(response.status).to.equal(200) + }) + }) +}) diff --git a/src/channel_integrations/slack_app/test/routes.js b/src/channel_integrations/slack_app/test/routes.js new file mode 100644 index 0000000..a882bba --- /dev/null +++ b/src/channel_integrations/slack_app/test/routes.js @@ -0,0 +1,16 @@ +import expect from 'expect.js' + +import { fetchMethod } from '../../../../test/tools' + +import SlackAppChannel from '../channel' +import OauthRoutes from '../routes' + +describe('Oauth Routes Testing', () => { + describe('GET /oauth/slack/:channel_id', () => { + it('should call SlackAppChannel#receiveOauth', async () => { + expect(fetchMethod(OauthRoutes, 'GET', '/oauth/slack/:channel_id')) + .to.equal(SlackAppChannel.receiveOauth) + }) + }) + +}) diff --git a/src/channel_integrations/telegram/channel.js b/src/channel_integrations/telegram/channel.js new file mode 100644 index 0000000..42adbeb --- /dev/null +++ b/src/channel_integrations/telegram/channel.js @@ -0,0 +1,310 @@ +import _ from 'lodash' +import superAgent from 'superagent' +import superAgentPromise from 'superagent-promise' + +import AbstractChannelIntegration from '../abstract_channel_integration' +import { logger } from '../../utils' +import { + BadRequestError, + InternalServerError, + StopPipeline, + ValidationError, +} from '../../utils/errors' +import { formatMarkdownHelper } from '../../utils/utils' + +const agent = superAgentPromise(superAgent, Promise) + +export default class Telegram extends AbstractChannelIntegration { + + validateChannelObject (channel) { + if (!channel.token) { + throw new ValidationError('token', 'missing') + } + } + + async beforeChannelCreated (channel) { + const { token, webhook } = channel + + try { + await this.setWebhook(token, webhook) + channel.isErrored = false + } catch (err) { + logger.error(`[Telegram] Cannot set webhook: ${err}`) + channel.isErrored = true + } + + return channel.save() + } + + async afterChannelUpdated (channel, oldChannel) { + await this.afterChannelDeleted(oldChannel) + await this.beforeChannelCreated(channel) + } + + async afterChannelDeleted (channel) { + const { token } = channel + + try { + const { status } = await agent.get(`https://api.telegram.org/bot${token}/deleteWebhook`) + + if (status !== 200) { + throw new InternalServerError(`[Telegram][Status ${status}] Cannot delete webhook`) + } + } catch (err) { + logger.error(`[Telegram] Cannot unset the webhook: ${err}`) + channel.isErrored = true + } + } + + populateMessageContext (req) { + if (req.body.edited_message || req.body.edited_channel_post) { + throw new StopPipeline() + } + + const message = req.body.message || req.body.channel_post + return { + chatId: _.get(message, 'chat.id'), + senderId: _.get(message, 'from.id'), + } + } + + finalizeWebhookRequest (req, res) { + res.status(200).send({ status: 'success' }) + } + + parseIncomingMessage (conversation, { message, callback_query }) { + const channelType = _.get(conversation, 'channel.type') + const content = _.get(message, 'text') + + const buttonClickText = _.get(callback_query, 'data') + + if (!content && !buttonClickText) { + logger.error('[Telegram] No text field in incoming message') + throw new StopPipeline() + } + + return { + attachment: { type: 'text', content: content || buttonClickText }, + channelType, + } + } + + formatMarkdown (message) { + return formatMarkdownHelper(message) + } + + formatOutgoingMessage ({ channel, chatId }, { attachment }, { senderId }) { + const { type, content } = attachment + const reply = { + chatId, + type, + to: senderId, + token: _.get(channel, 'token'), + } + switch (type) { + case 'text': + case 'video': + return { ...reply, body: content } + case 'picture': + return { ...reply, type: 'photo', body: content } + case 'card': + case 'quickReplies': + return { + ...reply, + type: 'card', + photo: _.get(content, 'imageUrl'), + body: `${_.get(content, 'title', '')}\n${_.get(content, 'subtitle', '')}`, + keyboard: tgKeyboardLayout(content.buttons.map(tgFormatButton)), + } + case 'list': + return { + ...reply, + keyboard: tgKeyboardLayout( + _.flattenDeep([ + content.buttons.map(tgFormatButton), + content.elements.map(elem => elem.buttons.map(tgFormatButton)), + ]) + ), + body: content.elements.map(e => `*- ${e.title}*\n${e.subtitle}\n${e.imageUrl || ''}`), + } + case 'carousel': + case 'carouselle': + return { + ...reply, + keyboard: tgKeyboardLayout( + _.flatten( + content.map(card => card.buttons.map(tgFormatButton)) + ) + ), + body: content.map(({ imageUrl, title, subtitle }) => + `*${title}*\n[${subtitle || ''}](${imageUrl})`), + } + case 'buttons': + return { + ...reply, + type: 'text', + keyboard: tgKeyboardLayout(content.buttons.map(tgFormatButton)), + body: _.get(content, 'title', ''), + } + case 'custom': + return { + ...reply, + body: content, + } + default: + throw new BadRequestError('Message type non-supported by Telegram') + } + } + + async sendMessage ({ channel }, { token, type, chatId, body, photo, keyboard }) { + const url = `https://api.telegram.org/bot${token}` + const method = type === 'text' ? 'sendMessage' : `send${_.capitalize(type)}` + + if (type === 'card') { + try { + if (!_.isUndefined(photo)) { + await agent.post(`${url}/sendPhoto`, { chat_id: chatId, photo }) + } + await agent.post(`${url}/sendMessage`, { + chat_id: chatId, + text: body, + reply_markup: { keyboard, one_time_keyboard: true }, + parse_mode: 'Markdown', + }) + } catch (err) { + this.logSendMessageError(err, type) + } + } else if (type === 'quickReplies') { + try { + await agent.post(`${url}/sendMessage`, { + chat_id: chatId, + text: body, + reply_markup: { keyboard, one_time_keyboard: true }, + parse_mode: 'Markdown', + }) + } catch (err) { + this.logSendMessageError(err, type) + } + } else if (type === 'carousel' || type === 'carouselle' || type === 'list') { + let i = 0 + for (const elem of body) { + // Send keyboard if this is the last POST request + if (i === body.length - 1) { + try { + await agent.post(`${url}/sendMessage`, { + chat_id: chatId, + text: elem, + reply_markup: { keyboard, one_time_keyboard: true }, + parse_mode: 'Markdown', + }) + } catch (err) { + this.logSendMessageError(err, type) + } + } + try { + await agent.post( + `${url}/sendMessage`, { chat_id: chatId, text: elem, parse_mode: 'Markdown' } + ) + } catch (err) { + this.logSendMessageError(err, type) + } + i++ + } + } else if (type === 'custom') { + const allowedMethods = [ + 'sendPhoto', + 'sendAudio', + 'sendDocument', + 'sendVideo', + 'sendVoice', + 'sendVideoNote', + 'sendMediaGroup', + 'sendLocation', + 'sendVenue', + 'sendContact', + 'sendChatAction', + ] + + for (const elem of body) { + const { method: customMethod, content } = elem + + if (!allowedMethods.includes(customMethod)) { + throw new BadRequestError(`Custom method ${customMethod} non-supported by Telegram`) + } + + try { + await agent.post(`${url}/${customMethod}`, { ...content, chat_id: chatId }) + } catch (err) { + this.logSendMessageError(err, type, customMethod) + } + } + } else { + try { + await agent.post(`${url}/${method}`, { + chat_id: chatId, + [type]: body, + reply_markup: { keyboard, one_time_keyboard: true }, + parse_mode: 'Markdown', + }) + } catch (err) { + this.logSendMessageError(err, type, method) + } + } + } + + /* + * Telegram specific helpers + */ + + logSendMessageError (err, type, method) { + if (method) { + logger.error(`[Telegram] Error sending message of type ${type} using method ${method}: ${err}`) + } else if (!method && type) { + logger.error(`[Telegram] Error sending message of type ${type}: ${err}`) + } else { + logger.error(`[Telegram] Error sending message: ${err})`) + } + logger.error( + `[Telegram] Error response: ${err.response.text}`, + '[Telegram] Error details', err.response.error + ) + } + + // Set a Telegram webhook + async setWebhook (token, webhook) { + const url = `https://api.telegram.org/bot${token}/setWebhook` + const { status } = await agent.post(url, { url: webhook }) + + if (status !== 200) { + throw new BadRequestError(`[Telegram][Status ${status}] Cannot set webhook`) + } + } + +} + +// These functions are exported only for tests purpose +export function tgFormatButton (button) { + const payload = { text: button.title } + if (button.type === 'web_url') { + payload.url = button.value + } else { + payload.callback_data = button.value + } + return payload +} + +const TG_MAX_KEYBOARD_LINES = 3 + +export function tgKeyboardLayout (buttons) { + const elemPerLine = Math.floor(buttons.length / TG_MAX_KEYBOARD_LINES) + const extraButtons = buttons.length % TG_MAX_KEYBOARD_LINES + const lines = [] + let seen = 0 + while (seen < buttons.length) { + const nb = elemPerLine + (extraButtons > lines.length ? 1 : 0) + lines.push(buttons.slice(seen, seen + nb)) + seen += nb + } + + return lines +} diff --git a/src/channel_integrations/telegram/index.js b/src/channel_integrations/telegram/index.js new file mode 100644 index 0000000..a37e105 --- /dev/null +++ b/src/channel_integrations/telegram/index.js @@ -0,0 +1,3 @@ +import channel from './channel' + +module.exports = { channel, identifiers: ['telegram'] } diff --git a/src/channel_integrations/telegram/test/channel.js b/src/channel_integrations/telegram/test/channel.js new file mode 100644 index 0000000..f655066 --- /dev/null +++ b/src/channel_integrations/telegram/test/channel.js @@ -0,0 +1,18 @@ +import { assert } from 'chai' +import Telegram from '../channel' +import mockups from './mockups.json' +const telegram = new Telegram() +const fakeFormatMessage = json => telegram.formatOutgoingMessage( + { channel: { token: '' }, chatId: '' }, + { attachment: json }, + { senderId: '' }, +) + +describe('Telegram service', () => { + mockups.forEach(mockup => + it('Should create keyboard layout from messages', () => { + const formattedMessage = fakeFormatMessage(mockup.json) + assert.deepEqual(JSON.parse(JSON.stringify(formattedMessage)), mockup.expected) + }) + ) +}) diff --git a/src/channel_integrations/telegram/test/integration.js b/src/channel_integrations/telegram/test/integration.js new file mode 100644 index 0000000..951ee7f --- /dev/null +++ b/src/channel_integrations/telegram/test/integration.js @@ -0,0 +1,320 @@ +import chai from 'chai' +import { setupChannelIntegrationTests } from '../../../../test/tools' +import nock from 'nock' +import _ from 'lodash' + +/* eslint max-nested-callbacks: 0 */ // --> OFF + +const expect = chai.expect +const should = chai.should() + +const channelCreationParams = { + type: 'telegram', + slug: 'my-awesome-channel', + token: 'token', +} + +describe('Telegram channel', () => { + + const { createChannel, updateChannel, deleteChannel, + sendMessageToWebhook } = setupChannelIntegrationTests() + const telegramAPI = 'https://api.telegram.org' + + beforeEach(() => { + nock(telegramAPI).post('/bottoken/setWebhook').query(true).reply(200, {}) + }) + + describe('Creation', () => { + it('should be successful with valid parameters', async () => { + const response = await createChannel(channelCreationParams) + const { results: result, message } = response.body + + expect(response.status).to.equal(201) + expect(message).to.equal('Channel successfully created') + expect(result.type).to.equal(channelCreationParams.type) + expect(result.slug).to.equal(channelCreationParams.slug) + /* eslint no-unused-expressions: 0 */ // --> OFF + expect(result.isErrored).to.be.false + expect(result.isActivated).to.be.true + }) + it('should return 400 with invalid parameters', async () => { + try { + await createChannel({}) + should.fail() + } catch (error) { + expect(error.status).to.equal(400) + } + }) + }) + + describe('Update', () => { + it('should be successful if channel exists', async () => { + let response = await createChannel(channelCreationParams) + const channel = response.body.results + const newValues = JSON.parse(JSON.stringify(channelCreationParams)) + newValues.token = 'newtoken' + nock(telegramAPI).post('/botnewtoken/setWebhook').query(true).reply(200, {}) + nock(telegramAPI).get('/bottoken/deleteWebhook').query(true).reply(200, {}) + response = await updateChannel(channel, newValues) + expect(response.status).to.equal(200) + expect(response.body.results.token).to.equal(newValues.token) + }) + }) + + describe('Deletion', () => { + it('should be successful if channel exists', async () => { + let response = await createChannel(channelCreationParams) + const channel = response.body.results + nock(telegramAPI).get('/bottoken/deleteWebhook').query(true).reply(200, {}) + response = await deleteChannel(channel) + expect(response.status).to.equal(200) + }) + }) + + describe('sending a message', () => { + + let channel + + beforeEach(async () => { + channel = (await createChannel(channelCreationParams)).body.results + }) + + it('should be successful with valid parameters', async () => { + const outgoingMessageCall = nock(telegramAPI).post('/bottoken/sendMessage').reply(200, {}) + const response = await sendMessageToWebhook(channel, { + message: { chat: { id: 123 }, text: 'a message' }, + }) + expect(response.status).to.equal(200) + expect(response.body).to.eql({ status: 'success' }) + expect(outgoingMessageCall.isDone()).to.be.true + }) + + describe('should be successful', () => { + + const body = { + message: { chat: { id: 123 }, text: 'a message' }, + } + const headers = {} + + it('in list format', async () => { + nock(telegramAPI).post('/bottoken/sendMessage').reply(200, {}) + const buttons = [ + { type: 'account_link', title: 'button title', value: 'https://link.com' }, + { type: 'web_url', title: 'button title', value: 'https://link.com' }, + { type: 'phone_number', title: 'button title', value: '0123554' }] + const listElement = { + title: 'title', + subtitle: 'subtitle', + imageUrl: 'https://img.url', + buttons, + } + const expectedBody = _.matches({ + chat_id: `${body.message.chat.id}`, + parse_mode: 'Markdown', + text: `*- ${listElement.title}*\n${listElement.subtitle}\n${listElement.imageUrl}`, + }) + const apiCall = nock(telegramAPI).post('/bottoken/sendMessage', expectedBody).reply(200, {}) + const botResponse = { + results: {}, + messages: JSON.stringify([{ + type: 'list', + content: { elements: [listElement], buttons }, + }]), + } + const response = await sendMessageToWebhook(channel, body, headers, botResponse) + expect(response.status).to.equal(200) + expect(apiCall.isDone()).to.be.true + }) + + it('in card format', async () => { + const cardElement = { + title: 'title', + subtitle: 'subtitle', + imageUrl: 'https://img.url', + buttons: [{ type: '', title: 'button title', value: 'abc' }], + } + const expectedPhotoBody = _.matches({ + chat_id: `${body.message.chat.id}`, + photo: cardElement.imageUrl, + }) + const photoRequest = nock(telegramAPI) + .post('/bottoken/sendPhoto', expectedPhotoBody).reply(200, {}) + const expectedMessageBody = _.matches({ + chat_id: `${body.message.chat.id}`, + parse_mode: 'Markdown', + reply_markup: { + keyboard: [[{ + text: cardElement.buttons[0].title, + callback_data: cardElement.buttons[0].value, + }]], + one_time_keyboard: true, + }, + text: `${cardElement.title}\n${cardElement.subtitle}`, + }) + const messageRequest = nock(telegramAPI) + .post('/bottoken/sendMessage', expectedMessageBody).reply(200, {}) + const botResponse = { + results: {}, + messages: JSON.stringify([{ type: 'card', content: cardElement }]), + } + const response = await sendMessageToWebhook(channel, body, headers, botResponse) + expect(response.status).to.equal(200) + expect(photoRequest.isDone()).to.be.true + expect(messageRequest.isDone()).to.be.true + }) + + it('in carousel format', async () => { + const carouselElement = { + title: 'title', + imageUrl: 'https://img.url', + buttons: [{ type: '', title: 'button title', value: 'abc' }], + } + const expectedMessageBody = _.matches({ + chat_id: `${body.message.chat.id}`, + parse_mode: 'Markdown', + text: `*${carouselElement.title}*\n[](${carouselElement.imageUrl})`, + }) + const keyboardBody = _.matches({ + chat_id: `${body.message.chat.id}`, + parse_mode: 'Markdown', + reply_markup: { + keyboard: [[{ + text: carouselElement.buttons[0].title, + callback_data: carouselElement.buttons[0].value, + }]], + one_time_keyboard: true, + }, + text: `*${carouselElement.title}*\n[](${carouselElement.imageUrl})`, + }) + + const keyboardRequest = nock(telegramAPI) + .post('/bottoken/sendMessage', keyboardBody).reply(200, {}) + const messageRequest = nock(telegramAPI) + .post('/bottoken/sendMessage', expectedMessageBody).reply(200, {}) + const botResponse = { + results: {}, + messages: JSON.stringify([{ type: 'carousel', content: [carouselElement] }]), + } + const response = await sendMessageToWebhook(channel, body, headers, botResponse) + expect(response.status).to.equal(200) + expect(messageRequest.isDone()).to.be.true + expect(keyboardRequest.isDone()).to.be.true + }) + + it('in quickReplies format', async () => { + const buttonsElement = { + title: 'title', + buttons: [{ type: '', title: 'button title', value: 'abc' }], + } + const expectedMessageBody = _.matches({ + chat_id: `${body.message.chat.id}`, + parse_mode: 'Markdown', + reply_markup: { + keyboard: [[{ + text: buttonsElement.buttons[0].title, + callback_data: buttonsElement.buttons[0].value, + }]], + one_time_keyboard: true, + }, + text: `${buttonsElement.title}\n`, + }) + const messageRequest = nock(telegramAPI) + .post('/bottoken/sendMessage', expectedMessageBody).reply(200, {}) + const botResponse = { + results: {}, + messages: JSON.stringify([{ type: 'quickReplies', content: buttonsElement }]), + } + const response = await sendMessageToWebhook(channel, body, headers, botResponse) + expect(response.status).to.equal(200) + expect(messageRequest.isDone()).to.be.true + }) + + it('in video format', async () => { + const videoLink = 'https://link.com' + const expectedVideoBody = _.matches({ + chat_id: `${body.message.chat.id}`, + reply_markup: { one_time_keyboard: true }, + video: videoLink, + }) + const videoRequest = nock(telegramAPI) + .post('/bottoken/sendVideo', expectedVideoBody).reply(200, {}) + const botResponse = { + results: {}, + messages: JSON.stringify([{ type: 'video', content: videoLink }]), + } + const response = await sendMessageToWebhook(channel, body, headers, botResponse) + expect(response.status).to.equal(200) + expect(videoRequest.isDone()).to.be.true + }) + + it('in picture format', async () => { + const pictureLink = 'https://link.com' + const expectedPictureBody = _.matches({ + chat_id: `${body.message.chat.id}`, + reply_markup: { one_time_keyboard: true }, + photo: pictureLink, + }) + const pictureRequest = nock(telegramAPI) + .post('/bottoken/sendPhoto', expectedPictureBody).reply(200, {}) + const botResponse = { + results: {}, + messages: JSON.stringify([{ type: 'picture', content: pictureLink }]), + } + const response = await sendMessageToWebhook(channel, body, headers, botResponse) + expect(response.status).to.equal(200) + expect(pictureRequest.isDone()).to.be.true + }) + + it('in picture format - failing', async () => { + const pictureLink = 'https://link.com' + const expectedPictureBody = _.matches({ + chat_id: `${body.message.chat.id}`, + reply_markup: { one_time_keyboard: true }, + photo: pictureLink, + }) + const pictureRequest = nock(telegramAPI) + .post('/bottoken/sendPhoto', expectedPictureBody) + .reply(403, { + ok: false, + error_code: 403, + description: 'Forbidden: bot was blocked by the user', + }) + const botResponse = { + results: {}, + messages: JSON.stringify([{ type: 'picture', content: pictureLink }]), + } + const response = await sendMessageToWebhook(channel, body, headers, botResponse) + expect(response.status).to.equal(200) + expect(pictureRequest.isDone()).to.be.true + }) + + it('in buttons format', async () => { + const buttonsElement = { + title: 'title', + buttons: [{ type: '', title: 'button title', value: 'abc' }], + } + const botResponse = { + results: {}, + messages: JSON.stringify([{ type: 'buttons', content: buttonsElement }]), + } + const expectedMessageBody = _.matches({ + chat_id: `${body.message.chat.id}`, + reply_markup: { + keyboard: [[{ + text: buttonsElement.buttons[0].title, + callback_data: buttonsElement.buttons[0].value, + }]], + one_time_keyboard: true, + }, + text: buttonsElement.title, + }) + const messageRequest = nock(telegramAPI) + .post('/bottoken/sendMessage', expectedMessageBody).reply(200, {}) + const response = await sendMessageToWebhook(channel, body, headers, botResponse) + expect(response.status).to.equal(200) + expect(messageRequest.isDone()).to.be.true + }) + }) + + }) +}) diff --git a/src/channel_integrations/telegram/test/mockups.json b/src/channel_integrations/telegram/test/mockups.json new file mode 100644 index 0000000..0a29a99 --- /dev/null +++ b/src/channel_integrations/telegram/test/mockups.json @@ -0,0 +1,205 @@ +[ + { + "json": { + "type": "quickReplies", + "content": { + "title": "Quick reply title", + "buttons": [ + { + "value": "quick reply payload1", + "title": "quick reply text1" + }, + { + "value": "quick reply payload2", + "title": "quick reply text2" + } + ] + } + }, + "expected": { + "chatId": "", + "type": "card", + "to": "", + "token": "", + "body": "Quick reply title\n", + "keyboard": [ + [ + { + "text": "quick reply text1", + "callback_data": "quick reply payload1" + } + ], + [ + { + "text": "quick reply text2", + "callback_data": "quick reply payload2" + } + ] + ] + } + }, + { + "json": { + "type": "quickReplies", + "content": { + "title": "Quick reply title", + "buttons": [ + { + "title": "t1", + "value": "v1" + }, + { + "title": "t2", + "value": "v2" + }, + { + "title": "t3", + "value": "v3" + }, + { + "title": "t4", + "value": "v4" + }, + { + "title": "t5", + "value": "v5" + }, + { + "title": "t6", + "value": "v6" + }, + { + "title": "t7", + "value": "v7" + }, + { + "title": "t8", + "value": "v8" + } + ] + } + }, + "expected": { + "chatId": "", + "type": "card", + "to": "", + "token": "", + "body": "Quick reply title\n", + "keyboard": [ + [ + { + "text": "t1", + "callback_data": "v1" + }, + { + "text": "t2", + "callback_data": "v2" + }, + { + "text": "t3", + "callback_data": "v3" + } + ], + [ + { + "text": "t4", + "callback_data": "v4" + }, + { + "text": "t5", + "callback_data": "v5" + }, + { + "text": "t6", + "callback_data": "v6" + } + ], + [ + { + "text": "t7", + "callback_data": "v7" + }, + { + "text": "t8", + "callback_data": "v8" + } + ] + ] + } + }, + { + "json": { + "type": "carousel", + "content": [ + { + "title": "Card 1 title", + "subtitle": "Card 1 subtitle", + "imageUrl": "image1", + "buttons": [ + { + "title": "t1", + "value": "v1", + "type": "postback" + }, + { + "title": "t2", + "value": "v2", + "type": "web_url" + } + ] + }, + { + "title": "Card 2 title", + "subtitle": "Card 2 subtitle", + "imageUrl": "image2", + "buttons": [ + { + "title": "t3", + "value": "v3", + "type": "postback" + }, + { + "title": "t4", + "value": "v4", + "type": "postback" + } + ] + } + ] + }, + "expected": { + "body": [ + "*Card 1 title*\n[Card 1 subtitle](image1)", + "*Card 2 title*\n[Card 2 subtitle](image2)" + ], + "chatId": "", + "keyboard": [ + [ + { + "callback_data": "v1", + "text": "t1" + }, + { + "text": "t2", + "url": "v2" + } + ], + [ + { + "callback_data": "v3", + "text": "t3" + } + ], + [ + { + "callback_data": "v4", + "text": "t4" + } + ] + ], + "to": "", + "token": "", + "type": "carousel" + } + } +] diff --git a/src/channel_integrations/twilio/channel.js b/src/channel_integrations/twilio/channel.js new file mode 100644 index 0000000..e431087 --- /dev/null +++ b/src/channel_integrations/twilio/channel.js @@ -0,0 +1,88 @@ +import _ from 'lodash' +import crypto from 'crypto' +import superagent from 'superagent' +import superagentPromise from 'superagent-promise' + +import AbstractChannelIntegration from '../abstract_channel_integration' +import { BadRequestError, ForbiddenError, textFormatMessage } from '../../utils' + +const agent = superagentPromise(superagent, Promise) + +export default class Twilio extends AbstractChannelIntegration { + + validateChannelObject (channel) { + channel.phoneNumber = channel.phoneNumber.split(' ').join('') + + if (!channel.clientId) { + throw new BadRequestError('Parameter is missing: Client Id') + } else if (!channel.clientSecret) { + throw new BadRequestError('Parameter is missing: Client Secret') + } else if (!channel.serviceId) { + throw new BadRequestError('Parameter is missing: Service Id') + } else if (!channel.phoneNumber) { + throw new BadRequestError('Parameter is missing: Phone Number') + } + } + + authenticateWebhookRequest (req, res, channel) { + const signature = req.headers['x-twilio-signature'] + const webhook = channel.webhook + let str = webhook + _.forOwn(_.sortBy(Object.keys(req.body)), (value) => { + str += value + str += req.body[value] + }) + const hmac = crypto.createHmac('SHA1', channel.clientSecret).update(str).digest('base64') + if (signature !== hmac) { + throw new ForbiddenError() + } + } + + populateMessageContext (req) { + const { body } = req + + return { + chatId: `${body.To}${body.From}`, + senderId: body.From, + } + } + + parseIncomingMessage (conversation, message) { + return { + attachment: { + type: 'text', + content: message.Body, + }, + } + } + + formatOutgoingMessage (conversation, message, opts) { + const { chatId } = conversation + const to = opts.senderId + + let msg + try { + msg = textFormatMessage(message) + } catch (error) { + throw new BadRequestError('Message type non-supported by Twilio') + } + + return { chatId, to, ...msg } + } + + async sendMessage (conversation, message) { + const data = { + To: message.to, + Body: message.body, + From: conversation.channel.phoneNumber, + MessagingServiceSid: conversation.channel.serviceId, + } + const url + = `https://api.twilio.com/2010-04-01/Accounts/${conversation.channel.clientId}/Messages.json` + await agent('POST', url) + .auth(conversation.channel.clientId, conversation.channel.clientSecret) + .type('form') + .send(data) + } + +} diff --git a/src/channel_integrations/twilio/index.js b/src/channel_integrations/twilio/index.js new file mode 100644 index 0000000..17354e1 --- /dev/null +++ b/src/channel_integrations/twilio/index.js @@ -0,0 +1,3 @@ +import channel from './channel' + +module.exports = { channel, identifiers: ['twilio'] } diff --git a/src/channel_integrations/twilio/test/integration.js b/src/channel_integrations/twilio/test/integration.js new file mode 100644 index 0000000..395712e --- /dev/null +++ b/src/channel_integrations/twilio/test/integration.js @@ -0,0 +1,137 @@ +import chai from 'chai' +import { setupChannelIntegrationTests } from '../../../../test/tools' +import crypto from 'crypto' +import _ from 'lodash' +import nock from 'nock' + +const expect = chai.expect +const should = chai.should() + +const channelCreationParams = { + type: 'twilio', + slug: 'my-awesome-channel', + clientId: 'client-id', + clientSecret: 'client-secret', + serviceId: 'service-id', + phoneNumber: 'phone-number', +} + +describe('Twilio channel', () => { + + const { createChannel, updateChannel, + deleteChannel, sendMessageToWebhook } = setupChannelIntegrationTests() + + describe('Creation', () => { + it('should be successful with valid parameters', async () => { + const response = await createChannel(channelCreationParams) + const { results: result, message } = response.body + + expect(response.status).to.equal(201) + expect(message).to.equal('Channel successfully created') + expect(result.type).to.equal(channelCreationParams.type) + expect(result.slug).to.equal(channelCreationParams.slug) + /* eslint no-unused-expressions: 0 */ // --> OFF + expect(result.isErrored).to.be.false + expect(result.isActivated).to.be.true + }) + + it('should return 400 with invalid parameters', async () => { + try { + await createChannel({}) + should.fail() + } catch (error) { + expect(error.status).to.equal(400) + } + }) + }) + + describe('Update', () => { + it('should be successful if channel exists', async () => { + let response = await createChannel(channelCreationParams) + const channel = response.body.results + const newValues = JSON.parse(JSON.stringify(channelCreationParams)) + newValues.clientId = 'newclientId' + response = await updateChannel(channel, newValues) + expect(response.status).to.equal(200) + expect(response.body.results.clientId).to.equal(newValues.clientId) + }) + }) + + describe('Deletion', () => { + it('should be successful if channel exists', async () => { + let response = await createChannel(channelCreationParams) + const channel = response.body.results + response = await deleteChannel(channel) + expect(response.status).to.equal(200) + }) + }) + + describe('sending a message', () => { + + let channel + + beforeEach(async () => { + channel = (await createChannel(channelCreationParams)).body.results + }) + + it('should be successful with valid token', async () => { + const outgoingMessageCall = nock('https://api.twilio.com') + .post('/2010-04-01/Accounts/client-id/Messages.json').reply(200, {}) + const message = { To: 'recipient', From: 'sender', Body: 'Text' } + let signature = channel.webhook + /* eslint max-nested-callbacks: ["error", 4]*/ + _.forOwn(_.sortBy(Object.keys(message)), (key) => { + signature += key + signature += message[key] + }) + const hmac = crypto.createHmac('SHA1', channel.clientSecret) + .update(signature) + .digest('base64') + const headers = { 'x-twilio-signature': hmac } + const response = await sendMessageToWebhook(channel, message, headers) + expect(response.status).to.equal(200) + expect(outgoingMessageCall.isDone()).to.be.true + }) + + it('should not send a response for an empty incoming message', async () => { + const outgoingMessageCall = nock('https://api.twilio.com') + .post('/2010-04-01/Accounts/client-id/Messages.json').reply(200, {}) + const message = { To: 'recipient', From: 'sender' } + let signature = channel.webhook + /* eslint max-nested-callbacks: ["error", 4]*/ + _.forOwn(_.sortBy(Object.keys(message)), (key) => { + signature += key + signature += message[key] + }) + const hmac = crypto.createHmac('SHA1', channel.clientSecret) + .update(signature) + .digest('base64') + const headers = { 'x-twilio-signature': hmac } + const response = await sendMessageToWebhook(channel, message, headers) + expect(response.status).to.equal(200) + expect(outgoingMessageCall.isDone()).to.be.false + }) + + it('should return 401 with invalid token', async () => { + try { + const headers = { 'x-twilio-signature': 'invalid' } + const message = { To: 'recipient', From: 'sender' } + await sendMessageToWebhook(channel, message, headers) + should.fail() + } catch (error) { + expect(error.status).to.equal(401) + } + }) + + it('should return 401 without a token', async () => { + try { + const message = { To: 'recipient', From: 'sender' } + await sendMessageToWebhook(channel, message) + should.fail() + } catch (error) { + expect(error.status).to.equal(401) + } + }) + + }) +}) diff --git a/src/channel_integrations/twitter/channel.js b/src/channel_integrations/twitter/channel.js new file mode 100644 index 0000000..ed409dc --- /dev/null +++ b/src/channel_integrations/twitter/channel.js @@ -0,0 +1,297 @@ +import _ from 'lodash' + +import AbstractChannelIntegration from '../abstract_channel_integration' +import { logger, getTwitterWebhookToken, + deleteTwitterWebhook, postMediaToTwitterFromUrl } from '../../utils' +import { BadRequestError, ForbiddenError, StopPipeline } from '../../utils/errors' +import Twit from 'twit' +import { URL } from 'url' + +const VIDEO_AS_LINK_HOSTS = [ + 'youtube.com', + 'youtu.be', +] + +export default class Twitter extends AbstractChannelIntegration { + + validateChannelObject (channel) { + const params = ['consumerKey', 'consumerSecret', 'accessToken', 'accessTokenSecret', 'envName'] + params.forEach(param => { + if (!channel[param] || typeof channel[param] !== 'string') { + throw new BadRequestError('Bad parameter '.concat(param).concat(' : missing or not string')) + } + }) + } + + async afterChannelCreated (channel) { + const T = new Twit({ + consumer_key: channel.consumerKey, + consumer_secret: channel.consumerSecret, + access_token: channel.accessToken, + access_token_secret: channel.accessTokenSecret, + app_only_auth: true, + timeout_ms: 60 * 1000, + }) + + try { + // Get the previously set webhook + const res = await T.get(`account_activity/all/${channel.envName}/webhooks`, {}) + + if (!res.data || res.data.length === 0) { + throw new Error() + } + // Try to delete the webhook if there's one + await deleteTwitterWebhook(T, res.data[0].id, channel.envName) + } catch (err) { + logger.error(`[Twitter] Unable to get and delete previously set webhook: ${err}`) + } + + try { + T.config.app_only_auth = false + const res = await T.post(`account_activity/all/${channel.envName}/webhooks`, + { url: channel.webhook }) + const ret = res.data + + channel.isErrored = ret.valid !== true || !ret.url || !ret.id + if (channel.isErrored) { + throw new Error() + } + + await T + .post(`account_activity/all/${channel.envName}/subscriptions`, {}) + const account = await T.get('account/verify_credentials', {}) + channel.clientId = account.data.id_str + } catch (err) { + logger.error(`[Twitter] Unable to set the webhook: ${err}`) + channel.isErrored = true + } + + return channel.save() + } + + async afterChannelUpdated (channel, oldChannel) { + await this.afterChannelDeleted(oldChannel) + await this.beforeChannelCreated(channel) + } + + async afterChannelDeleted (channel) { + const T = new Twit({ + consumer_key: channel.consumerKey, + consumer_secret: channel.consumerSecret, + access_token: channel.accessToken, + access_token_secret: channel.accessTokenSecret, + timeout_ms: 60 * 1000, + }) + + try { + await deleteTwitterWebhook(T, channel.webhookToken) + } catch (err) { + logger.error(`[Twitter] Error while unsetting webhook: ${err}`) + } + } + + validateWebhookSubscriptionRequest (req, res, channel) { + if (!channel.consumerSecret) { + throw new BadRequestError('Error while checking webhook validity : no channel.consumerSecret') + } + + const crcToken = req.query.crc_token + const sha = getTwitterWebhookToken(channel.consumerSecret, crcToken) + res.status(200).json({ response_token: sha }) + } + + authenticateWebhookRequest (req, res, channel) { + const hash = req.headers['X-Twitter-Webhooks-Signature'] + || req.headers['x-twitter-webhooks-signature'] || '' + const test = getTwitterWebhookToken(channel.consumerSecret, req.rawBody) + + if (!hash.startsWith('sha256=') || hash !== test) { + throw new ForbiddenError('Invalid Twitter signature') + } + } + + populateMessageContext (req) { + const recipientId + = _.get(req, 'body.direct_message_events[0].message_create.target.recipient_id') + const senderId + = _.get(req, 'body.direct_message_events[0].message_create.sender_id') + + return { + chatId: `${recipientId}-${senderId}`, + senderId, + } + } + + parseIncomingMessage (conversation, message, opts) { + message = _.get(message, 'direct_message_events[0]') + const channel = conversation.channel + const senderId = _.get(message, 'message_create.sender_id') + const recipientId = _.get(message, 'message_create.target.recipient_id') + const data = _.get(message, 'message_create.message_data') + const quickReply = _.get(message, 'message_create.message_data.quick_reply_response.metadata') + + // can be an echo message + if (senderId !== opts.senderId + || senderId === channel.clientId + || recipientId !== channel.clientId + || !data) { + throw new StopPipeline() + } + + const msg = {} + const hasMedia = (_.get(data, 'attachment.type') === 'media') + if (quickReply) { + msg.attachment = { type: 'text', content: quickReply, is_button_click: true } + } else if (!hasMedia) { + msg.attachment = { type: 'text', content: _.get(data, 'text') } + } else { + const media = _.get(data, 'attachment.media') + let type = media.type + if (type === 'photo' || type === 'animated_gif') { + type = 'picture' + } else if (type === 'video') { + type = 'video' + } else { + throw new StopPipeline() + } + msg.attachment = { type, content: media.media_url } + } + return msg + } + + async formatOutgoingMessage (conversation, message, opts) { + const { type, content } = _.get(message, 'attachment') + let data = [{ text: '' }] + + const makeListElement = async ({ title, imageUrl, subtitle, buttons }) => { + const msg = {} + const mediaId = await postMediaToTwitterFromUrl(conversation.channel, imageUrl) + msg.attachment = { type: 'media', media: { id: mediaId } } + msg.text = `${title}\r\n` + if (subtitle) { + msg.text += subtitle.concat('\r\n') + } + buttons = buttons.map(({ title }) => `- ${title}`).join('\r\n') + msg.text += buttons + return msg + } + + if (type === 'text') { + data[0].text = content + } else if (type === 'video' || type === 'picture') { + let hostname = (new URL(content)).hostname + if (hostname.startsWith('www.')) { + hostname = hostname.slice(4, hostname.length) + } + if (type === 'video' && VIDEO_AS_LINK_HOSTS.indexOf(hostname) !== -1) { + data[0].text = content + } else { + const mediaId = await postMediaToTwitterFromUrl(conversation.channel, content) + data[0].attachment = { type: 'media', media: { id: mediaId } } + } + } else if (type === 'quickReplies') { + data[0].text = content.title + const context = content.buttons + .map(({ title, value }) => ({ label: title, metadata: value })) + data[0].quick_reply = { type: 'options', options: context } + } else if (type === 'card') { + const { title, subtitle, imageUrl, buttons } = content + const mediaId = await postMediaToTwitterFromUrl(conversation.channel, imageUrl) + data[0].attachment = { type: 'media', media: { id: mediaId } } + data[0].text = title + + data.push({}) + data[1] = { text: subtitle.length > 0 ? subtitle : '.' } + const context = buttons + .map(({ title, value }) => ({ label: title, metadata: value })) + data[1].quick_reply = { type: 'options', options: context } + } else if (type === 'carousel' || type === 'carouselle') { + data = await Promise.all(content + .map(makeListElement)) + } else if (type === 'list') { + data = await Promise.all(content.elements + .map(makeListElement)) + if (content.buttons) { + const context = content.buttons + .map(({ title, value }) => ({ label: title, metadata: value })) + data.push({ text: '_', quick_reply: { type: 'options', options: context } }) + } + } else if (type === 'buttons') { + data[0].text = content.title + data[0].ctas = content.buttons + .map(({ title, value }) => ({ type: 'web_url', label: title, url: value })) + } else if (type === 'custom') { + data = content + } else { + throw new BadRequestError('Message type non-supported by Twitter : '.concat(type)) + } + + const makeMessage = (data) => { + return { + event: { + type: 'message_create', + message_create: { + target: { recipient_id: opts.senderId }, + message_data: data, + }, + }, + } + } + + return data.map(makeMessage) + } + + async sendMessage (conversation, message) { + const channel = conversation.channel + const T = new Twit({ + consumer_key: channel.consumerKey, + consumer_secret: channel.consumerSecret, + access_token: channel.accessToken, + access_token_secret: channel.accessTokenSecret, + timeout_ms: 60 * 1000, + }) + + await T.post('direct_messages/events/new', message) + } + + /* + * Gromit methods + */ + + populateParticipantData (participant, channel) { + return new Promise((resolve, reject) => { + const T = new Twit({ + consumer_key: channel.consumerKey, + consumer_secret: channel.consumerSecret, + app_only_auth: true, + timeout_ms: 60 * 1000, + }) + + T.get('users/lookup', { user_id: participant.senderId }, (err, data) => { + if (err) { + logger.error(`[Twitter] Error when retrieving Twitter user info: ${err}`) + return reject(err) + } + if (data.length <= 0) { + const msg = '[Twitter] Error when retrieving Twitter user info: no data' + logger.error(msg) + return reject(new Error(msg)) + } + participant.data = data[0] + participant.markModified('data') + participant.save().then(resolve).catch(reject) + }) + }) + } + + parseParticipantDisplayName (participant) { + const informations = {} + + // could get quite a lot more information + if (participant.data) { + informations.userName = participant.data.name + } + + return informations + } +} diff --git a/src/channel_integrations/twitter/index.js b/src/channel_integrations/twitter/index.js new file mode 100644 index 0000000..34f6c79 --- /dev/null +++ b/src/channel_integrations/twitter/index.js @@ -0,0 +1,3 @@ +import channel from './channel' + +module.exports = { channel, identifiers: ['twitter'] } diff --git a/src/channel_integrations/twitter/test/integration.js b/src/channel_integrations/twitter/test/integration.js new file mode 100644 index 0000000..96504bb --- /dev/null +++ b/src/channel_integrations/twitter/test/integration.js @@ -0,0 +1,245 @@ +import chai from 'chai' +import { setupChannelIntegrationTests } from '../../../../test/tools' +import nock from 'nock' +import { Channel, Connector } from '../../../models' +import _ from 'lodash' + +/* eslint max-nested-callbacks: 0 */ // --> OFF + +const expect = chai.expect +const should = chai.should() +const channelCreationParams = { + type: 'twitter', + slug: 'my-awesome-channel', + consumerKey: 'consumerkey', + consumerSecret: 'consumersecret', + accessToken: 'accesstoken', + accessTokenSecret: 'accesstokensecret', +} + +describe('Twitter channel', () => { + + // Need to increase timeout, because channel creation contains hard-coded 4s delay + const creationTimeout = 10000 + const { createChannel, updateChannel, + deleteChannel, sendMessageToWebhook } = setupChannelIntegrationTests(false) + const twitterAPI = 'https://api.twitter.com' + + let channel + // Only create channel once to avoid long test runs due to hard-coded 4s delay in creation call + before(function (done) { + // Mock Twitter API calls + const webhook = { valid: true, id: 123, url: 'webhook' } + nock(twitterAPI).get('/1.1/account_activity/webhooks.json').query(true).reply(200, [{ id: 0 }]) + nock(twitterAPI).delete('/1.1/account_activity/webhooks/0.json').query(true).reply(200, {}) + nock(twitterAPI).post('/1.1/account_activity/webhooks.json').query(true).reply(200, webhook) + nock(twitterAPI).post('/1.1/account_activity/webhooks/123/subscriptions.json') + .query(true).reply(200, {}) + nock(twitterAPI).get('/1.1/account/verify_credentials.json').query(true).reply(200, { + data: { id_str: 'client-id' }, + }) + this.timeout(creationTimeout) + createChannel(channelCreationParams).then((result) => { + channel = result.body.results + done() + }) + }) + + after(async () => { + await Connector.remove() + await Channel.remove() + }) + + describe('Creation', () => { + it('should be successful with valid parameters', async () => { + expect(channel.type).to.equal(channelCreationParams.type) + expect(channel.slug).to.equal(channelCreationParams.slug) + /* eslint no-unused-expressions: 0 */ // --> OFF + expect(channel.isErrored).to.be.false + expect(channel.isActivated).to.be.true + }) + it('should return 400 with invalid parameters', async () => { + try { + await createChannel({}) + should.fail() + } catch (error) { + expect(error.status).to.equal(400) + } + }) + }) + + describe('Update', () => { + it('should be successful if channel exists', async () => { + const newValues = JSON.parse(JSON.stringify(channelCreationParams)) + newValues.accessToken = 'newtoken' + nock(twitterAPI).delete('/1.1/account_activity/webhooks/123.json').reply(200, {}) + const response = await updateChannel(channel, newValues) + expect(response.status).to.equal(200) + expect(response.body.results.accessToken).to.equal(newValues.accessToken) + }) + }) + + describe('Deletion', () => { + it('should be successful if channel exists', async () => { + const deletableChannel = _.cloneDeep(channelCreationParams) + deletableChannel.slug = 'to-be-deleted' + let response = await createChannel(deletableChannel) + const deleteMe = response.body.results + nock(twitterAPI).delete('/1.1/account_activity/webhooks/undefined.json').reply(200, {}) + nock(twitterAPI).post('/1.1/account_activity/webhooks/').reply(200, {}) + response = await deleteChannel(deleteMe) + expect(response.status).to.equal(200) + }) + }) + + describe('sending a message', () => { + + it('should return 401 with invalid token', async () => { + try { + await sendMessageToWebhook(channel, {}, { 'X-Twitter-Webhooks-Signature': 'invalid-token' }) + should.fail() + } catch (error) { + expect(error.status).to.equal(401) + expect(error.response.body.message).to.equal('Invalid Twitter signature') + } + }) + + it('should return 401 without token', async () => { + try { + await sendMessageToWebhook(channel, {}) + should.fail() + } catch (error) { + expect(error.status).to.equal(401) + expect(error.response.body.message).to.equal('Invalid Twitter signature') + } + }) + + describe('should be successful', () => { + + beforeEach(() => { + nock(twitterAPI).get('/1.1/users/lookup.json').query(true).reply(200, {}) + }) + + const body = { + direct_message_events: [ + { + message_create: { + sender_id: 456, + target: { + // recipient_id: channelCreationParams.clientId, + }, + message_data: { + text: 'a message', + }, + }, + }, + ], + } + const signature = 'sha256=9a0dmwmJem4AvLj2aKpSqcvWv9SAVGH0BWZkAPNsMjg=' + const headers = { 'X-Twitter-Webhooks-Signature': signature } + + it('in text format', async () => { + const response = await sendMessageToWebhook(channel, body, headers) + expect(response.status).to.equal(200) + }) + + // The twitter integration wants to upload attached image files to twitter + // Not sure how to mock this, so skipping those test cases for now + + it.skip('in list format', async () => { + const buttons = [ + { type: 'account_link', title: 'button title', value: 'https://link.com' }, + { type: 'web_url', title: 'button title', value: 'https://link.com' }, + { type: 'phone_number', title: 'button title', value: '0123554' }] + const listElement = { + title: 'title', + subtitle: 'subtitle', + imageUrl: 'https://image.png', + buttons, + } + const botResponse = { + results: {}, + messages: JSON.stringify([{ + type: 'list', + content: { elements: [listElement], buttons }, + }]), + } + const response = await sendMessageToWebhook(channel, body, headers, botResponse) + expect(response.status).to.equal(200) + }) + + it.skip('in card format', async () => { + const cardElement = { + title: 'title', + subtitle: 'subtitle', + imageUrl: 'https://img.url', + buttons: [{ type: '', title: 'button title', value: 'abc' }], + } + const botResponse = { + results: {}, + messages: JSON.stringify([{ type: 'card', content: cardElement }]), + } + const response = await sendMessageToWebhook(channel, body, headers, botResponse) + expect(response.status).to.equal(200) + }) + + it.skip('in carousel format', async () => { + const carouselElement = { + title: 'title', + imageUrl: 'https://img.url', + buttons: [{ type: '', title: 'button title', value: 'abc' }], + } + const botResponse = { + results: {}, + messages: JSON.stringify([{ type: 'carousel', content: [carouselElement] }]), + } + const response = await sendMessageToWebhook(channel, body, headers, botResponse) + expect(response.status).to.equal(200) + }) + + it('in quickReplies format', async () => { + const buttonsElement = { + title: 'title', + buttons: [{ type: '', title: 'button title', value: 'abc' }], + } + const botResponse = { + results: {}, + messages: JSON.stringify([{ type: 'quickReplies', content: buttonsElement }]), + } + const response = await sendMessageToWebhook(channel, body, headers, botResponse) + expect(response.status).to.equal(200) + }) + + it.skip('in video format', async () => { + const botResponse = { + results: {}, + messages: JSON.stringify([{ type: 'video', content: 'https://link.com' }]), + } + const response = await sendMessageToWebhook(channel, body, headers, botResponse) + expect(response.status).to.equal(200) + }) + + it.skip('in picture format', async () => { + const botResponse = { + results: {}, + messages: JSON.stringify([{ type: 'picture', content: 'https://link.com' }]), + } + const response = await sendMessageToWebhook(channel, body, headers, botResponse) + expect(response.status).to.equal(200) + }) + + it('in buttons format', async () => { + const buttonsElement = { + title: 'title', + buttons: [{ type: '', title: 'button title', value: 'abc' }], + } + const botResponse = { + results: {}, + messages: JSON.stringify([{ type: 'buttons', content: buttonsElement }]), + } + const response = await sendMessageToWebhook(channel, body, headers, botResponse) + expect(response.status).to.equal(200) + }) + }) + }) +}) diff --git a/src/channel_integrations/webchat/channel.js b/src/channel_integrations/webchat/channel.js new file mode 100644 index 0000000..3541624 --- /dev/null +++ b/src/channel_integrations/webchat/channel.js @@ -0,0 +1,49 @@ +import AbstractChannelIntegration from '../abstract_channel_integration' +import { getWebhookToken, sendToWatchers } from '../../utils' +import { Participant } from '../../models' +export default class Webchat extends AbstractChannelIntegration { + + async beforeChannelCreated (channel) { + channel.token = getWebhookToken(channel._id, channel.slug) + return channel.save() + } + + populateMessageContext (req) { + const { chatId } = req.body + + return { + chatId, + senderId: `p-${chatId}`, + } + } + + formatMarkdown (message) { + return message + } + + formatOutgoingMessage (conversation, message) { + return message + } + + parseIncomingMessage (conversation, body) { + const { attachment } = body.message + + if (attachment.type === 'button' || attachment.type === 'quickReply') { + attachment.type = 'text' + attachment.title = attachment.content.title + attachment.content = attachment.content.value + } + + return { attachment } + } + + getMemoryOptions (body) { + return body.memoryOptions || { memory: {}, merge: true } + } + + async sendMessage (conversation, message) { + message.participant = await Participant.findById(message.participant) + await sendToWatchers(conversation._id, [message.serialize]) + } + +} diff --git a/src/channel_integrations/webchat/index.js b/src/channel_integrations/webchat/index.js new file mode 100644 index 0000000..2864877 --- /dev/null +++ b/src/channel_integrations/webchat/index.js @@ -0,0 +1,3 @@ +import channel from './channel' + +module.exports = { channel, identifiers: ['webchat', 'recastwebchat'] } diff --git a/src/channel_integrations/webchat/test/integration.js b/src/channel_integrations/webchat/test/integration.js new file mode 100644 index 0000000..a89cbf4 --- /dev/null +++ b/src/channel_integrations/webchat/test/integration.js @@ -0,0 +1,68 @@ +import chai from 'chai' +import { setupChannelIntegrationTests } from '../../../../test/tools' + +const expect = chai.expect +const channelCreationParams = { + type: 'webchat', + slug: 'my-awesome-channel', +} + +describe('Webchat channel', () => { + + const { createChannel, deleteChannel, updateChannel, + sendMessageToWebhook } = setupChannelIntegrationTests() + + describe('Creation', () => { + it('should be successful with valid parameters', async () => { + const response = await createChannel(channelCreationParams) + const { results: result, message } = response.body + + expect(response.status).to.equal(201) + expect(message).to.equal('Channel successfully created') + expect(result.type).to.equal(channelCreationParams.type) + expect(result.slug).to.equal(channelCreationParams.slug) + /* eslint no-unused-expressions: 0 */ // --> OFF + expect(result.isErrored).to.be.false + expect(result.isActivated).to.be.true + }) + }) + + describe('Update', () => { + it('should be successful if channel exists', async () => { + let response = await createChannel(channelCreationParams) + const channel = response.body.results + const newValues = Object.assign({}, channelCreationParams) + newValues.token = 'newtoken' + response = await updateChannel(channel, newValues) + expect(response.status).to.equal(200) + expect(response.body.results.token).to.equal(newValues.token) + }) + }) + + describe('Deletion', () => { + it('should be successful if channel exists', async () => { + let response = await createChannel(channelCreationParams) + const channel = response.body.results + response = await deleteChannel(channel) + expect(response.status).to.equal(200) + }) + }) + + describe('sending a message', () => { + + let channel + + beforeEach(async () => { + channel = (await createChannel(channelCreationParams)).body.results + }) + + it('should be successful with valid parameters', async () => { + const response = await sendMessageToWebhook(channel, { + chatId: 123, + message: { attachment: { type: 'text', content: { value: 'a message' } } }, + }) + expect(response.status).to.equal(200) + }) + + }) +}) diff --git a/src/constants/channels.js b/src/constants/channels.js new file mode 100644 index 0000000..16d93c1 --- /dev/null +++ b/src/constants/channels.js @@ -0,0 +1,19 @@ +export const permitted = '{type,slug,isActivated,token,userName,apiKey,webhook,' + + 'clientId,clientSecret,botuser,password,phoneNumber,serviceId,consumerKey,' + + 'consumerSecret,accessToken,accessTokenSecret,envName,clientAppId,accentColor,' + + 'complementaryColor,botMessageColor,botMessageBackgroundColor,' + + 'backgroundColor,headerLogo,headerTitle,botPicture,userPicture,' + + 'onboardingMessage,userInputPlaceholder,expanderLogo,' + + 'expanderTitle,conversationTimeToLive,characterLimit,' + + 'characterLimit,openingType,welcomeMessage,refreshToken,' + + 'oAuthCode,invocationName,webchatLocale}' + +export const permittedUpdate = '{slug,isActivated,token,userName,apiKey,webhook,' + + 'clientId,clientSecret,botuser,password,phoneNumber,serviceId,consumerKey,' + + 'consumerSecret,accessToken,accessTokenSecret,clientAppId,accentColor,' + + 'complementaryColor,botMessageColor,botMessageBackgroundColor,' + + 'backgroundColor,headerLogo,headerTitle,botPicture,userPicture,' + + 'onboardingMessage,userInputPlaceholder,expanderLogo,expanderTitle,' + + 'conversationTimeToLive,characterLimit,' + + 'openingType,welcomeMessage,refreshToken,' + + 'invocationName,botEnvironmentId,webchatLocale}' diff --git a/src/constants/index.js b/src/constants/index.js new file mode 100644 index 0000000..9ca5413 --- /dev/null +++ b/src/constants/index.js @@ -0,0 +1 @@ +export ChannelsConstants from './Channels.constants' diff --git a/src/controllers/App.controller.js b/src/controllers/App.controller.js deleted file mode 100644 index ed7795b..0000000 --- a/src/controllers/App.controller.js +++ /dev/null @@ -1,9 +0,0 @@ -class AppController { - - static index (req, res) { - res.status(200).send('Hi!') - } - -} - -module.exports = AppController diff --git a/src/controllers/Channels.controller.js b/src/controllers/Channels.controller.js deleted file mode 100644 index e587047..0000000 --- a/src/controllers/Channels.controller.js +++ /dev/null @@ -1,130 +0,0 @@ -import filter from 'filter-object' - -import { - invoke, -} from '../utils' - -import { - NotFoundError, - ConflictError, -} from '../utils/errors' - -import { - renderOk, - renderCreated, - renderDeleted, -} from '../utils/responses' - -const permitted = '{type,slug,isActivated,token,userName,apiKey,webhook,clientId,clientSecret,botuser,password,phoneNumber,serviceId}' - -export default class ChannelsController { - - /** - * Create a new channel - */ - static async createChannelByConnectorId (req, res) { - const { connector_id } = req.params - const params = filter(req.body, permitted) - const { slug } = req.body - - const connector = await models.Connector.findById(connector_id).populate('channels') - - if (!connector) { throw new NotFoundError('Connector') } - - let channel = connector.channels.find(c => c.slug === slug) - if (channel) { throw new ConflictError('Channel slug is already taken') } - - channel = await new models.Channel({ ...params, connector: connector._id }) - channel.webhook = `${config.base_url}/webhook/${channel._id}` - connector.channels.push(channel._id) - - await Promise.all([ - connector.save(), - channel.save(), - ]) - await invoke(channel.type, 'onChannelCreate', [channel]) - - return renderCreated(res, { - results: channel.serialize, - message: 'Channel successfully created', - }) - } - - /** - * Index bot's channels - */ - static async getChannelsByConnectorId (req, res) { - const { connector_id } = req.params - - const connector = await models.Connector.findById(connector_id) - .populate('channels') - - if (!connector) { throw new NotFoundError('Connector') } - if (!connector.channels.length) { - return renderOk(res, { results: [], message: 'No channels' }) - } - - return renderOk(res, { - results: connector.channels.map(c => c.serialize), - message: 'Channels successfully rendered', - }) - } - - /** - * Show a channel - */ - static async getChannelByConnectorId (req, res) { - const { connector_id, channel_slug } = req.params - - const channel = await models.Channel.findOne({ connector: connector_id, slug: channel_slug }) - - if (!channel) { throw new NotFoundError('Channel') } - - return renderOk(res, { - results: channel.serialize, - message: 'Channel successfully rendered', - }) - } - - /** - * Update a channel - */ - static async updateChannelByConnectorId (req, res) { - const { connector_id, channel_slug } = req.params - - const oldChannel = await models.Channel.findOne({ slug: channel_slug, connector: connector_id }) - - if (!oldChannel) { throw new NotFoundError('Channel') } - - const channel = await models.Channel.findOneAndUpdate( - { slug: channel_slug, connector: connector_id }, - { $set: filter(req.body, permitted) }, - { new: true } - ) - if (!channel) { throw new NotFoundError('Channel') } - - await invoke(channel.type, 'onChannelUpdate', [channel, oldChannel]) - - renderOk(res, { - results: channel.serialize, - message: 'Channel successfully updated', - }) - } - - /** - * Delete a channel - */ - static async deleteChannelByConnectorId (req, res) { - const { connector_id, channel_slug } = req.params - - const channel = await models.Channel.findOne({ connector: connector_id, slug: channel_slug }) - if (!channel) { throw new NotFoundError('Channel') } - - await Promise.all([ - channel.remove(), - invoke(channel.type, 'onChannelDelete', [channel]), - ]) - - renderDeleted(res, 'Channel successfully deleted') - } -} diff --git a/src/controllers/Connectors.controller.js b/src/controllers/Connectors.controller.js deleted file mode 100644 index df54d25..0000000 --- a/src/controllers/Connectors.controller.js +++ /dev/null @@ -1,86 +0,0 @@ -import filter from 'filter-object' - -import { - NotFoundError, -} from '../utils/errors' - -import { - renderOk, - renderCreated, - renderDeleted, -} from '../utils/responses' - -const permittedAdd = '{url}' -const permittedUpdate = '{url}' - -export default class ConnectorsController { - - /** - * Create a new connector - */ - static async createConnector (req, res) { - const payload = filter(req.body, permittedAdd) - - const connector = await new models.Connector(payload).save() - - return renderCreated(res, { - results: connector.serialize, - message: 'Connector successfully created', - }) - } - - /** - * Show a connector - */ - static async getConnectorByBotId (req, res) { - const { connector_id } = req.params - - const connector = await models.Connector.findById(connector_id) - - if (!connector) { throw new NotFoundError('Connector') } - - return renderOk(res, { - results: connector.serialize, - message: 'Connector successfully found', - }) - } - - /** - * Update a connector - */ - static async updateConnectorByBotId (req, res) { - const { connector_id } = req.params - - const connector = await models.Connector.findOneAndUpdate({ _id: connector_id }, - { $set: filter(req.body, permittedUpdate) }, { new: true } - ).populate('channels') - - if (!connector) { throw new NotFoundError('Connector') } - - return renderOk(res, { - results: connector.serialize, - message: 'Connector successfully updated', - }) - } - - /** - * Delete a connector - */ - static async deleteConnectorByBotId (req, res) { - const { connector_id } = req.params - - const connector = await models.Connector.findById(connector_id) - .populate('channels conversations') - - if (!connector) { throw new NotFoundError('Connector') } - - await Promise.all([ - ...connector.conversations.map(c => c.remove()), - ...connector.channels.map(c => c.remove()), - connector.remove(), - ]) - - return renderDeleted(res, 'Connector successfully deleted') - } - -} diff --git a/src/controllers/Conversations.controller.js b/src/controllers/Conversations.controller.js deleted file mode 100644 index 0122b42..0000000 --- a/src/controllers/Conversations.controller.js +++ /dev/null @@ -1,86 +0,0 @@ -import { renderOk, renderDeleted } from '../utils/responses' -import { NotFoundError, BadRequestError } from '../utils/errors' - -export default class ConversationController { - - /* - * Index all connector conversations - */ - static async getConversationsByConnectorId (req, res) { - const { connector_id } = req.params - - const conversations = await models.Conversation.find({ connector: connector_id }) - - renderOk(res, { - results: conversations.map(c => c.serialize), - message: conversations.length ? 'Conversations rendered with success' : 'No conversations', - }) - } - - /* - * Show a conversation - */ - static async getConversationByConnectorId (req, res) { - const { connector_id, conversation_id } = req.params - - const conversation = await models.Conversation.findOne({ _id: conversation_id, connector: connector_id }).populate('participants messages') - - if (!conversation) { throw new NotFoundError('Conversation') } - - return renderOk(res, { - results: conversation.full, - message: 'Conversation rendered with success', - }) - } - - /* - * Delete a conversation - */ - static async deleteConversationByConnectorId (req, res) { - const { connector_id, conversation_id } = req.params - - const conversation = await models.Conversation.findOne({ _id: conversation_id, connector: connector_id }) - - if (!conversation) { throw new NotFoundError('Conversation') } - - await conversation.remove() - - renderDeleted(res, 'Conversation deleted with success') - } - - /* - * Find or create a conversation - */ - static async findOrCreateConversation (channelId, chatId) { - let conversation = await models.Conversation.findOne({ channel: channelId, chatId }) - .populate('channel') - .populate('connector', 'url _id') - .populate('participants') - .exec() - - if (conversation) { return conversation } - - const channel = await models.Channel.findById(channelId).populate('connector').exec() - - if (!channel) { - throw new NotFoundError('Channel') - } else if (!channel.isActivated) { - throw new BadRequestError('Channel is not activated') - } - - const { connector } = channel - - if (!connector) { - throw new NotFoundError('Connector') - } - - conversation = await new models.Conversation({ connector: connector._id, chatId, channel: channel._id }).save() - connector.conversations.push(conversation._id) - await models.Connector.update({ _id: connector._id }, { $push: { conversations: conversation._id } }) - - conversation.connector = connector - conversation.channel = channel - return conversation - } - -} diff --git a/src/controllers/Messages.controller.js b/src/controllers/Messages.controller.js deleted file mode 100644 index e96cecf..0000000 --- a/src/controllers/Messages.controller.js +++ /dev/null @@ -1,223 +0,0 @@ -import Logger from '../utils/Logger' -import { invoke, invokeSync } from '../utils' -import { isValidFormatMessage } from '../utils/format' -import { NotFoundError, BadRequestError, ServiceError } from '../utils/errors' -import { renderCreated } from '../utils/responses' -import _ from 'lodash' - -class MessagesController { - - static async pipeMessage (id, message, options) { - return controllers.Conversations.findOrCreateConversation(id, options.chatId) - .then(conversation => controllers.Messages.parseChannelMessage(conversation, message, options)) - .then(controllers.Messages.saveMessage) - .then(controllers.Webhooks.sendMessageToBot) - } - - /** - * Parse a message received - * from a channel to the BC format - */ - static parseChannelMessage (conversation, message, options) { - return invoke(conversation.channel.type, 'parseChannelMessage', [conversation, message, options]) - } - - /* Save a message in db and create the participant if necessary */ - static async saveMessage ([conversation, message, options]) { - let participant = _.find(conversation.participants, p => p.senderId === options.senderId) - - if (!participant) { - participant = await new models.Participant({ senderId: options.senderId }).save() - - await models.Conversation.update({ _id: conversation._id }, { $push: { participants: participant._id } }) - conversation.participants.push(participant) - } - - const newMessage = new models.Message({ - participant: participant._id, - conversation: conversation._id, - attachment: message.attachment, - }) - - conversation.messages.push(newMessage) - - return Promise.all([ - conversation, - newMessage.save(), - options, - models.Conversation.update({ _id: conversation._id }, { $push: { messages: newMessage._id } }), - ]) - } - - /** - * Check if all the messages received from the bot are well formatted - */ - static async bulkCheckMessages ([conversation, messages, opts]) { - if (!Array.isArray(messages)) { - throw new BadRequestError('Message is not well formated') - } - - for (const message of messages) { - if (!isValidFormatMessage(message)) { - throw new BadRequestError('Message is not well formated') - } - } - - return Promise.all([conversation, messages, opts]) - } - - /** - * Save an array of message in db - */ - static async bulkSaveMessages ([conversation, messages, opts]) { - let participant = _.find(conversation.participants, p => p.isBot) - if (!participant) { - participant = await new models.Participant({ senderId: conversation.connector._id, isBot: true }).save() - conversation.participants.push(participant) - } - - messages = await Promise.all(messages.map(attachment => { - const newMessage = new models.Message({ - participant: participant._id, - conversation: conversation._id, - attachment, - }) - conversation.messages.push(newMessage) - return newMessage.save() - })) - - return Promise.all([ - conversation.save(), - messages, - opts, - ]) - } - - /** - * Format an array of messages - */ - static bulkFormatMessages ([conversation, messages, options]) { - const channelType = conversation.channel.type - - messages = messages - .filter(message => !message.attachment.only || message.attachment.only.indexOf(channelType) !== -1) - .map(message => invokeSync(channelType, 'formatMessage', [conversation, message, options])) - - return Promise.resolve([conversation, messages, options]) - } - - /** - * Send an array of messages to the bot - */ - static async bulkSendMessages ([conversation, messages, opts]) { - const channelType = conversation.channel.type - - for (const message of messages) { - let err = null - - // Try 3 times to send the message - for (let i = 0; i < 3; i++) { - try { - await invoke(channelType, 'sendMessage', [conversation, message, opts]) - break - } catch (ex) { - // Wait 2000ms before trying to send the message again - await new Promise(resolve => setTimeout(resolve, 2000)) - err = ex - } - } - - if (err) { throw new ServiceError('Error while sending message', err) } - } - - return ([conversation, messages, opts]) - } - - /** - * Post from a bot to a channel - */ - static async postMessage (req, res) { - const { connector_id, conversation_id } = req.params - let { messages } = req.body - - if (!messages) { - throw new BadRequestError('Invalid \'messages\' parameter') - } else if (typeof messages === 'string') { - try { - messages = JSON.parse(messages) - } catch (err) { - throw new BadRequestError('Invalid \'messages\' parameter') - } - } - - const conversation = await models.Conversation.findOne({ _id: conversation_id, connector: connector_id }) - .populate('participants channel connector').exec() - - const participant = conversation.participants.find(p => !p.isBot) - if (!participant) { throw new NotFoundError('Participant') } - - const opts = { - senderId: participant.senderId, - chatId: conversation.chatId, - } - - await controllers.Messages.bulkCheckMessages([conversation, messages, opts]) - .then(controllers.Messages.bulkSaveMessages) - .then(controllers.Messages.bulkFormatMessages) - .then(controllers.Messages.bulkSendMessages) - - return renderCreated(res, { results: null, message: 'Messages successfully posted' }) - } - - static async postToConversation (conversation, messages) { - for (const participant of conversation.participants) { - if (participant.isBot) { continue } - - const opts = { - chatId: conversation.chatId, - senderId: participant.senderId, - } - - await new Promise((resolve, reject) => { - MessagesController.bulkSaveMessages([conversation, messages, opts]) - .then(MessagesController.bulkFormatMessages) - .then(MessagesController.bulkSendMessages) - .then(resolve) - .catch(reject) - }) - } - } - - /** - * Post message to a bot - */ - static async postMessages (req, res) { - const { connector_id } = req.params - let { messages } = req.body - - if (!messages || !Array.isArray(messages)) { - throw new BadRequestError('Invalid messages parameter') - } else if (typeof messages === 'string') { - try { - messages = JSON.parse(messages) - } catch (e) { - throw new BadRequestError('Invalid messages parameter') - } - } - - const connector = await models.Connector.findById(connector_id).populate('conversations') - - if (!connector) { throw new NotFoundError('Connector') } - - for (const conversation of connector.conversations) { - try { - await controllers.Messages.postToConversation(conversation, messages) - } catch (err) { - Logger.error('Error while broadcasting message', err) - } - } - renderCreated(res, { results: null, message: 'Messages successfully posted' }) - } -} - -module.exports = MessagesController diff --git a/src/controllers/Participants.controller.js b/src/controllers/Participants.controller.js deleted file mode 100644 index 59ef13f..0000000 --- a/src/controllers/Participants.controller.js +++ /dev/null @@ -1,40 +0,0 @@ -import { NotFoundError } from '../utils/errors' -import { renderOk } from '../utils/responses' - -export default class ParticipantController { - - /* - * Index all connector's participants - */ - static async getParticipantsByConnectorId (req, res) { - const { connector_id } = req.params - const results = [] - - const conversations = await models.Conversation.find({ connector: connector_id }).populate('participants') - - conversations.forEach(c => { - c.participants.forEach(p => results.push(p.serialize)) - }) - - return renderOk(res, { - results, - messages: results.length ? 'Participants successfully rendered' : 'No participants', - }) - } - - /* - * Show a participant - */ - static async getParticipantByConnectorId (req, res) { - const { participant_id } = req.params - - const participant = await models.Participant.findById(participant_id) - - if (!participant) { throw new NotFoundError('Participant') } - - return renderOk(res, { - results: participant.serialize, - message: 'Participant successfully rendered', - }) - } -} diff --git a/src/controllers/Webhooks.controller.js b/src/controllers/Webhooks.controller.js deleted file mode 100644 index 6fa6f11..0000000 --- a/src/controllers/Webhooks.controller.js +++ /dev/null @@ -1,76 +0,0 @@ -import request from 'superagent' - -import { invoke, invokeSync } from '../utils' -import { NotFoundError, BadRequestError } from '../utils/errors' - -export default class WebhooksController { - /** - * Receive a new message from a channel - * Retrieve the proper channel - * Invoke beforePipeline, extractOptions and checkSecurity - * Call the pipeline - */ - static async forwardMessage (req, res) { - const { channel_id } = req.params - let channel = await models.Channel.findById(channel_id).populate({ path: 'children' }) - - if (!channel) { - throw new NotFoundError('Channel') - } else if (!channel.isActivated) { - throw new BadRequestError('Channel is not activated') - } else if (!channel.type) { - throw new BadRequestError('Type is not defined') - } - - channel = await invoke(channel.type, 'beforePipeline', [req, res, channel]) - - const options = invokeSync(channel.type, 'extractOptions', [req, res, channel]) - invokeSync(channel.type, 'checkSecurity', [req, res, channel]) - - await controllers.Messages.pipeMessage(channel._id, req.body, options) - } - - /** - * Send a message to a bot - */ - static sendMessageToBot ([conversation, message, opts]) { - return new Promise((resolve, reject) => { - request.post(conversation.connector.url) - .set('Accept', 'application/json') - .set('Content-Type', 'application/json') - .send({ message, chatId: opts.chatId, senderId: opts.senderId }) - .end((err, response) => { - if (err) { return reject(err) } - return resolve([conversation, response.body, opts]) - }) - }) - } - - /** - * Send a message to a channel - */ - static async sendMessage ([conversation, messages, opts]) { - const channelType = conversation.channel.type - - for (const message of messages) { - await invoke(channelType, 'sendMessage', [conversation, message, opts]) - } - - return conversation - } - - // TODO Abstract it! - static async subscribeFacebookWebhook (req, res) { - const { channel_id } = req.params - - const channel = await models.Channel.findById(channel_id) - if (!channel) { throw new NotFoundError('Channel') } - - if (services.messenger.connectWebhook(req, channel)) { - res.status(200).send(req.query['hub.challenge']) - } else { - res.status(403).json({ results: null, message: 'Error while connecting the webhook' }) - } - } - -} diff --git a/src/controllers/application.js b/src/controllers/application.js new file mode 100644 index 0000000..7200b9b --- /dev/null +++ b/src/controllers/application.js @@ -0,0 +1,7 @@ +export default class AppController { + + static async index (req, res) { + return res.status(200).send('Hi!') + } + +} diff --git a/src/controllers/channels.js b/src/controllers/channels.js new file mode 100644 index 0000000..12a1f3a --- /dev/null +++ b/src/controllers/channels.js @@ -0,0 +1,187 @@ +import filter from 'filter-object' + +import * as channelConstants from '../constants/channels' +import { NotFoundError, ConflictError } from '../utils/errors' +import { renderOk, renderCreated, renderDeleted } from '../utils/responses' +import { Channel, Connector } from '../models' +import { getChannelIntegrationByIdentifier } from '../channel_integrations' + +export default class ChannelsController { + + /** + * Create a new channel + */ + static async create (req, res) { + const connector = await Connector.findOne({ _id: req.params.connectorId, isActive: true }) + + const params = filter(req.body, channelConstants.permitted) + const slug = params.slug + + if (!connector) { + throw new NotFoundError('Connector') + } else if (await Channel.findOne({ + connector: connector._id, + isActive: true, + slug })) { + throw new ConflictError('Channel slug is already taken') + } + const channel = await new Channel({ ...params, + connector: connector._id }) + const channelIntegration = getChannelIntegrationByIdentifier(channel.type) + channel.webhook = channelIntegration.buildWebhookUrl(channel) + await channelIntegration.beforeChannelCreated(channel, req) + await channel.save() + await channelIntegration.afterChannelCreated(channel, req) + + return renderCreated(res, { + results: channel.serialize, + message: 'Channel successfully created', + }) + } + + /** + * Index channels + */ + static async index (req, res) { + const connector = await Connector.findOne({ _id: req.params.connectorId, isActive: true }) + + if (!connector) { + throw new NotFoundError('Connector') + } + + const channels = await Channel.find({ + connector: connector._id, + }) + + return renderOk(res, { + results: channels.map(c => c.serialize), + message: channels.length ? 'Channels successfully rendered' : 'No channels', + }) + } + + /** + * Show a channel + */ + static async show (req, res) { + const connector = await Connector.findOne({ _id: req.params.connectorId, isActive: true }) + const channel_slug = req.params.channel_slug + + if (!connector) { + throw new NotFoundError('Connector') + } + + const channel = await Channel.findOne({ + slug: channel_slug, + connector: connector._id, + isActive: true, + }).populate('children') + + if (!channel) { + throw new NotFoundError('Channel') + } + + return renderOk(res, { + results: channel.serialize, + message: 'Channel successfully rendered', + }) + } + + /** + * Index a channel's redirection_groups + */ + static async getCRMRedirectionGroups (req, res) { + const connector = await Connector.findOne({ _id: req.params.connectorId, isActive: true }) + const channel_slug = req.params.channel_slug + + if (!connector) { + throw new NotFoundError('Connector') + } + + const channel = await Channel.findOne({ + slug: channel_slug, + connector: connector._id, + isActive: true, + }) + + if (!channel) { + throw new NotFoundError('Channel') + } + const channelIntegration = getChannelIntegrationByIdentifier(channel.type) + const redirectionGroups = await channelIntegration.getCRMRedirectionGroups(channel) + return renderOk(res, { + results: redirectionGroups, + message: 'Redirection groups successfully rendered', + }) + } + + /** + * Update a channel + */ + static async update (req, res) { + const connector = await Connector.findOne({ _id: req.params.connectorId, isActive: true }) + const channel_slug = req.params.channel_slug + const slug = req.body.slug + const params = filter(req.body, channelConstants.permittedUpdate) + + if (!connector) { + throw new NotFoundError('Connector') + } + + const oldChannel = await Channel + .findOne({ connector: connector._id, isActive: true, slug: channel_slug }) + + if (slug && slug !== channel_slug && await Channel.findOne({ + connector: connector._id, + isActive: true, + slug, + })) { + throw new ConflictError('Channel slug is already taken') + } + + const channel = await Channel.findOneAndUpdate( + { slug: channel_slug, connector: connector._id, isActive: true }, + { $set: { ...params } }, + { new: true } + ) + + if (!channel) { + throw new NotFoundError('Channel') + } + const channelIntegration = getChannelIntegrationByIdentifier(channel.type) + await channelIntegration.afterChannelUpdated(channel, oldChannel) + + return renderOk(res, { + results: channel.serialize, + message: 'Channel successfully updated', + }) + } + + /** + * Delete a channel + */ + static async delete (req, res) { + const connector = await Connector.findOne({ _id: req.params.connectorId, isActive: true }) + const channel_slug = req.params.channel_slug + + if (!connector) { + throw new NotFoundError('Connector') + } + + const channel = await Channel + .findOne({ connector: connector._id, isActive: true, slug: channel_slug }) + if (!channel) { + throw new NotFoundError('Channel') + } + const channelIntegration = getChannelIntegrationByIdentifier(channel.type) + await channelIntegration.beforeChannelDeleted(channel) + + channel.isActive = false + await Promise.all([ + channel.save(), + channelIntegration.afterChannelDeleted(channel), + ]) + + return renderDeleted(res, 'Channel successfully deleted') + } + +} diff --git a/src/controllers/connectors.js b/src/controllers/connectors.js new file mode 100644 index 0000000..f28aac0 --- /dev/null +++ b/src/controllers/connectors.js @@ -0,0 +1,122 @@ +import filter from 'filter-object' + +import { BadRequestError, NotFoundError } from '../utils/errors' +import { renderOk, renderCreated, renderDeleted } from '../utils/responses' +import { Connector, Channel, Conversation } from '../models' + +const permittedAdd = '{url,isTyping,defaultDelay}' +const permittedUpdate = '{url,isTyping,defaultDelay}' + +export default class Connectors { + + /** + * Create a new connector + */ + static async create (req, res) { + const payload = filter(req.body, permittedAdd) + + const connector = await new Connector(payload).save() + const result = connector.serialize + result.conversations = [] + result.channels = [] + + return renderCreated(res, { + results: result, + message: 'Connector successfully created', + }) + } + + /** + * Show a connector + */ + static async show (req, res) { + const connector = await Connector.findOne({ _id: req.params.connectorId, isActive: true }) + + if (!connector) { + throw new NotFoundError('Connector') + } + + const result = connector.serialize + result.channels = await Channel.find({ connector: connector._id, isActive: true }) + result.channels = result.channels.map(c => c.serialize || c) + if (req.query.light !== 'true') { + result.conversations = await Conversation.find( + { connector: connector._id }, + { _id: 1 }) + result.conversations = result.conversations.map(c => c._id) + } + + return renderOk(res, { + results: result, + message: 'Connector successfully found', + }) + } + + /* + * Update a connector + */ + static async update (req, res) { + const connector = await Connector.findOne({ _id: req.params.connectorId, isActive: true }) + + if (!connector) { + throw new NotFoundError('Connector') + } + + const delay = req.body.defaultDelay + // isNaN(null) is a Number + if (delay !== undefined && (isNaN(delay) || delay < 0 || delay > 5)) { + throw new BadRequestError('defaultDelay parameter is invalid') + } + + const updatedConnector = await Connector + .findOneAndUpdate({ _id: connector._id }, + { $set: filter(req.body, permittedUpdate) }, { new: true } + ) + + const result = updatedConnector.serialize + result.channels = await Channel.find({ connector: connector._id, isActive: true }) + result.channels = result.channels.map(c => c.serialize || c) + result.conversations = await Conversation.find( + { connector: connector._id }, + { _id: 1 }) + result.conversations = result.conversations.map(c => c._id) + + return renderOk(res, { + results: result, + message: 'Connector successfully updated', + }) + } + + /** + * Delete a connector + */ + static async delete (req, res) { + const connector = await Connector.findOne({ _id: req.params.connectorId, isActive: true }) + + if (!connector) { + throw new NotFoundError('Connector') + } + + await recursiveDestroy(connector) + await Connector.updateOne({ _id: connector._id }, { $set: { isActive: false } }) + + return renderDeleted(res, 'Connector successfully deleted') + } + +} + +/* + * Helpers + */ + +/** + * Recursively delete all the channels and the conversations associated to a connector + */ +const recursiveDestroy = async (connector) => { + await Channel.update({ connector: connector._id }, + { $set: { isActive: false } }, + { multi: true }) + await Conversation.update({ connector: connector._id }, + { $set: { isActive: false } }, + { multi: true }) +} diff --git a/src/controllers/conversations.js b/src/controllers/conversations.js new file mode 100644 index 0000000..4345da0 --- /dev/null +++ b/src/controllers/conversations.js @@ -0,0 +1,190 @@ +import _ from 'lodash' +import moment from 'moment' +import archiver from 'archiver' + +import { renderOk, renderDeleted } from '../utils/responses' +import { logger, fmtConversationHeader, sendMail, + sendArchiveByMail, fmtMessageDate } from '../utils' +import { NotFoundError } from '../utils/errors' +import { Connector, Conversation, Message, Participant } from '../models' + +export default class ConversationController { + + static async index (req, res) { + const connector = await Connector.findOne({ _id: req.params.connectorId, isActive: true }) + + if (!connector) { + throw new NotFoundError('Connector') + } + + const conversations = await Conversation + .find({ connector: connector._id, isActive: true }) + + return renderOk(res, { + results: conversations.map(c => c.serialize), + message: conversations.length ? 'Conversations successfully found' : 'No conversations', + }) + } + + static async show (req, res) { + const connector = await Connector.findOne({ _id: req.params.connectorId, isActive: true }) + const conversation_id = req.params.conversation_id + + if (!connector) { + throw new NotFoundError('Connector') + } + + const conversation = await Conversation.findOne({ + _id: conversation_id, + connector: connector._id, + isActive: true, + }) + + if (!conversation) { + throw new NotFoundError('Conversation') + } + + const result = conversation.full + result.participants = await Participant.find({ conversation: conversation._id }) + result.participants = result.participants.map(p => p.serialize) + result.messages = await Message.find({ + conversation: conversation._id, + isActive: true, + }).sort('receivedAt') + result.messages = result.messages.map(m => m.serialize) + + return renderOk(res, { + results: result, + message: 'Conversation successfully found', + }) + } + + static async delete (req, res) { + const connector = await Connector.findOne({ _id: req.params.connectorId, isActive: true }) + const conversation_id = req.params.conversation_id + + if (!connector) { + throw new NotFoundError('Connector') + } + + const conversation = await Conversation.findOne({ + _id: conversation_id, + connector: connector._id, + isActive: true, + }) + + if (!conversation) { + throw new NotFoundError('Conversation') + } + + conversation.isActive = false + await conversation.save() + + return renderDeleted(res, 'Conversation successfully deleted') + } + + /* + * Accepted parameters + * - by: 'zip' || 'mail' - either returns the conversations as an archive in the + * request response, or send it by mail to the adresses specified + * - to: "jerome.houdan@sap.com,jerome.houdan@gmail.com" - a list of email adresses + * separated by a comma + * - from: "jerome.houdan@sap.com" - an email adress used as the sender + * - populate: true || false - determine if we add the participant information and + * the reception date of the messages or not + */ + static async dumpDelete (req, res) { + const dump = [] + const connector = await Connector.findOne({ _id: req.params.connectorId, isActive: true }) + const { to, from, populate } = req.body + const by = req.body.by || 'mail' + const cutoff = moment().subtract(12, 'hours') + let allConversations = '' + + if (!connector) { + throw new NotFoundError('Connector') + } + + const convModels = await Conversation + .find({ connector: connector._id, createdAt: { $lt: cutoff } }, 'createdAt') + + const conversations = await Promise.all(convModels.map(async (conv) => { + const participants = await Participant.find({ conversation: conv._id }) + const messages = await Message + .find({ conversation: conv._id }, { attachment: 1, participant: 1, receivedAt: 1 }) + .populate('participant', 'isBot') + .sort('receivedAt') + return { conv, participants, messages } + })) + + conversations + .filter(c => c.messages.length) + .forEach(({ conv, participants, messages }, i) => { + const conversationHeader = populate + ? fmtConversationHeader(conv, participants) + : '' + + const content = messages.reduce((tmp, m) => { + const messageContent = _.get(m, 'attachment.content', 'Empty message') + const content = typeof messageContent === 'string' + ? messageContent + : `${messageContent.title}\n${messageContent.buttons.map(b => b.title).join(' | ')}` + + const participantType = _.get(m, 'participant.isBot', false) ? 'BOT' : 'USER' + + return populate + ? `${tmp}${participantType} ${fmtMessageDate(m)} > ${content}\n` + : `${tmp}${participantType} > ${content}\n` + }, conversationHeader) + allConversations += `${content}\n\n---------------------------\n\n` + dump.push({ content: new Buffer(content, 'utf-8'), filename: `conversation-${i}.txt` }) + }) + + if (dump.length && by === 'mail') { + dump.push({ content: new Buffer(allConversations, 'utf-8'), filename: 'conversations.txt' }) + await sendArchiveByMail({ + to, + from, + subject: 'SAP Conversational AI daily logs', + text: 'Bonjour\n\nCi-joint les logs quotidiens du chatbot.\n\nCordialement,', + attachments: dump, + }) + } else if (by === 'zip') { + const archive = archiver('zip') + res.attachment('conversations.zip') + + archive.on('end', () => res.end()) + archive.pipe(res) + + for (const file of dump) { + archive.append(file.content, { name: file.filename }) + } + + archive.finalize() + } else { + await sendMail({ + to, + from, + subject: `Bot Connector ${connector._id}: Daily logs`, + text: 'Bonjour\n\nIl n\'y a pas de logs de conversation aujourd\'hui.\n\nCordialement,', + }) + } + + const conversationIds = conversations.map(c => c.conv._id) + try { + // Remove all the conversations, along with the messages and the + // participants belonging to the conversations + await Promise.all([ + Conversation.remove({ _id: { $in: conversationIds } }), + Message.remove({ conversation: { $in: conversationIds } }), + Participant.remove({ conversation: { $in: conversationIds } }), + ]) + } catch (err) { + logger.error(`Error while deleting conversations: ${err}`) + } + + if (by !== 'zip') { + return renderDeleted(res) + } + } +} diff --git a/src/controllers/get_started_buttons.js b/src/controllers/get_started_buttons.js new file mode 100644 index 0000000..a446d7e --- /dev/null +++ b/src/controllers/get_started_buttons.js @@ -0,0 +1,154 @@ +import { renderOk, renderCreated, renderDeleted } from '../utils/responses' +import { Channel, Connector, GetStartedButton } from '../models' +import { getChannelIntegrationByIdentifier } from '../channel_integrations' +import { NotFoundError, renderError } from '../utils' +import { ConflictError } from '../utils/errors' + +export default class GetStartedButtonController { + /** + * Create a new GetStartedButton + */ + static async create (req, res) { + const connector = await Connector.findOne({ _id: req.params.connectorId, isActive: true }) + if (!connector) { + throw new NotFoundError('Connector') + } + + const channel_id = req.params.channel_id + const channel = await Channel.findOne({ + connector: connector._id, + _id: channel_id, + }) + if (!channel) { + throw new NotFoundError('Channel') + } + + const getStarted = await GetStartedButton.findOne({ + channel_id: channel._id, + }) + if (getStarted) { + throw new ConflictError('A GetStartedButton already exists') + } + const getStartedButtonValue = req.body.value + + const channelIntegration = getChannelIntegrationByIdentifier(channel.type) + try { + await channelIntegration.setGetStartedButton(channel, getStartedButtonValue, connector) + } catch (err) { + return renderError(res, err) + } + const getStartedButton = await new GetStartedButton({ + channel_id: channel._id, + value: getStartedButtonValue, + }) + await getStartedButton.save() + channel.hasGetStarted = true + await channel.save() + + return renderCreated(res, { + results: getStartedButton.serialize, + message: 'GetStartedButton successfully created', + }) + } + + /** + * Show a GetStartedButton + */ + static async show (req, res) { + const connector = await Connector.findOne({ _id: req.params.connectorId, isActive: true }) + if (!connector) { + throw new NotFoundError('Connector') + } + + const channel_id = req.params.channel_id + const channel = await Channel.findOne({ + connector: connector._id, + _id: channel_id, + }) + if (!channel) { + throw new NotFoundError('Channel') + } + + const getStartedButton = await GetStartedButton.findOne({ channel_id: channel._id }) + if (!getStartedButton) { + throw new NotFoundError('GetStartedButton') + } + + return renderOk(res, { + results: getStartedButton.serialize, + message: 'getStartedButton successfully rendered', + }) + } + + /** + * Update a GetStartedButton + */ + static async update (req, res) { + const connector = await Connector.findOne({ _id: req.params.connectorId, isActive: true }) + if (!connector) { + throw new NotFoundError('Connector') + } + const channel_id = req.params.channel_id + const channel = await Channel.findOne({ + connector: connector._id, + _id: channel_id, + }) + if (!channel) { + throw new NotFoundError('Channel') + } + + const getStartedButton = await GetStartedButton.findOne({ channel_id: channel._id }) + if (!getStartedButton) { + throw new NotFoundError('GetStartedButton') + } + const newValue = req.body.value + const channelIntegration = getChannelIntegrationByIdentifier(channel.type) + try { + await channelIntegration.setGetStartedButton(channel, newValue) + } catch (err) { + return renderError(res, err) + } + getStartedButton.value = newValue + await getStartedButton.save() + + return renderOk(res, { + results: getStartedButton.serialize, + message: 'GetStartedButton successfully updated', + }) + } + + /** + * Delete a GetStartedButton + */ + static async delete (req, res) { + const connector = await Connector.findOne({ _id: req.params.connectorId, isActive: true }) + if (!connector) { + throw new NotFoundError('Connector') + } + const channel_id = req.params.channel_id + const channel = await Channel.findOne({ + connector: connector._id, + _id: channel_id, + }) + if (!channel) { + throw new NotFoundError('Channel') + } + + const getStartedButton = await GetStartedButton.findOne({ channel_id: channel._id }) + if (!getStartedButton) { + throw new NotFoundError('GetStartedButton') + } + + const channelIntegration = getChannelIntegrationByIdentifier(channel.type) + try { + await channelIntegration.deleteGetStartedButton(channel) + } catch (err) { + return renderError(res, err) + } + await getStartedButton.remove() + channel.hasGetStarted = false + await channel.save() + + return renderDeleted(res, 'GetStartedButton successfully deleted') + } +} diff --git a/src/controllers/index.js b/src/controllers/index.js deleted file mode 100644 index a66faa9..0000000 --- a/src/controllers/index.js +++ /dev/null @@ -1,17 +0,0 @@ -import App from './App.controller' -import Connectors from './Connectors.controller' -import Channels from './Channels.controller' -import Messages from './Messages.controller' -import Webhooks from './Webhooks.controller' -import Conversations from './Conversations.controller' -import Participants from './Participants.controller' - -export default { - App, - Connectors, - Channels, - Messages, - Webhooks, - Conversations, - Participants, -} diff --git a/src/controllers/message_pipe.js b/src/controllers/message_pipe.js new file mode 100644 index 0000000..87a19f1 --- /dev/null +++ b/src/controllers/message_pipe.js @@ -0,0 +1,173 @@ +import _ from 'lodash' +import MessageController from './messages' +import { getChannelIntegrationByIdentifier } from '../channel_integrations' +import WebhookController from './webhooks' +import { Channel, Conversation, Message, Participant } from '../models' +import { logger, sendToWatchers } from '../utils' +import { BadRequestError, NotFoundError } from '../utils/errors' + +/** + * Encapsulates handling of a message sent to a channel's webhook. + * Currently a WIP: First move all "controller" and "util" functions into this class, then refactor. + */ +class MessagePipe { + constructor (channel, context) { + this.channel = channel + this.messageContext = context + this.channelIntegration = getChannelIntegrationByIdentifier(channel.type) + } + + /** + * Runs the message pipe for a given input message + * @param message The message to be processed + * @return {Promise<*>} The response object returned by the bot + */ + async run (message) { + let conversation = await this.findOrCreateConversation() + conversation = this.channelIntegration.updateConversationContextFromMessage( + conversation, message) + await conversation.save() + + this.messageContext.mentioned = true + const parsedMessage = await this.parseIncomingMessage(conversation, message) + const memoryOptions = await this.getMemoryOptions(message) + const savedMessage = await this.saveMessage(conversation, parsedMessage) + await this.sendMessageToWatchers(conversation, savedMessage) + if (this.channel.connector.isTyping) { + const echo = _.get(message, 'entry[0].messaging[0]', {}) + // don't send isTyping if the message is an echo from facebook + if (this.channel.type !== 'messenger' || (echo.message && !(echo.is_echo && echo.app_id))) { + await this.channelIntegration.onIsTyping( + this.channel, this.messageContext) + } + } + + const botResponse = await WebhookController.sendMessageToBot( + [conversation, savedMessage, memoryOptions, this.messageContext] + ) + // save original response from rafiki / custom bot to context so it can be used later if needed + this.messageContext.originalBotResponse = botResponse.results || botResponse + return this.sendRepliesToChannel(conversation, botResponse) + } + + async sendMessageToWatchers (conversation, message) { + if (['webchat', 'recastwebchat'].includes(conversation.channel.type)) { + try { + message.participant = await Participant.findById(message.participant) + } catch (err) { + logger.error('Could not populate participant', err) + } + sendToWatchers(conversation._id, [message.serialize]) + } + } + + async parseIncomingMessage (conversation, message) { + return this.channelIntegration.parseIncomingMessage( + conversation, + message, + this.messageContext) + } + + async getMemoryOptions (message) { + return this.channelIntegration.getMemoryOptions(message, this.messageContext) + } + + async findOrCreateConversation () { + const chatId = this.messageContext.chatId + const channelId = this.channel._id + let conversation + = await Conversation.findOne({ channel: channelId, chatId, isActive: true }) + .populate('channel') + .populate('connector') + .exec() + + if (conversation && conversation.isActive) { + return conversation + } + + const channel = await Channel.findById(channelId).populate('connector').exec() + + if (!channel || !channel.isActive) { + throw new NotFoundError('Channel') + } else if (!channel.isActivated) { + throw new BadRequestError('Channel is not activated') + } + + const connector = channel.connector + + if (!connector) { + throw new NotFoundError('Connector') + } + + conversation = await new Conversation({ + connector: connector._id, + chatId, + channel: channel._id, + }).save() + + conversation.connector = connector + conversation.channel = channel + return conversation + } + + async sendRepliesToChannel (conversation, replies) { + const { results } = replies + let { messages } = replies + // Rafiki sends a json with an object "results" wrapping data + // Since we decided that it would be weird for users to do the same thing + // We look for the object "messages" both at the root and in results + if (results && results.messages) { + messages = results.messages + _.set(this.messageContext, 'memory', _.get(results, 'conversation.memory', {})) + } else { + _.set(this.messageContext, 'memory', _.get(replies, 'conversation.memory', {})) + } + + if (!messages) { + return [] + } + messages = MessageController.checkAndTransformMessages(messages) + return MessageController.sendMessagesToChannel( + [conversation, messages, this.messageContext]) + } + + async saveMessage (conversation, message) { + let participant = await Participant.findOne({ + conversation: conversation._id, + senderId: this.messageContext.senderId, + }) + + if (!participant) { + participant = await new Participant({ + conversation: conversation._id, + senderId: this.messageContext.senderId, + type: 'user', + }).save() + } + try { + const updatedParticipant + = await this.channelIntegration.populateParticipantData(participant, conversation.channel) + participant = updatedParticipant + } catch (err) { + logger.error(`Unable to get user infos: ${err}`) + } + + const newMessage = new Message({ + participant: participant._id, + conversation: conversation._id, + attachment: message.attachment, + }) + + if (_.get(message, 'newMessage.attachment.delay')) { + newMessage.delay = newMessage.attachment.delay + } + + this.messageContext.participantData + = this.channelIntegration.parseParticipantDisplayName(participant) + + return newMessage.save() + } + +} + +export default MessagePipe diff --git a/src/controllers/messages.js b/src/controllers/messages.js new file mode 100644 index 0000000..1bd0bc3 --- /dev/null +++ b/src/controllers/messages.js @@ -0,0 +1,257 @@ +import _ from 'lodash' +import { logger } from '../utils' +import { renderCreated } from '../utils/responses' +import { isValidFormatMessage } from '../utils/format' +import { BadRequestError, NotFoundError, ServiceError } from '../utils/errors' +import { Connector, Conversation, Message, Participant } from '../models' +import MessageController from '../controllers/messages' +import { getChannelIntegrationByIdentifier } from '../channel_integrations' + +export default class MessagesController { + + static checkAndTransformMessages (messages) { + if (!messages) { + throw new BadRequestError('Invalid \'messages\' parameter') + } else if (typeof messages === 'string') { + try { + messages = JSON.parse(messages) + } catch (err) { + throw new BadRequestError('Invalid \'messages\' parameter') + } + } + return messages + } + + static async sendMessagesToChannel ([conversation, messages, context]) { + let participant = await Participant.findOne({ + conversation: conversation._id, + isBot: true, + type: context.type, + }) + + if (!participant) { + participant = await new Participant({ + conversation: conversation._id, + senderId: conversation.connector._id, + isBot: true, + type: context.type, + }).save() + } + + messages = await MessageController.bulkSetCorrectDelayForMessages([conversation, messages]) + const returned_messages = [] + for (const message of messages) { + await MessageController.checkMessage([conversation, message, context]) + .then(async ([conversation, message, context]) => { + [conversation, message, context] + = await MessageController.saveMessage(conversation, message, context, participant) + return [conversation, message, context, message.delay] + }) + .then(MessageController.formatMessage) + .then(([conversation, message, context, delay]) => { + returned_messages.push(message) + return MessageController.sendMessage([conversation, message, context, delay]) + }) + } + return returned_messages + } + + static async postMessage (req, res) { + const connector = await Connector.findOne({ _id: req.params.connectorId, isActive: true }) + let messages = req.body.messages + const conversationId = req.params.conversationId + + if (!connector) { + throw new NotFoundError('Connector') + } + + messages = MessageController.checkAndTransformMessages(messages) + + const conversation = await Conversation + .findOne({ _id: conversationId, connector: connector._id }) + .populate('channel connector') + .exec() + + if (!conversation) { throw new NotFoundError('Conversation') } + + const participant = await Participant + .findOne({ conversation: conversation._id, isBot: false }) + if (!participant) { throw new NotFoundError('Participant') } + + const context = { + senderId: participant.senderId, + chatId: conversation.chatId, + } + + await MessageController.sendMessagesToChannel([conversation, messages, context]) + + return renderCreated(res, { results: null, message: 'Messages successfully posted' }) + } + + static async broadcastMessage (req, res) { + const connector = await Connector.findOne({ _id: req.params.connectorId, isActive: true }) + let messages = req.body.messages + + if (!messages || !Array.isArray(messages)) { + throw new BadRequestError('Invalid messages parameter') + } else if (typeof messages === 'string') { + try { + messages = JSON.parse(messages) + } catch (e) { + throw new BadRequestError('Invalid messages parameter') + } + } + + if (!connector) { + throw new NotFoundError('Connector') + } + + const conversations + = await Conversation.find({ connector: connector._id, isActive: true }) + for (const conversation of conversations) { + try { + await MessageController.postToConversation(conversation, messages) + } catch (err) { + logger.error(`Error while broadcasting message: ${err}`) + } + } + + return renderCreated(res, { results: null, message: 'Messages successfully posted' }) + } + + /* + * Helpers + */ + + /** + * Check if the message received is well formatted + */ + static async checkMessage ([conversation, message, context]) { + if (!isValidFormatMessage(message)) { + throw new BadRequestError('Message is not well formatted') + } + + return Promise.all([conversation, message, context]) + } + + /** + * Save a message in db + */ + static async saveMessage (conversation, message, context, participant) { + if (!context.type) { + context.type = 'bot' + } + + const now = Date.now() + + const newMessage = new Message({ + participant: participant._id, + conversation: conversation._id, + attachment: message, + receivedAt: now, + }) + if (message.delay) { + newMessage.delay = message.delay + } + if (message.markdown) { + newMessage.markdown = message.markdown + } + await newMessage.save() + + return Promise.all([ + conversation, + newMessage, + context, + ]) + } + + /** + * Extract the delay from the message and format the message. + * Go through each message, and set the message delay + * either to the specific delay provided in the message + * or to the default delay if it's not the last message + */ + static async bulkSetCorrectDelayForMessages ([conversation, messages]) { + const messages_length = messages.length + messages.map((message, i) => { + let message_delay = message.delay + if (!message_delay && message_delay !== 0 && conversation.connector.defaultDelay) { + if (i === messages_length - 1) { + message_delay = 0 + } else { + message_delay = conversation.connector.defaultDelay + } + } else { + if (!_.isFinite(message_delay) || message_delay < 0) { + message_delay = 0 + } + if (message_delay > 5) { + message_delay = 5 + } + } + message.delay = message_delay + return message + }) + + return messages + } + + /** + * Format a message + */ + static async formatMessage ([conversation, message, context, delay]) { + const channelType = conversation.channel.type + + if (message.attachment.only && !message.attachment.only.indexOf(channelType) !== -1) { + return Promise.resolve() + } + const channelIntegration = getChannelIntegrationByIdentifier(channelType) + let formattedMessage = message.markdown ? channelIntegration.formatMarkdown(message) : message + + formattedMessage = await channelIntegration.formatOutgoingMessage(conversation, formattedMessage, context) + return Promise.resolve([conversation, formattedMessage, context, delay]) + } + + static async delayNextMessage (delay, conversation, channelIntegration, context) { + if (delay) { + if (conversation.connector.isTyping) { + await channelIntegration.onIsTyping(conversation.channel, context) + } + return new Promise(resolve => setTimeout(resolve, delay * 1000)) + } + } + + /** + * Send a message to the user + */ + static async sendMessage ([conversation, message, context, delay]) { + const channelType = conversation.channel.type + const channelIntegration = getChannelIntegrationByIdentifier(channelType) + try { + await channelIntegration.sendMessage(conversation, message, context) + await MessageController.delayNextMessage(delay, conversation, channelIntegration, context) + } catch (err) { + logger.error('Failed to send messages', err) + throw new ServiceError('Error while sending message', err) + } + } + + static async postToConversation (conversation, messages) { + const participants + = await Participant.find({ conversation: conversation._id, isBot: false }) + for (const participant of participants) { + const context = { + chatId: conversation.chatId, + senderId: participant.senderId, + } + + messages = await MessageController.bulkSetCorrectDelayForMessages([conversation, messages]) + for (const message of messages) { + await MessagesController.saveMessage([conversation, message, context], participant) + .then(MessagesController.formatMessage) + .then(MessagesController.sendMessage) + } + } + } + +} diff --git a/src/controllers/participants.js b/src/controllers/participants.js new file mode 100644 index 0000000..13cd3cc --- /dev/null +++ b/src/controllers/participants.js @@ -0,0 +1,51 @@ +import { renderOk } from '../utils/responses' +import { NotFoundError } from '../utils/errors' +import { Connector, Conversation, Participant } from '../models' + +export default class ParticipantController { + + static async index (req, res) { + const connector = await Connector.findOne({ _id: req.params.connectorId, isActive: true }) + + if (!connector) { + throw new NotFoundError('Connector') + } + + const conversationsIds = await Conversation + .find({ connector: connector._id, isActive: true }, { _id: 1 }) + + const participants = await Participant + .find({ conversation: { $in: conversationsIds.map(c => c._id) } }) + + return renderOk(res, { + results: participants.map(p => p.serialize), + message: participants.length ? 'Participants successfully rendered' : 'No participants', + }) + } + + static async show (req, res) { + const connector = await Connector.findOne({ _id: req.params.connectorId, isActive: true }) + const participant_id = req.params.participant_id + + if (!connector) { + throw new NotFoundError('Connector') + } + + const participant = await Participant.findById(participant_id) + if (!participant) { + throw new NotFoundError('Participant') + } + + const conversation = await Conversation + .findOne({ _id: participant.conversation, isActive: true }) + if (!conversation || conversation.connector !== connector._id) { + throw new NotFoundError('Participant') + } + + return renderOk(res, { + results: participant.serialize, + message: 'Participant successfully rendered', + }) + } + +} diff --git a/src/controllers/persistent_menus.js b/src/controllers/persistent_menus.js new file mode 100644 index 0000000..0a86ef2 --- /dev/null +++ b/src/controllers/persistent_menus.js @@ -0,0 +1,226 @@ +import { renderOk, renderCreated, renderDeleted } from '../utils/responses' +import { Channel, Connector, PersistentMenu } from '../models' +import { getChannelIntegrationByIdentifier } from '../channel_integrations' +import { NotFoundError } from '../utils' +import { ConflictError } from '../utils/errors' + +export default class PersistentMenuController { + /** + * Create a new Persistent menu for a given language + */ + static async create (req, res) { + const connector = await Connector.findOne({ _id: req.params.connectorId, isActive: true }) + if (!connector) { + throw new NotFoundError('Connector') + } + if (await PersistentMenu.findOne({ + connector_id: connector._id, + locale: req.body.language, + })) { + throw new ConflictError('A persistent menu already exists for this language') + } + // Retrieving all the channels to be updated with the new menu + const channels = await Channel.find({ connector: connector._id, isActive: true }) + + const newMenu = await new PersistentMenu({ + connector_id: connector._id, + menu: req.body.menu, + locale: req.body.language, + }) + // Retrieving existing menus for other languages + const existingMenus = await PersistentMenu.find({ connector_id: connector._id }) + if (!existingMenus.length) { + newMenu.default = true + } + existingMenus.push(newMenu) + await Promise.all(channels.map(async (channel) => { + try { + const channelIntegration = getChannelIntegrationByIdentifier(channel.type) + await channelIntegration.setPersistentMenu(channel, existingMenus) + } catch (err) {} //eslint-disable-line + })) + + await newMenu.save() + + return renderCreated(res, { + results: newMenu.serialize, + message: 'PersistentMenu successfully created', + }) + } + + /** + * Index persistent menus for all languages + */ + static async index (req, res) { + const connector = await Connector.findOne({ _id: req.params.connectorId, isActive: true }) + if (!connector) { + throw new NotFoundError('Connector') + } + const existingMenus = await PersistentMenu.find({ connector_id: connector._id }) + + return renderOk(res, { + results: existingMenus.map(m => m.serialize), + message: existingMenus.length + ? 'Persistent menus successfully rendered' : 'No Persistent menu', + }) + } + + /** + * Show a persistent menu for a language + */ + static async show (req, res) { + const connector = await Connector.findOne({ _id: req.params.connectorId, isActive: true }) + if (!connector) { + throw new NotFoundError('Connector') + } + const menu = await PersistentMenu.findOne({ + connector_id: connector.id, + locale: req.params.language, + }) + if (!menu) { + throw new NotFoundError('PersistentMenu') + } + + return renderOk(res, { + results: menu.serialize, + message: 'PersistentMenu successfully rendered', + }) + } + + /** + * set the given language menu to default + */ + static async setDefault (req, res) { + const connector = await Connector.findOne({ _id: req.params.connectorId, isActive: true }) + if (!connector) { + throw new NotFoundError('Connector') + } + const language = req.body.language + // throw error if language menu does not exist + if (!await PersistentMenu.findOneAndUpdate( + { connector_id: connector._id, locale: language }, + { $set: { default: true } })) { + throw new NotFoundError('PersistentMenu') + } + // unset previous default menu + await PersistentMenu.findOneAndUpdate( + { connector_id: connector._id, default: true, locale: { $ne: language } }, + { $set: { default: false } }) + const existingMenus = await PersistentMenu.find({ connector_id: connector._id }) + + const channels = await Channel.find({ connector: connector._id, isActive: true }) + + await Promise.all(channels.map(async (channel) => { + try { + const channelIntegration = getChannelIntegrationByIdentifier(channel.type) + await channelIntegration.setPersistentMenu(channel, existingMenus) + } catch (err) {} //eslint-disable-line + })) + + return renderOk(res, { + message: 'Default menu successfully updated', + }) + } + + /** + * Update a menu for a given language + */ + static async update (req, res) { + const connector = await Connector.findOne({ _id: req.params.connectorId, isActive: true }) + if (!connector) { + throw new NotFoundError('Connector') + } + + const language = req.params.language + const persistentMenu = await PersistentMenu.findOne({ + connector_id: connector._id, + locale: language, + }) + if (!persistentMenu) { + throw new NotFoundError('PersistentMenu') + } + persistentMenu.menu = req.body.menu + const existingMenus = await PersistentMenu.find({ + connector_id: connector._id, + locale: { $ne: language }, + }) + existingMenus.push(persistentMenu) + const channels = await Channel.find({ connector: connector._id, isActive: true }) + await Promise.all(channels.map(async (channel) => { + try { + const channelIntegration = getChannelIntegrationByIdentifier(channel.type) + await channelIntegration.setPersistentMenu(channel, existingMenus) + } catch (e) {} //eslint-disable-line + })) + + persistentMenu.markModified('menu') + await persistentMenu.save() + + return renderOk(res, { + results: persistentMenu.serialize, + message: 'PersistentMenu successfully updated', + }) + } + + /** + * Delete a persistent menu for a given language + * If it's the only menu for the connector, delete the property + * Else, update the property to all channels + */ + static async delete (req, res) { + const connector = await Connector.findOne({ _id: req.params.connectorId, isActive: true }) + if (!connector) { + throw new NotFoundError('Connector') + } + const language = req.params.language + const menu = await PersistentMenu.findOne({ + connector_id: connector._id, + locale: language, + }) + if (!menu) { + throw new NotFoundError('PersistentMenu') + } else if (menu.default === true) { + // if deleted menu is default, set another menu to default + await PersistentMenu.findOneAndUpdate( + { connector_id: connector._id, default: false }, + { $set: { default: true } }) + } + const existingMenus = await PersistentMenu.find({ + connector_id: connector._id, + locale: { $ne: language }, + }) + + const channels = await Channel.find({ connector: connector._id, isActive: true }) + await Promise.all(channels.map(async (channel) => { + try { + const channelIntegration = getChannelIntegrationByIdentifier(channel.type) + if (!existingMenus.length) { + await channelIntegration.deletePersistentMenu(channel) + } else { + await channelIntegration.setPersistentMenu(channel, existingMenus) + } + } catch (e) {} //eslint-disable-line + })) + + await menu.remove() + + return renderDeleted(res, 'Persistent Menu successfully deleted') + } + + static async deleteAll (req, res) { + const connector = await Connector.findOne({ _id: req.params.connectorId, isActive: true }) + if (!connector) { + throw new NotFoundError('Connector') + } + await PersistentMenu.deleteMany({ connector_id: connector._id }) + const channels = await Channel.find({ connector: connector._id, isActive: true }) + await Promise.all(channels.map(async (channel) => { + try { + const channelIntegration = getChannelIntegrationByIdentifier(channel.type) + await channelIntegration.deletePersistentMenu(channel) + } catch (e) {} //eslint-disable-line + })) + + return renderDeleted(res, 'Persistent Menu successfully deleted') + } +} diff --git a/src/controllers/webhooks.js b/src/controllers/webhooks.js new file mode 100644 index 0000000..74156ca --- /dev/null +++ b/src/controllers/webhooks.js @@ -0,0 +1,316 @@ +import request from 'superagent' +import uuidV4 from 'uuid/v4' +import _ from 'lodash' + +import { logger, renderPolledMessages } from '../utils' +import { renderCreated, renderOk } from '../utils/responses' +import { BadRequestError, ForbiddenError, NotFoundError } from '../utils/errors' +import { Channel, Conversation, Message, Participant, PersistentMenu } from '../models' +import WebhookController from '../controllers/webhooks' +import messageQueue from '../utils/message_queue' +import { getChannelIntegrationByIdentifier } from '../channel_integrations' +import MessagePipe from './message_pipe' + +export default class WebhooksController { + + static async serviceHandleMethodAction (req, res) { + const channel_type = req.params.channel_type + const channelIntegration = getChannelIntegrationByIdentifier(channel_type) + if (!channelIntegration) { + throw new BadRequestError('Channel type does not exist') + } + const messageMethods = channelIntegration.webhookHttpMethods() + /* Requests made on methods declared in 'webhookHttpMethods' for a channel type + will be treated as an incoming message */ + if (messageMethods.includes(req.method)) { + const identityPairs = channelIntegration.getIdPairsFromSharedWebhook(req, res) + if (!identityPairs) { + throw new NotFoundError('Channel identity') + } + const channel = await Channel + .find({ + ...identityPairs, + isActive: true, + }) + .sort({ createdAt: -1 }) + .limit(1) + .populate({ path: 'children connector' }) + .cursor() + .next() + if (!channel) { + throw new NotFoundError('Channel') + } + + await WebhookController.forwardMessage(req, res, channel) + return + } + + /* Requests made on other methods will be treated as a subscription request */ + await WebhookController.serviceSubscribeWebhook(req, res, channel_type) + } + + static async serviceSubscribeWebhook (req, res, channel_type) { + const channelIntegration = getChannelIntegrationByIdentifier(channel_type) + return channelIntegration.onSharedWebhookChecking(req, res) + } + + static async handleMethodAction (req, res) { + const channel_id = req.params.channel_id + const channel = await Channel + .findById(channel_id) + .populate({ path: 'children connector' }) + + if (!channel) { + throw new NotFoundError('Channel') + } + const channelIntegration = getChannelIntegrationByIdentifier(channel.type) + const messageMethods = channelIntegration.webhookHttpMethods() + /* Requests made on methods declared in 'webhookHttpMethods' for a channel type + will be treated as an incoming message */ + if (messageMethods.includes(req.method)) { + await WebhookController.forwardMessage(req, res, channel) + return + } + + /* Requests made on other methods will be treated as a subscription request */ + try { + await WebhookController.subscribeWebhook(req, res, channel) + } catch (e) { + throw new BadRequestError('Unimplemented service method') + } + + } + + static async forwardMessage (req, res, channel) { + if (!channel.isActivated) { + throw new BadRequestError('Channel is not activated') + } + const channelIntegration = getChannelIntegrationByIdentifier(channel.type) + await channelIntegration.authenticateWebhookRequest(req, res, channel) + channel = await channelIntegration.onWebhookCalled(req, res, channel) + const context = await channelIntegration.populateMessageContext(req, res, channel) + const message = channelIntegration.getRawMessage(channel, req, context) + const pipe = new MessagePipe(channel, context) + const botResponse = await pipe.run(message) + channelIntegration.finalizeWebhookRequest(req, res, context, botResponse) + if (!res.finished) { + const warning + = `${channel.type} channel did not finalize webhook request. Sending default response` + logger.warning(warning) + res.status(200).json({ results: null, message: 'Message successfully received' }) + } + } + + static async subscribeWebhook (req, res, channel) { + const channelIntegration = getChannelIntegrationByIdentifier(channel.type) + return channelIntegration.validateWebhookSubscriptionRequest(req, res, channel) + } + + static async createConversation (req, res) { + const authorization = req.headers.authorization + const channel_id = req.params.channel_id + const channel = await Channel.findById(channel_id) + .populate('connector').exec() + + if (!channel) { throw new NotFoundError('Channel') } + if (!['webchat', 'recastwebchat'].includes(channel.type)) { + throw new BadRequestError('Invalid channel type') + } + if (channel.token !== authorization) { throw new ForbiddenError() } + + const conversation = await new Conversation({ + connector: channel.connector._id, + channel: channel._id, + chatId: 'tmp', + }).save() + conversation.chatId = conversation._id.toString() + + await conversation.save() + + if (channel.forwardConversationStart) { + const message = { message: { attachment: { type: 'conversation_start', content: '' } } } + const context = { + chatId: conversation.chatId, + senderId: `p-${conversation.chatId}`, + } + const pipe = new MessagePipe(channel, context) + await pipe.run(message) + } + + const result = conversation.full + result.participants = await Participant.find({ conversation: conversation._id }) + result.participants = result.participants.map(p => p.serialize) + result.messages = await Message + .find({ conversation: conversation._id, isActive: true }) + .sort('receivedAt') + result.messages = result.messages.map(m => m.serialize) + + return renderCreated(res, { + results: result, + message: 'Conversation successfully created', + }) + } + + static async getMessages (req, res) { + const authorization = req.headers.authorization + const channel_id = req.params.channel_id + const conversation_id = req.params.conversation_id + const channel = await Channel.findById(channel_id) + + if (!channel) { throw new NotFoundError('Channel') } + if (channel.token !== authorization) { throw new ForbiddenError() } + + const conversation = await Conversation + .findOne({ channel: channel_id, _id: conversation_id }) + if (!conversation) { throw new NotFoundError('Conversation') } + + const messages = await Message + .find({ conversation: conversation._id }) + .populate('participant') + .sort('receivedAt') + + return renderOk(res, { + message: 'Messages successfully fetched', + results: messages.map(m => m.serialize), + }) + } + + static async poll (req, res) { + const authorization = req.headers.authorization + const channel_id = req.params.channel_id + const conversation_id = req.params.conversation_id + const last_message_id = req.query.last_message_id + const channel = await Channel.findById(channel_id) + + if (!channel) { throw new NotFoundError('Channel') } + if (!['webchat', 'recastwebchat'].includes(channel.type)) { + throw new BadRequestError('Invalid channel type') + } + if (channel.token !== authorization) { throw new ForbiddenError() } + + const conversation + = await Conversation.findOne({ channel: channel_id, _id: conversation_id }) + if (!conversation) { throw new NotFoundError('Conversation') } + + let since = conversation.createdAt - 1 + if (last_message_id) { + const lastMessage = await Message.findOne({ + conversation: conversation._id, + _id: last_message_id, + }) + if (!lastMessage) { throw new NotFoundError('Message') } + since = lastMessage.receivedAt + } + + WebhookController.watchConversation(req, res, conversation._id, since) + } + + static closeRequestClosure (res, convId, watcherId) { + let closed = false + return (messages, waitTime) => { + if (closed) { return } + closed = true + waitTime = waitTime ? waitTime : 0 + renderPolledMessages(res, messages, waitTime) + messageQueue.removeWatcher(convId, watcherId) + } + } + + static async watchConversation (req, res, convId, since) { + const watcherId = uuidV4() + const closeRequest = WebhookController.closeRequestClosure(res, convId, watcherId) + req.once('close', () => closeRequest([])) + setTimeout(() => closeRequest([]), 30 * 1000) + messageQueue.setWatcher(convId, watcherId, closeRequest) + // important to query the db after we set a watcher, otherwise we might miss messages + const newMessages = await Message + .find({ conversation: convId, receivedAt: { $gt: since } }) + .sort('receivedAt') + .populate('participant') + if (newMessages.length > 0) { return closeRequest(newMessages.map(m => m.serialize)) } + // more than two minutes ago, wait for two minutes + if (Date.now() - since > 2 * 60 * 1000) { + return closeRequest([], 120) + } + } + + static async getPreferences (req, res) { + const authorization = req.headers.authorization + const channel_id = req.params.channel_id + const channel = await Channel.findById(channel_id) + + if (!channel) { throw new NotFoundError('Channel') } + if (!['webchat', 'recastwebchat'].includes(channel.type)) { + throw new BadRequestError('Invalid channel type') + } + if (channel.token !== authorization) { throw new ForbiddenError() } + + const preferences = { + accentColor: channel.accentColor, + complementaryColor: channel.complementaryColor, + botMessageColor: channel.botMessageColor, + botMessageBackgroundColor: channel.botMessageBackgroundColor, + backgroundColor: channel.backgroundColor, + headerLogo: channel.headerLogo, + headerTitle: channel.headerTitle, + botPicture: channel.botPicture, + userPicture: channel.userPicture, + onboardingMessage: channel.onboardingMessage, + userInputPlaceholder: channel.userInputPlaceholder, + expanderLogo: channel.expanderLogo, + expanderTitle: channel.expanderTitle, + conversationTimeToLive: channel.conversationTimeToLive, + openingType: channel.openingType, + welcomeMessage: channel.welcomeMessage, + characterLimit: channel.characterLimit, + } + + try { + const locale = channel.webchatLocale + preferences.menu = locale + ? await PersistentMenu.findOne({ connector_id: channel.connector.id, locale }) + : await PersistentMenu.findOne({ connector_id: channel.connector.id, default: true }) + } catch (err) { + console.log('preferences', err) // eslint-disable-line + } + + renderOk(res, { + message: 'Preferences successfully rendered', + results: preferences, + }) + } + + static sendMessageToBot ([conversation, message, memoryOptions, context]) { + // Don't send a message to the bot if the message is empty (Nil) + if (_.isEmpty(_.get(message, 'attachment.content', null))) { + return new Promise((resolve) => resolve({})) + } + + const participantData = context.participantData + const { chatId, senderId, mentioned } = context + const origin = conversation.channel.type + + if (participantData) { + message.data = participantData + } + + const headers = { + Accept: 'application/json', + 'Content-Type': 'application/json', + } + + return request.post(conversation.connector.url) + .set(headers) + .send({ + message, + chatId, + senderId, + mentioned, + origin, + memory: memoryOptions.memory, + merge_memory: memoryOptions.merge, + }) + .then((response) => response.body) + } + +} diff --git a/src/index.js b/src/index.js index a860708..e53b860 100644 --- a/src/index.js +++ b/src/index.js @@ -1,72 +1,7 @@ -import express from 'express' -import mongoose from 'mongoose' -import bodyParser from 'body-parser' -import _ from 'lodash' +import { logger } from './utils' +import { startApplication } from './app' +import config from '../config' -import configs from '../config' -import { initServices, Logger } from './utils' - -const app = express() - -// Load the mongoose Schemas - -import _models from './models' -import _controllers from './controllers' -import _services from './services' - -global.models = _models -global.controllers = _controllers -global.services = {} - -_.forOwn(_services, (service, serviceName) => { - services[serviceName.toLowerCase()] = service -}) - -const createRouter = require('./routes').createRouter - -// Load the configuration -global.config = configs - -// Enable Cross Origin Resource Sharing -app.use((req, res, next) => { - res.header('Access-Control-Allow-Origin', '*') - res.header('Access-Control-Allow-Headers', '*, X-Expiry, X-Client, X-Access-Token, X-Uuid, Content-Type, Authorization') - res.header('Access-Control-Expose-Headers', 'X-Client, X-Access-Token, X-Expiry, X-Uuid') - res.header('Access-Control-Allow-Methods', 'GET, DELETE, POST, PUT, OPTIONS') - res.header('Cache-Control', 'private, no-cache, no-store, must-revalidate') - res.header('Expires', '-1') - res.header('Pragma', 'no-cache') - next() -}) - -// Enable auto parsing of json content -app.use(bodyParser.json()) -app.use(bodyParser.urlencoded({ extended: true })) - -// Use native promise API with mongoose -mongoose.Promise = global.Promise - -// Mongoose connection -let dbUrl = 'mongodb://' - -if (config.db.username) { - dbUrl = `${dbUrl}${config.db.username}:${config.db.password}@` -} -dbUrl = `${dbUrl}${config.db.host}:${config.db.port}/${config.db.dbName}?ssl=${config.db.ssl || 'false'}` - -mongoose.connect(dbUrl) -const db = mongoose.connection -db.on('error', err => { - Logger.error('FAILED TO CONNECT', err) - process.exit(1) -}) - -// Launch the application -db.once('open', () => { - createRouter(app) - initServices() - app.listen(config.server.port, () => { - app.emit('ready') - Logger.info(`App is running and listening to port ${config.server.port}`) - }) -}) +startApplication().then( + () => logger.info(`App is running and listening to port ${config.server.port}`), + (err) => logger.error(`Failed to start app: ${err}`)) diff --git a/src/models/Channel.model.js b/src/models/Channel.model.js deleted file mode 100644 index 516b5a9..0000000 --- a/src/models/Channel.model.js +++ /dev/null @@ -1,60 +0,0 @@ -import mongoose from 'mongoose' -import uuidV4 from 'uuid/v4' -import _ from 'lodash' - -import { getWebhookToken } from '../utils' - -const ChannelSchema = new mongoose.Schema({ - _id: { type: String, default: uuidV4 }, - connector: { type: String, ref: 'Connector', required: true }, - slug: { type: String, required: true }, - type: { type: String, required: true }, - isErrored: { type: Boolean, required: true, default: false }, - isActivated: { type: Boolean, required: true, default: true }, - - token: String, - clientId: String, - clientSecret: String, - botuser: String, - userName: String, - password: String, - serviceId: String, - phoneNumber: String, - apiKey: String, - webhook: String, - oAuthUrl: String, - webhookToken: String, - app: { type: String, ref: 'Channel' }, - children: [{ type: String, ref: 'Channel' }], -}, { - timestamps: true, -}) - -async function generateUUID (next) { - if (this.isNew) { - while (await models.Channel.findOne({ _id: this._id })) { - this._id = uuidV4() - } - } - next() -} - -ChannelSchema.pre('save', generateUUID) - -ChannelSchema.virtual('serialize').get(function () { - // Filter the content of the Channel to keep only the initialized field - const filteredChannel = _.pickBy(this.toObject(), (value) => value) - delete filteredChannel._id - - return { - id: this._id, - ...filteredChannel, - isActivated: this.isActivated, - isErrored: this.isErrored, - webhookToken: this.type === 'messenger' ? getWebhookToken(this._id, this.slug) : this.webhookToken, - } -}) - -const Channel = mongoose.model('Channel', ChannelSchema) - -module.exports = Channel diff --git a/src/models/channel.js b/src/models/channel.js new file mode 100644 index 0000000..5abb5f5 --- /dev/null +++ b/src/models/channel.js @@ -0,0 +1,145 @@ +import _ from 'lodash' +import uuidV4 from 'uuid/v4' +import mongoose from 'mongoose' + +import { getWebhookToken } from '../utils' +import slug from 'slug' + +export const slugify = str => slug(str, { lower: true, replacement: '-' }) + +const ChannelSchema = new mongoose.Schema({ + _id: { type: String, default: uuidV4 }, + connector: { type: String, ref: 'Connector', required: true }, + slug: { type: String, required: true }, + type: { type: String, required: true }, + isErrored: { type: Boolean, required: true, default: false }, + isActive: { type: Boolean, required: true, default: true }, + isActivated: { type: Boolean, required: true, default: true }, + forwardConversationStart: { type: Boolean, default: false }, + hasGetStarted: { type: Boolean, default: false }, + + token: String, + clientAppId: String, + clientId: String, + clientSecret: String, + consumerKey: String, + consumerSecret: String, + accessToken: String, + accessTokenSecret: String, + envName: String, + bearerToken: String, + botuser: String, + userName: String, + password: String, + serviceId: String, + phoneNumber: String, + apiKey: String, + webhook: String, + oAuthUrl: String, + webhookToken: String, + refreshToken: String, + app: { type: String, ref: 'Channel' }, + children: [{ type: String, ref: 'Channel' }], + + /* + * Fields used for the Webchat channel configuration + */ + accentColor: String, + webchatLocale: { type: String }, + complementaryColor: String, + botMessageColor: String, + botMessageBackgroundColor: String, + backgroundColor: String, + headerLogo: String, + headerTitle: String, + botPicture: String, + userPicture: String, + onboardingMessage: String, + expanderLogo: String, + expanderTitle: String, + conversationTimeToLive: Number, + welcomeMessage: String, + openingType: { type: String, enum: ['memory', 'never', 'always'], default: 'never' }, + characterLimit: Number, + // if channel type is webchat and there's nothing (NOT if value is empty!), set to empty string + userInputPlaceholder: String, + socketId: String, + + /* + * Fields used for Amazon Alexa integration + */ + oAuthCode: String, + oAuthTokens: Object, + invocationName: String, + vendor: String, + skillId: String, + locales: [String], +}, { + timestamps: true, + usePushEach: true, +}) + +async function generateUUID (next) { + if (this.isNew) { + while (await ChannelModel.findOne({ _id: this._id })) { + this._doc._id = uuidV4() + } + } + next() +} + +function slugifyName (next) { + this._doc.slug = slugify(this._doc.slug) + next() +} + +const DEFAULT_INPUT_PLACEHOLDER = 'Write a reply' + +function addUserInputPlaceholder (next) { + const isWebchat = ['webchat', 'recastwebchat'].includes(this.type) + if (isWebchat && (typeof this.userInputPlaceholder === 'undefined')) { + this.userInputPlaceholder = DEFAULT_INPUT_PLACEHOLDER + } + next() +} + +ChannelSchema.pre('save', generateUUID) +ChannelSchema.pre('save', slugifyName) +ChannelSchema.pre('save', addUserInputPlaceholder) + +ChannelSchema.virtual('serialize').get(function () { + const filteredChannel = _.pickBy(this.toObject(), (value, key) => { + return value !== undefined && !['_id', '__v', 'isActive'].includes(key) + }) + + // Cannot serialize children as they're not Mongoose object anymore + // due to this.toObject + if (filteredChannel.type === 'slackapp') { + filteredChannel.children = filteredChannel.children.map(c => { + return typeof c === 'string' + ? c + : { + createdAt: c.createdAt, + slug: c.slug, + botuser: c.botuser, + token: c.token, + } + }) + } else { + delete filteredChannel.children + } + + return { + id: this._id, + ...filteredChannel, + isErrored: this.isErrored, + isActivated: this.isActivated, + webhookToken: this.type === 'messenger' + ? getWebhookToken(this._id, this.slug) : this.webhookToken, + hasGetStarted: this.hasGetStarted, + } +}) + +ChannelSchema.index({ connector: 1 }) +const ChannelModel = mongoose.model('Channel', ChannelSchema) +export default ChannelModel diff --git a/src/models/Connector.model.js b/src/models/connector.js similarity index 57% rename from src/models/Connector.model.js rename to src/models/connector.js index 610e375..814a832 100644 --- a/src/models/Connector.model.js +++ b/src/models/connector.js @@ -1,25 +1,17 @@ -import mongoose from 'mongoose' import uuidV4 from 'uuid/v4' +import mongoose from 'mongoose' const ConnectorSchema = new mongoose.Schema({ _id: { type: String, default: uuidV4 }, url: { type: String, required: true }, - channels: [{ type: String, ref: 'Channel' }], - conversations: [{ type: String, ref: 'Conversation' }], + isActive: { type: Boolean, required: true, default: true }, + isTyping: { type: Boolean, required: true, default: true }, + defaultDelay: { type: Number, min: 0, max: 5 }, }, { usePushEach: true, timestamps: true, }) -async function generateUUID (next) { - if (this.isNew) { - while (await models.Connector.findOne({ _id: this._id })) { - this._id = uuidV4() - } - } - next() -} - ConnectorSchema .pre('save', generateUUID) @@ -27,8 +19,8 @@ ConnectorSchema.virtual('serialize').get(function () { return { id: this._id, url: this.url, - conversations: this.conversations, - channels: this.channels.map(c => c.serialize || c), + isTyping: this.isTyping, + defaultDelay: this.defaultDelay, } }) @@ -39,6 +31,15 @@ ConnectorSchema.virtual('lightSerialize').get(function () { } }) -const Connector = mongoose.model('Connector', ConnectorSchema) +const ConnectorModel = mongoose.model('Connector', ConnectorSchema) + +async function generateUUID (next) { + if (this.isNew) { + while (await ConnectorModel.findOne({ _id: this._id })) { + this._doc._id = uuidV4() + } + } + next() +} -module.exports = Connector +export default ConnectorModel diff --git a/src/models/Conversation.model.js b/src/models/conversation.js similarity index 64% rename from src/models/Conversation.model.js rename to src/models/conversation.js index 65e3330..41f783a 100644 --- a/src/models/Conversation.model.js +++ b/src/models/conversation.js @@ -1,13 +1,16 @@ -import mongoose from 'mongoose' import uuidV4 from 'uuid/v4' +import mongoose from 'mongoose' const ConversationSchema = new mongoose.Schema({ _id: { type: String, default: uuidV4 }, channel: { type: String, ref: 'Channel', required: true }, connector: { type: String, ref: 'Connector', required: true }, chatId: { type: String, required: true }, - participants: [{ type: String, ref: 'Participant' }], - messages: [{ type: String, ref: 'Message' }], + isActive: { type: Boolean, required: true, default: true }, + + microsoftAddress: Object, + socketId: String, + replyToken: String, }, { usePushEach: true, timestamps: true, @@ -15,8 +18,8 @@ const ConversationSchema = new mongoose.Schema({ async function generateUUID (next) { if (this.isNew) { - while (await models.Conversation.findOne({ _id: this._id })) { - this._id = uuidV4() + while (await ConversationModel.findOne({ _id: this._id })) { + this._doc._id = uuidV4() } } next() @@ -39,11 +42,11 @@ ConversationSchema.virtual('full').get(function () { connector: this.connector, chatId: this.chatId, channel: this.channel, - participants: this.participants.map(p => p.serialize), - messages: this.messages.map(m => m.serialize), } }) -const Conversation = mongoose.model('Conversation', ConversationSchema) - -module.exports = Conversation +ConversationSchema.index({ channel: 1 }) +ConversationSchema.index({ connector: 1 }) +ConversationSchema.index({ chatId: 1 }) +const ConversationModel = mongoose.model('Conversation', ConversationSchema) +export default ConversationModel diff --git a/src/models/get_started_button.js b/src/models/get_started_button.js new file mode 100644 index 0000000..161a233 --- /dev/null +++ b/src/models/get_started_button.js @@ -0,0 +1,21 @@ +import uuidV4 from 'uuid/v4' +import mongoose from 'mongoose' + +const GetStartedButtonSchema = new mongoose.Schema({ + _id: { type: String, default: uuidV4 }, + channel_id: { type: String, required: true, unique: true }, + value: String, +}, { + timestamps: true, +}) + +GetStartedButtonSchema.index({ channel_id: 1 }) + +GetStartedButtonSchema.virtual('serialize').get(function () { + return { + value: this.value, + } +}) + +const GetStartedButtonModel = mongoose.model('GetStartedButton', GetStartedButtonSchema) +export default GetStartedButtonModel diff --git a/src/models/index.js b/src/models/index.js index 41bdb09..2c2b524 100644 --- a/src/models/index.js +++ b/src/models/index.js @@ -1,13 +1,7 @@ -import Connector from './Connector.model' -import Channel from './Channel.model' -import Message from './Message.model' -import Participant from './Participant.model' -import Conversation from './Conversation.model' - -export default { - Connector, - Channel, - Message, - Participant, - Conversation, -} +export Connector from './connector' +export Channel from './channel' +export Message from './message' +export Participant from './participant' +export Conversation from './conversation' +export GetStartedButton from './get_started_button' +export PersistentMenu from './persistent_menu' diff --git a/src/models/Message.model.js b/src/models/message.js similarity index 67% rename from src/models/Message.model.js rename to src/models/message.js index 1f19fdb..cdcd761 100644 --- a/src/models/Message.model.js +++ b/src/models/message.js @@ -6,13 +6,17 @@ const MessageSchema = new mongoose.Schema({ attachment: { type: Object }, participant: { type: String, ref: 'Participant', required: true }, conversation: { type: String, ref: 'Conversation', required: true }, - receivedAt: { type: Date, default: Date.now() }, + isActive: { type: Boolean, required: true, default: true }, + data: Object, + delay: { type: Number }, + markdown: { type: Boolean }, + receivedAt: { type: Date, default: Date.now }, }) async function generateUUID (next) { if (this.isNew) { - while (await models.Message.findOne({ _id: this._id })) { - this._id = uuidV4() + while (await Message.findOne({ _id: this._id })) { + this._doc._id = uuidV4() } } next() @@ -29,6 +33,6 @@ MessageSchema.virtual('serialize').get(function () { } }) +MessageSchema.index({ conversation: 1 }) const Message = mongoose.model('Message', MessageSchema) - -module.exports = Message +export default Message diff --git a/src/models/Participant.model.js b/src/models/participant.js similarity index 54% rename from src/models/Participant.model.js rename to src/models/participant.js index 04b907e..0a5c019 100644 --- a/src/models/Participant.model.js +++ b/src/models/participant.js @@ -3,21 +3,15 @@ import uuidV4 from 'uuid/v4' const ParticipantSchema = new mongoose.Schema({ _id: { type: String, default: uuidV4 }, + conversation: { type: String, required: true }, senderId: String, + data: { type: Object }, isBot: { type: Boolean, default: false }, + type: String, // One of 'user', 'bot' or 'agent' }, { timestamps: true, }) -async function generateUUID (next) { - if (this.isNew) { - while (await models.Participant.findOne({ _id: this._id })) { - this._id = uuidV4() - } - } - next() -} - ParticipantSchema.pre('save', generateUUID) ParticipantSchema.virtual('serialize').get(function () { @@ -28,6 +22,26 @@ ParticipantSchema.virtual('serialize').get(function () { } }) +ParticipantSchema.virtual('adminSerialize').get(function () { + return { + id: this._id, + data: this.data, + isBot: this.isBot, + senderId: this.senderId, + type: this.type, + } +}) + +ParticipantSchema.index({ conversation: 1 }) const Participant = mongoose.model('Participant', ParticipantSchema) -module.exports = Participant +async function generateUUID (next) { + if (this.isNew) { + while (await Participant.findOne({ _id: this._id })) { + this._doc._id = uuidV4() + } + } + next() +} + +export default Participant diff --git a/src/models/persistent_menu.js b/src/models/persistent_menu.js new file mode 100644 index 0000000..c0d86bf --- /dev/null +++ b/src/models/persistent_menu.js @@ -0,0 +1,26 @@ +import uuidV4 from 'uuid/v4' +import mongoose from 'mongoose' + +const PersistentMenuSchema = new mongoose.Schema({ + _id: { type: String, default: uuidV4 }, + connector_id: { type: String, required: true }, + menu: { type: mongoose.Schema.Types.Mixed, required: true }, + default: { type: Boolean, default: false, required: true }, + locale: { type: String, required: true }, +}, { + timestamps: true, +}) + +PersistentMenuSchema.index({ connector_id: 1, locale: 1 }, { unique: true }) +PersistentMenuSchema.index({ connector_id: 1 }) + +PersistentMenuSchema.virtual('serialize').get(function () { + return { + menu: this.menu, + default: this.default, + locale: this.locale, + } +}) + +const PersistentMenuModel = mongoose.model('PersistentMenu', PersistentMenuSchema) +export default PersistentMenuModel diff --git a/src/routes/App.routes.js b/src/routes/App.routes.js deleted file mode 100644 index c8fb19d..0000000 --- a/src/routes/App.routes.js +++ /dev/null @@ -1,15 +0,0 @@ -export default [ - { - method: 'GET', - path: '/', - validators: [], - handler: controllers.App.index, - }, - - { - method: 'POST', - path: '/', - validators: [], - handler: controllers.App.index, - }, -] diff --git a/src/routes/Channels.routes.js b/src/routes/Channels.routes.js deleted file mode 100644 index 4bf6fc3..0000000 --- a/src/routes/Channels.routes.js +++ /dev/null @@ -1,172 +0,0 @@ -import * as channelValidators from '../validators/Channels.validators.js' - -export default [ - - /** - * @api {post} /bots/:bot_id/channels Create a Channel - * @apiName createChannelByConnectorId - * @apiGroup Channel - * - * @apiDescription Create a channel only with parameters provided. - * - * @apiParam {String} slug Channel slug - * @apiParam {String} type Channel type - * @apiParam {String} token (Optionnal) Channel token (Messenger and Slack) - * @apiParam {String} userName (Optionnal) Channel username (Kik) - * @apiParam {String} apiKey (Optionnal) Channel apiKey (Kik and Messenger) - * @apiParam {String} webhook (Optionnal) Channel webhook (Kik and Messenger) - * @apiParam {Boolean} isActivated Channel isActivated - * - * @apiSuccess {Object} results Channel information - * @apiSuccess {String} results.slug Channel slug - * @apiSuccess {String} results.type Channel type - * @apiSuccess {String} results.token Channel token - * @apiSuccess {String} results.userName Channel userName - * @apiSuccess {String} results.apiKey Channel apiKey - * @apiSuccess {String} results.webhook Channel webhook - * @apiSuccess {Boolean} results.isActivated Channel isActivated - * @apiSuccess {String} message success message - * - * @apiError (Bad Request 400 bot_id not valid) {String} message Return if bot_id is invalid - * - * @apiError (Bad Request 400 type not valid) {String} message Return if type is invalid - * - * @apiError (Bad Request 400 isActivated not valid) {String} message Return if isActivated is invalid - * - * @apiError (Bad Request 400 slug not valid) {String} message Return if slug is invalid - * - * @apiError (Conflict 409 slug already taken) {String} message Return if slug is already taken in the current Connector scope - */ - { - method: 'POST', - path: '/connectors/:connector_id/channels', - validators: [channelValidators.createChannelByConnectorId], - handler: controllers.Channels.createChannelByConnectorId, - }, - - /** - * @api {get} /bots/:bot_id/channels Get Channels - * @apiName getChannelsByConnectorId - * @apiGroup Channel - * - * @apiDescription Get all Channels of a Connector - * - * @apiParam {String} bot_id Connector id - * - * @apiSuccess {Array} results Array of Channels - * @apiSuccess {String} results.bot Connector object - * @apiSuccess {String} results.slug Channel slug - * @apiSuccess {String} results.type Channel type - * @apiSuccess {String} results.token Channel token - * @apiSuccess {String} results.userName Channel userName - * @apiSuccess {String} results.apiKey Channel apiKey - * @apiSuccess {String} results.webhook Channel webhook - * @apiSuccess {Boolean} results.isActivated Channel isActivated - * @apiSuccess {String} message success message - * - * @apiError (Bad Request 400 bot_id not valid) {String} message Return if bot_id is invalid - * - * @apiError (Not found 404) {String} message Return if the Connector doesn't exist - */ - { - method: 'GET', - path: '/connectors/:connector_id/channels', - validators: [], - handler: controllers.Channels.getChannelsByConnectorId, - }, - - /** - * @api {get} /bots/:bot_id/channels/:channel_slug Get a Channel - * @apiName getChannelByConnectorId - * @apiGroup Channel - * - * @apiDescription Get a Channel of a Connector - * - * @apiParam {String} channel_slug Channel slug. - * - * @apiSuccess {Object} results Channel information - * @apiSuccess {String} results.channel_slug Channel slug. - * @apiSuccess {String} results.bot Connector object. - * @apiSuccess {String} results.slug Channel slug. - * @apiSuccess {String} results.type Channel type. - * @apiSuccess {String} results.token Channel token. - * @apiSuccess {String} results.userName Channel userName. - * @apiSuccess {String} results.apiKey Channel apiKey. - * @apiSuccess {String} results.webhook Channel webhook. - * @apiSuccess {Boolean} results.isActivated Channel isActivated. - * @apiSuccess {String} message success message - * - * @apiError (Bad Request 400 bot_id not valid) {String} message Return if bot_id is invalid - * - * @apiError (Bad Request 400 channel_slug not valid) {String} message Return if channel_slug is invalid - * - * @apiError (Not found 404) {String} message Return if either the Connector or the Channel doesn't exist - */ - { - method: 'GET', - path: '/connectors/:connector_id/channels/:channel_slug', - validators: [], - handler: controllers.Channels.getChannelByConnectorId, - }, - - /** - * @api {put} /bots/:bot_id/channels/:channel_slug Update a Channel - * @apiName updateChannelByConnectorId - * @apiGroup Channel - * - * @apiDescription Update a Channel - * - * @apiParam {String} channel_slug Channel slug. - * @apiParam {String} slug Channel slug - * @apiParam {String} type Channel type - * @apiParam {String} token (Optionnal) Channel token (Messenger and Slack) - * @apiParam {String} userName (Optionnal) Channel username (Kik) - * @apiParam {String} apiKey (Optionnal) Channel apiKey (Kik and Messenger) - * @apiParam {String} webhook (Optionnal) Channel webhook (Kik and Messenger) - * @apiParam {Boolean} isActivated Channel isActivated - * - * @apiSuccess {Object} results Channel information - * @apiSuccess {String} results.channel_slug Channel slug. - * @apiSuccess {String} results.bot Connector object. - * @apiSuccess {String} results.slug Channel slug. - * @apiSuccess {String} results.type Channel type. - * @apiSuccess {String} results.token Channel token. - * @apiSuccess {String} results.userName Channel userName. - * @apiSuccess {String} results.apiKey Channel apiKey. - * @apiSuccess {String} results.webhook Channel webhook. - * @apiSuccess {Boolean} results.isActivated Channel isActivated. - * @apiSuccess {String} message success message - * - * @apiError (Bad Request 400 bot_id not valid) {String} message Return if bot_id is invalid - * - * @apiError (Bad Request 400 channel_slug not valid) {String} message Return if channel_slug is invalid - * - * @apiError (Not found 404) {String} message Return if either the Connector or the Channel doesn't exist - * - * @apiError (Conflict 409 slug already taken) {String} message Return if slug is already taken - */ - { - method: 'PUT', - path: '/connectors/:connector_id/channels/:channel_slug', - validators: [], - handler: controllers.Channels.updateChannelByConnectorId, - }, - - /** - * @api {delete} /bots/:bot_id/channels/:channel_slug Delete a Channel - * @apiName deleteChannelByConnectorId - * @apiGroup Channel - * - * @apiDescription Delete a Channel - * - * @apiParam {String} channel_slug Channel slug. - * - * @apiError (Not found 404) {String} message Return if either the Connector or the Channel doesn't exist - */ - { - method: 'DELETE', - path: '/connectors/:connector_id/channels/:channel_slug', - validators: [], - handler: controllers.Channels.deleteChannelByConnectorId, - }, -] diff --git a/src/routes/Connectors.routes.js b/src/routes/Connectors.routes.js deleted file mode 100644 index 10262b0..0000000 --- a/src/routes/Connectors.routes.js +++ /dev/null @@ -1,110 +0,0 @@ -import * as connectorValidators from '../validators/Connectors.validators.js' - -export default [ - -/** -* @api {GET} /connectors/:bot_id Get a connector by botId -* @apiName getConnectorByBotId -* @apiGroup Connector -* -* @apiDescription Get a connector by botId -* -* @apiParam {String} bot_id BotId -* -* @apiSuccess {Object} results Connector information -* @apiSuccess {String} results.id Connector id -* @apiSuccess {String} results.url Bot url -* @apiSuccess {String} results.botId BotId -* @apiSuccess {Array} results.channels Array of Channels (see Channels) -* @apiSuccess {Array} results.conversations Array of Conversations (see Conversations) -* @apiSuccess {String} message success message -* -* @apiError (Bad Request 400 bot_id is invalid) {String} message Return if bot_id is invalid -* -* @apiError (Not found 404) {String} message Return if the connector doesn't exist -*/ - { - method: 'GET', - path: '/connectors/:connector_id', - validators: [], - handler: controllers.Connectors.getConnectorByBotId, - }, - -/** -* @api {POST} /bots Create a connector -* @apiName createConnector -* @apiGroup Connector -* -* @apiDescription Create a new connector -* -* @apiParam {String} url Bot url endpoint -* @apiParam {String} botId BotId (ref to bernard's bot) -* -* @apiSuccess {Object} results Connector information -* @apiSuccess {String} results.id Connector id -* @apiSuccess {String} results.url Bot url -* @apiSuccess {String} results.botId Bot id -* @apiSuccess {Array} results.channels Array of Channels (see Channels) -* @apiSuccess {Array} results.conversations Array of Conversations (see Conversations) -* @apiSuccess {String} message success message -* -* @apiError (Bad Request 400 url not valid) {String} message Return if url is invalid -* @apiError (Bad Request 400 url not valid) {String} message Return if botId is invalid -*/ - { - method: 'POST', - path: '/connectors', - validators: [connectorValidators.createConnector], - handler: controllers.Connectors.createConnector, - }, - -/** -* @api {PUT} /connectors/:bot_id Update a connector -* @apiName updateConnectorByBotId -* @apiGroup Connector -* -* @apiDescription Update a connector -* -* @apiParam {String} bot_id Bot id -* @apiParam {String} url Bot new url endpoint -* -* @apiSuccess {Object} results Connector information -* @apiSuccess {String} results.id Connector id -* @apiSuccess {String} results.url Bot url endpoint -* @apiSuccess {String} results.botId Bot id -* @apiSuccess {Array} results.channels Array of Channels (see Channels) -* @apiSuccess {Array} results.conversations Array of Conversations (see Conversations) -* @apiSuccess {String} message success message -* -* @apiError (Bad Request 400 url not valid) {String} message Return if url is invalid -* -* @apiError (Bad Request 400 bot_id not valid) {String} message Return if bot_id is invalid -* -* @apiError (Not found 404) {String} message Return if the bot doesn't exist -*/ - { - method: 'PUT', - path: '/connectors/:connector_id', - validators: [connectorValidators.updateConnectorByBotId], - handler: controllers.Connectors.updateConnectorByBotId, - }, - - /** -* @api {DELETE} /connectors/:bot_id Delete a connector -* @apiName deleteConnectorByBotId -* @apiGroup Connector -* -* @apiDescription Delete a connector -* -* @apiParam {String} bot_id Bot id -* -* @apiError (Bad Request 400 bot_id not valid) {String} message Return if bot_id is invalid -* -*/ - { - method: 'DELETE', - path: '/connectors/:connector_id', - validators: [], - handler: controllers.Connectors.deleteConnectorByBotId, - }, -] diff --git a/src/routes/Oauth.routes.js b/src/routes/Oauth.routes.js deleted file mode 100644 index d216c21..0000000 --- a/src/routes/Oauth.routes.js +++ /dev/null @@ -1,10 +0,0 @@ -import SlackAppService from '../services/SlackApp.service' - -export default [ - { - method: 'GET', - path: '/oauth/slack/:channel_id', - validators: [], - handler: SlackAppService.receiveOauth, - }, -] diff --git a/src/routes/Webhooks.routes.js b/src/routes/Webhooks.routes.js deleted file mode 100644 index d9c957d..0000000 --- a/src/routes/Webhooks.routes.js +++ /dev/null @@ -1,26 +0,0 @@ -export default [ - - /* - * This route is the webhook shared with a channel - * Depending on incomming request, it automatically detect which channel message is comming from. - * In many cases, this webhook is automatically registered onto right channel (Kik for example). - * Check our documentation for more info. - */ - { - method: 'POST', - path: '/webhook/:channel_id', - validators: [], - handler: controllers.Webhooks.forwardMessage, - }, - - /* - * This route is a specific Facebook endpoint. - * Facebook needs a GET endpoint on same route as webhook one to validate this webhook - */ - { - method: 'GET', - path: '/webhook/:channel_id', - validators: [], - handler: controllers.Webhooks.subscribeFacebookWebhook, - }, -] diff --git a/src/routes/application.js b/src/routes/application.js new file mode 100644 index 0000000..ad143dd --- /dev/null +++ b/src/routes/application.js @@ -0,0 +1,18 @@ +import AppController from '../controllers/application' + +export default [ + { + method: 'GET', + path: ['/'], + validators: [], + authenticators: [], + handler: AppController.index, + }, + { + method: 'POST', + path: ['/'], + validators: [], + authenticators: [], + handler: AppController.index, + }, +] diff --git a/src/routes/channels.js b/src/routes/channels.js new file mode 100644 index 0000000..3d11737 --- /dev/null +++ b/src/routes/channels.js @@ -0,0 +1,197 @@ +import * as channelValidators from '../validators/channels' +import ChannelController from '../controllers/channels' + +export default [ + + /** + * @api {post} /connectors/:connectorId/channels Create a Channel + * @apiName createChannelByConnectorId + * @apiGroup Channel + * @apiVersion 1.0.0 + * + * @apiDescription Creates a channel only with parameters provided. + * + * @apiParam {String} connectorId Connector id + * @apiParam {String} slug Channel slug + * @apiParam {String} type Channel type + * @apiParam {String} [token] Channel token (Messenger and Slack) + * @apiParam {String} [userName] Channel username (Kik) + * @apiParam {String} [apiKey] Channel apiKey (Kik and Messenger) + * @apiParam {String} [clientId] + * @apiParam {String} [clientSecret] + * @apiParam {Boolean} isActivated Channel isActivated + * + * @apiSuccess {Object} results Channel information + * @apiSuccess {String} results.slug Channel slug + * @apiSuccess {String} results.type Channel type + * @apiSuccess {String} [results.token] Channel token, only present if a token has been set + * @apiSuccess {String} [results.userName] Channel userName, only present if a username has been set + * @apiSuccess {String} [results.apiKey] Channel apiKey, only present if an api key has been set + * @apiSuccess {String} [clientId] + * @apiSuccess {String} [clientSecret] + * @apiSuccess {String} results.webhook Channel webhook + * @apiSuccess {Boolean} results.isActivated Channel isActivated + * @apiSuccess {String} message success message + * + * @apiError (400 Bad Request - type not valid) {String} message Returned if type is invalid + * + * @apiError (400 Bad Request - isActivated not valid) {String} message Return if isActivated is invalid + * + * @apiError (400 Bad Request - slug missing) {String} message "Parameter slug is missing" + * + * @apiError (409 Conflict - slug already taken) {String} message Returned if slug is already taken in the current Connector scope + * @apiError (409 Conflict - slug already taken) {null} results null + */ + { + method: 'POST', + path: ['/connectors/:connectorId/channels'], + validators: [channelValidators.createChannelByConnectorId], + authenticators: [], + handler: ChannelController.create, + }, + + /** + * @api {get} /connectors/:connectorId/channels Get Channels + * @apiName getChannelsByConnectorId + * @apiGroup Channel + * @apiVersion 1.0.0 + * + * @apiDescription Get all Channels of a Connector + * + * @apiParam {String} connectorId Connector id + * + * @apiSuccess {Array} results Array of Channels + * @apiSuccess {String} results.connector Connector object + * @apiSuccess {String} results.slug Channel slug + * @apiSuccess {String} results.type Channel type + * @apiSuccess {String} [results.token] Channel token + * @apiSuccess {String} [results.userName] Channel userName + * @apiSuccess {String} [results.apiKey] Channel apiKey + * @apiSuccess {String} [results.clientId] clientId of the channel + * @apiSuccess {String} [results.clientSecret] clientSecret of the channel + * @apiSuccess {String} results.webhook Channel webhook + * @apiSuccess {Boolean} results.isActivated if false, incoming messages won't be forwarded to the bot + * @apiSuccess {String} message success message + * + * @apiError (404 Not Found) {String} message "Connector not found" + * @apiError (404 Not Found) {null} results null + */ + { + method: 'GET', + path: ['/connectors/:connectorId/channels'], + validators: [], + authenticators: [], + handler: ChannelController.index, + }, + + /** + * @api {get} /connectors/:connectorId/channels/:channel_slug Get a Channel + * @apiName getChannelByConnectorId + * @apiGroup Channel + * @apiVersion 1.0.0 + * + * @apiDescription Get a Channel of a Connector + * + * @apiParam {String} channel_slug Channel slug. + * + * @apiSuccess {Object} results Channel information + * @apiSuccess {String} results.channel_slug Channel slug. + * @apiSuccess {String} results.connector Connector object. + * @apiSuccess {String} results.slug Channel slug. + * @apiSuccess {String} results.type Channel type. + * @apiSuccess {String} [results.token] Channel token. + * @apiSuccess {String} [results.userName] Channel userName. + * @apiSuccess {String} [results.apiKey] Channel apiKey. + * @apiSuccess {String} [results.clientId] clientId of the channel + * @apiSuccess {String} [results.clientSecret] clientSecret of the channel + * @apiSuccess {String} results.webhook Channel webhook. + * @apiSuccess {Boolean} results.isActivated Channel isActivated. + * @apiSuccess {String} message success message + * + * @apiError (400 Bad Request - connectorId not valid) {String} message Return if connectorId is invalid + * + * @apiError (400 Bad Request - channel_slug not valid) {String} message Return if channel_slug is invalid + * + * @apiError (404 Not Found) {String} message Return if either the Connector or the Channel doesn't exist + */ + { + method: 'GET', + path: ['/connectors/:connectorId/channels/:channel_slug'], + validators: [], + authenticators: [], + handler: ChannelController.show, + }, + + /** + * @api {put} /connectors/:connectorId/channels/:channel_slug Update a Channel + * @apiName updateChannelByConnectorId + * @apiGroup Channel + * @apiVersion 1.0.0 + * + * @apiDescription Update a Channel + * + * @apiParam {String} channel_slug Channel slug. + * @apiParam {String} slug Channel slug + * @apiParam {String} type Channel type + * @apiParam {String} [token] Channel token (Messenger and Slack) + * @apiParam {String} [userName] Channel username (Kik) + * @apiParam {String} [apiKey] Channel apiKey (Kik and Messenger) + * @apiParam {String} [clientId] clientId of the channel + * @apiParam {String} [clientSecret] clientSecret of the channel + * @apiParam {String} [webhook] Channel webhook + * @apiParam {Boolean} isActivated Channel isActivated + * + * @apiSuccess {Object} results Channel information + * @apiSuccess {String} results.channel_slug Channel slug. + * @apiSuccess {String} results.bot Connector object. + * @apiSuccess {String} results.slug Channel slug. + * @apiSuccess {String} results.type Channel type. + * @apiSuccess {String} [results.token] Channel token. + * @apiSuccess {String} [results.userName] Channel userName. + * @apiSuccess {String} [results.apiKey] Channel apiKey. + * @apiSuccess {String} [results.clientId] clientId of the channel + * @apiSuccess {String} [results.clientSecret] clientSecret of the channel + * @apiSuccess {String} results.webhook Channel webhook. + * @apiSuccess {Boolean} results.isActivated Channel isActivated. + * @apiSuccess {String} message success message + * + * @apiError (400 Bad Request - connectorId not valid) {String} message Returned if connectorId is invalid + * + * @apiError (400 Bad Request - channel_slug not valid) {String} message Returned if channel_slug is invalid + * + * @apiError (400 Bad Request) channel_slug invalid The channel slug is empty or missing + * + * @apiError (400 Bad Request) connectorId not valid + * + * @apiError (404 Not Found - Connector or Channel not found) {String} message indicates whether the Connector or the Channel doesn't exist + * + * @apiError (409 Conflict - slug already taken) {String} message Returned if slug is already taken + */ + { + method: 'PUT', + path: ['/connectors/:connectorId/channels/:channel_slug'], + validators: [channelValidators.updateChannel], + authenticators: [], + handler: ChannelController.update, + }, + + /** + * @api {delete} /connectors/:connectorId/channels/:channel_slug Delete a Channel + * @apiName deleteChannelByConnectorId + * @apiGroup Channel + * @apiVersion 1.0.0 + * + * @apiDescription Delete a Channel + * + * @apiParam {String} channel_slug Slug of the Channel to be deleted + * + * @apiError (404 Not Found) {String} message Returned if either the Connector or the Channel doesn't exist + */ + { + method: 'DELETE', + path: ['/connectors/:connectorId/channels/:channel_slug'], + validators: [], + authenticators: [], + handler: ChannelController.delete, + }, +] diff --git a/src/routes/connectors.js b/src/routes/connectors.js new file mode 100644 index 0000000..707df01 --- /dev/null +++ b/src/routes/connectors.js @@ -0,0 +1,119 @@ +import * as connectorValidators from '../validators/connectors' +import Connectors from '../controllers/connectors' + +export default [ + /** + * @api {GET} /connectors/:connectorId Get a connector by Id + * @apiName getConnectorById + * @apiGroup Connector + * @apiVersion 1.0.0 + * + * @apiDescription Get a connector by Id + * + * @apiParam {String} connectorId connector id + * + * @apiSuccess {Object} results Connector information + * @apiSuccess {String} results.id Connector Id + * @apiSuccess {String} results.url Bot URL + * @apiSuccess {Boolean} results.isTyping if true, the bot is shown as typing while processing a response + * @apiSuccess {Array} results.channels Array of Channels (see Channels) + * @apiSuccess {Array} results.conversations Array of Conversations (see Conversations) + * @apiSuccess {String} message success message + * + * @apiError (404 Not Found) {String} message if the connector doesn't exist + * @apiError (404 Not Found) {String} results + */ + { + method: 'GET', + path: ['/connectors/:connectorId'], + validators: [], + authenticators: [], + handler: Connectors.show, + }, + + /** + * @api {POST} /connectors Create a connector + * @apiName createConnector + * @apiGroup Connector + * @apiVersion 1.0.0 + * + * @apiDescription Create a new connector + * + * @apiParam {String} url Bot URL endpoint + * @apiParam {Boolean} [isTyping=true] if true, the bot will be shown as typing while processing a response + * + * @apiSuccess {Object} results Connector information + * @apiSuccess {String} results.id Connector id + * @apiSuccess {String} results.url Bot url + * @apiSuccess {Boolean} results.isTyping if true, the bot is shown as typing while processing a response + * @apiSuccess {Array} results.channels Array of Channels (see Channels) + * @apiSuccess {Array} results.conversations Array of Conversations (see Conversations) + * @apiSuccess {String} message success message + * + * @apiError (400 Bad Request - url parameter missing) {String} message "Parameter url is missing" + * @apiError (400 Bad Request - url parameter missing) {null} results + */ + { + method: 'POST', + path: ['/connectors'], + validators: [connectorValidators.createConnector], + authenticators: [], + handler: Connectors.create, + }, + + /** + * @api {PUT} /connectors/:connectorId Update a connector + * @apiName updateConnectorById + * @apiGroup Connector + * @apiVersion 1.0.0 + * + * @apiDescription Update a connector + * + * @apiParam {String} connectorId connector id + * @apiParam {String} [url] Bot URL endpoint + * @apiParam {Boolean} [isTyping] if true, the bot will be shown as typing while processing a response. + * + * @apiSuccess {Object} results Connector information + * @apiSuccess {String} results.id Connector id + * @apiSuccess {String} results.url Bot url endpoint + * @apiSuccess {Array} results.channels Array of Channels (see Channels) + * @apiSuccess {Array} results.conversations Array of Conversations (see Conversations) + * @apiSuccess {String} message success message + * + * @apiError (400 Bad Request - url not valid) {String} message Return if url is invalid + * + * @apiError (404 Not Found) {String} message "Connector not found" + */ + { + method: 'PUT', + path: ['/connectors/:connectorId'], + validators: [connectorValidators.updateConnector], + authenticators: [], + handler: Connectors.update, + }, + + /** + * @api {DELETE} /connectors/:connectorId Delete a connector + * @apiName deleteConnectorById + * @apiGroup Connector + * @apiVersion 1.0.0 + * + * @apiDescription Delete a connector + * + * @apiParam {String} connectorId connector id + * + * @apiSuccess (200 OK) {String} message "Connector successfully deleted" + * @apiSuccess (200 OK) {null} results + * + * @apiError (404 Not Found) {String} message "Connector not found" + * @apiError (404 Not Found) {null} results + * + */ + { + method: 'DELETE', + path: ['/connectors/:connectorId'], + validators: [], + authenticators: [], + handler: Connectors.delete, + }, +] diff --git a/src/routes/Conversations.routes.js b/src/routes/conversations.js similarity index 50% rename from src/routes/Conversations.routes.js rename to src/routes/conversations.js index 2deff43..66cb8ab 100644 --- a/src/routes/Conversations.routes.js +++ b/src/routes/conversations.js @@ -1,35 +1,42 @@ -export default [ +import ConversationController from '../controllers/conversations' +export default [ /** - * @api {get} /conversations Get all Conversations of a Connector + * @api {get} /connectors/:connectorId/conversations Get all Conversations of a Connector * @apiName getConversationsByConnectorId * @apiGroup Conversation + * @apiVersion 1.0.0 * * @apiDescription List all the Conversations of a Connector * + * @apiParam {String} connectorId connector id + * * @apiSuccess {Array} results Array of Conversations * @apiSuccess {String} results.id Conversation id - * @apiSuccess {String} results.channel if of the Channel's Conversation + * @apiSuccess {String} results.channel Id of the Channel the Conversation belongs to * @apiSuccess {String} results.chatId id of the chat linked to the Conversation * @apiSuccess {String} results.connector ObjectId of the connector * @apiSuccess {String} message success message * - * @apiError (Not Found 404) {String} message Bot not found + * @apiError (404 Not Found) {String} message Connector not found */ { method: 'GET', - path: '/connectors/:connector_id/conversations', + path: ['/connectors/:connectorId/conversations'], validators: [], - handler: controllers.Conversations.getConversationsByConnectorId, + authenticators: [], + handler: ConversationController.index, }, /** - * @api {get} /conversation/:conversation_id Get Conversation + * @api {get} /connectors/:connectorId/conversation/:conversation_id Get Conversation * @apiName getConversationByConnectorId * @apiGroup Conversation + * @apiVersion 1.0.0 * * @apiDescription Get a Conversation * + * @apiParam {String} connectorId connector id * @apiParam {String} conversation_id Conversation id * * @apiSuccess {Object} results Conversation information @@ -41,37 +48,43 @@ export default [ * @apiSuccess {Array} results.messages Array of Messages * @apiSuccess {String} message success message * - * @apiError (Bad Request 400) {String} message Parameter conversation_id is invalid - - * @apiError (Not Found 404) {String} message Conversation not found + * @apiError (400 Bad Request) {String} message Parameter conversation_id is invalid + * @apiError (404 Not Found) {String} message Conversation not found */ - { method: 'GET', - path: '/connectors/:connector_id/conversations/:conversation_id', + path: ['/connectors/:connectorId/conversations/:conversation_id'], validators: [], - handler: controllers.Conversations.getConversationByConnectorId, + authenticators: [], + handler: ConversationController.show, }, /** - * @api {delete} /conversations/:conversation_id Delete conversation + * @api {delete} /connectors/:connectorId/conversations/:conversation_id Delete conversation * @apiName deleteConversationByConnectorId * @apiGroup Conversation * * @apiDescription Delete a Connector's Conversation * + * @apiParam {String} connectorId connector id * @apiParam {String} conversation_id Conversation id * - * @apiError (Bad Request 400) {String} message Parameter conversation_id is invalid - * - * @apiError (Not Found 404) {String} message Bot or Conversation not found + * @apiError (400 Bad Request) {String} message Parameter conversation_id is invalid + * @apiError (404 Not Found) {String} message Connector or Conversation not found */ - { method: 'DELETE', - path: '/connectors/:connector_id/conversations/:conversation_id', + path: ['/connectors/:connectorId/conversations/:conversation_id'], validators: [], - handler: controllers.Conversations.deleteConversationByConnectorId, + authenticators: [], + handler: ConversationController.delete, }, + { + method: 'POST', + path: ['/connectors/:connectorId/conversations/dump'], + validators: [], + authenticators: [], + handler: ConversationController.dumpDelete, + }, ] diff --git a/src/routes/get_started_buttons.js b/src/routes/get_started_buttons.js new file mode 100644 index 0000000..73fe5e5 --- /dev/null +++ b/src/routes/get_started_buttons.js @@ -0,0 +1,32 @@ +import GetStartedButtonController from '../controllers/get_started_buttons' + +export default [ + { + method: 'POST', + path: ['/connectors/:connectorId/channels/:channel_id/getstartedbuttons'], + validators: [], + authenticators: [], + handler: GetStartedButtonController.create, + }, + { + method: 'GET', + path: ['/connectors/:connectorId/channels/:channel_id/getstartedbuttons'], + validators: [], + authenticators: [], + handler: GetStartedButtonController.show, + }, + { + method: 'PUT', + path: ['/connectors/:connectorId/channels/:channel_id/getstartedbuttons'], + validators: [], + authenticators: [], + handler: GetStartedButtonController.update, + }, + { + method: 'DELETE', + path: ['/connectors/:connectorId/channels/:channel_id/getstartedbuttons'], + validators: [], + authenticators: [], + handler: GetStartedButtonController.delete, + }, +] diff --git a/src/routes/index.js b/src/routes/index.js index f77b053..cda766c 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -1,32 +1,35 @@ import express from 'express' -import { Logger } from '../utils' -import appRoutes from './App.routes' -import oauthRoutes from './Oauth.routes' -import connectorRoutes from './Connectors.routes' -import channelRoutes from './Channels.routes' -import messagesRoutes from './Messages.routes' -import webhooksRoutes from './Webhooks.routes' -import conversationRoutes from './Conversations.routes' -import participantsRoutes from './Participants.routes' - -import { renderConnectorError } from '../utils/errors' +import appRoutes from './application' +import connectorRoutes from './connectors' +import channelRoutes from './channels' +import messageRoutes from './messages' +import webhookRoutes from './webhooks' +import conversationRoutes from './conversations' +import participantRoutes from './participants' +import getStartedButtonRoutes from './get_started_buttons' +import persistentMenuRoutes from './persistent_menus' +import { getChannelIntegrationRoutes } from '../channel_integrations' +import { renderError } from '../utils/errors' export const routes = [ ...appRoutes, - ...oauthRoutes, ...connectorRoutes, ...channelRoutes, - ...messagesRoutes, - ...webhooksRoutes, + ...messageRoutes, + ...webhookRoutes, + ...participantRoutes, ...conversationRoutes, - ...participantsRoutes, + ...getStartedButtonRoutes, + ...persistentMenuRoutes, + ...getChannelIntegrationRoutes(), ] export const createRouter = app => { const router = express.Router() routes.forEach(r => { + router[r.method.toLowerCase()](r.path, async (req, res) => { try { // Validate the request parameters @@ -34,14 +37,23 @@ export const createRouter = app => { await validator(req, res) } + // Validate the request authentication + for (const authenticator of r.authenticators) { + await authenticator(req, res) + } + await r.handler(req, res) } catch (err) { - Logger.error(err) - renderConnectorError(res, err) + renderError(res, err) } + }) + }) - app.use(router) + app.get('/', (req, res) => { + res.send('Hi!') + }) + app.use('/v1', router) } diff --git a/src/routes/Messages.routes.js b/src/routes/messages.js similarity index 81% rename from src/routes/Messages.routes.js rename to src/routes/messages.js index fdcaed2..71d94a8 100644 --- a/src/routes/Messages.routes.js +++ b/src/routes/messages.js @@ -1,12 +1,14 @@ -export default [ +import MessageController from '../controllers/messages' +export default [ /** - * @api {post} /conversations/:conversation_id/messages Post a message into a specific conversation + * @api {post} /connectors/:connectorId/conversations/:conversation_id/messages Post a message into a specific conversation * @apiName Post a message to a conversation * @apiGroup Messages * * @apiDescription Post a message into a specific conversation. With this route, you do not have to only answer to an user as response of a previous message, we can also directly send him messages * + * @apiParam (Route Parameters) {ObjectId} connectorId connector id * @apiParam (Route Parameters) {ObjectId} conversation_id Conversation ObjectId * * @apiParam (Text message parameters) {String} type=text Must be 'text' in this case @@ -42,8 +44,8 @@ export default [ * @apiError (Bad Request 400 for conversation_id) {null} results Response data * @apiError (Bad Request 400 for conversation_id) {String} message Parameter conversation_id is invalid * - * @apiError (Not found 404 for bot) {null} results Response data - * @apiError (Not found 404 for bot) {String} message Bot not found + * @apiError (Not found 404 for connector) {null} results Response data + * @apiError (Not found 404 for connector) {String} message Connector not found * * @apiError (Not found 404 for conversation) {null} results Response data * @apiError (Not found 404 for conversation) {String} message Conversation not found @@ -53,17 +55,20 @@ export default [ */ { method: 'POST', - path: '/connectors/:connector_id/conversations/:conversation_id/messages', + path: ['/connectors/:connectorId/conversations/:conversationId/messages'], validators: [], - handler: controllers.Messages.postMessage, + authenticators: [], + handler: MessageController.postMessage, }, /** * @api {post} /messages Post a message to a specific bot * @apiName Post a message to a bot - * @apiGroup Bot + * @apiGroup Connector * - * @apiDescription Post a message to a specific. With this route, you do not have to only answer to an user as response of a previous message, we can also directly send him messages + * @apiDescription Post a message to a specific bot. With this route, you do not have to only answer to a user in response to a previous message, we can also directly send them messages + + * @apiParam {String} connectorId connector id * * @apiParam (Text message parameters) {String} type=text Must be 'text' in this case * @apiParam (Text message parameters) {String} value Your message @@ -95,19 +100,17 @@ export default [ * @apiSuccess (Success 200) {Participant} results.participant Message Participant * @apiSuccess (Success 200) {String} message Message successfully posted * - * @apiError (Bad Request 400 for conversation_id) {null} results Response data - * @apiError (Bad Request 400 for conversation_id) {String} message Parameter conversation_id is invalid - * - * @apiError (Not found 404 for bot) {null} results Response data - * @apiError (Not found 404 for bot) {String} message Bot not found + * @apiError (Not found 404 for connector) {null} results Response data + * @apiError (Not found 404 for connector) {String} message Connector not found * - * @apiError (Internal Server Error 500 if bot participant not found) {null} results Response data - * @apiError (Internal Server Error 500 if bot participant not found) {String} message Bot participant not found in this conversation + * @apiError (Not Found 404 if bot participant not found) {null} results Response data + * @apiError (Not Found 404 if bot participant not found) {String} message Bot participant not found in this conversation */ { method: 'POST', - path: '/connectors/:connector_id/messages', + path: ['/connectors/:connectorId/messages'], validators: [], - handler: controllers.Messages.postMessages, + authenticators: [], + handler: MessageController.broadcastMessage, }, ] diff --git a/src/routes/Participants.routes.js b/src/routes/participants.js similarity index 73% rename from src/routes/Participants.routes.js rename to src/routes/participants.js index 449c66b..6d99790 100644 --- a/src/routes/Participants.routes.js +++ b/src/routes/participants.js @@ -1,32 +1,34 @@ +import ParticipantController from '../controllers/participants' + export default [ /** - * @api {get} /participants Index Connector's Participants + * @api {get} /connectors/:connectorId/participants Index Connector's Participants * @apiName List participants * @apiGroup Participants * - * @apiDescription Index connector participants (for all conversations and all channels) + * @apiDescription List all connector participants (for all conversations and all channels) * * @apiSuccess (Success 200 with at least one participant) {Array} results Array of participants * @apiSuccess (Success 200 with at least one participant) {ObjectId} results.id Participant objectId * @apiSuccess (Success 200 with at least one participant) {String} results.name Participant name * @apiSuccess (Success 200 with at least one participant) {String} results.slug Participant slug * @apiSuccess (Success 200 with at least one participant) {Object} results.information Particpant information - * @apiSuccess (Success 200 with at least one participant) {Boolean} results.isBot Is this particpant a bot ? + * @apiSuccess (Success 200 with at least one participant) {Boolean} results.isBot Is this particpant a bot? * @apiSuccess (Success 200 with at least one participant) {String} message Participants successfully rendered * - * * @apiSuccess (Success 200 with no participant) {null} results Response data * @apiSuccess (Success 200 with no participant) {String} message No participants */ { method: 'GET', - path: '/connectors/:connector_id/participants', + path: ['/connectors/:connectorId/participants'], validators: [], - handler: controllers.Participants.getParticipantsByConnectorId, + authenticators: [], + handler: ParticipantController.index, }, /** - * @api {get} /participants/:participant_id Show a specific participant (for a connector) + * @api {get} /connectors/:connectorId/participants/:participant_id Show a specific participant (for a connector) * @apiName Show participant * @apiGroup Participants * @@ -39,15 +41,16 @@ export default [ * @apiSuccess (Success 200) {String} results.name Participant name * @apiSuccess (Success 200) {String} results.slug Participant slug * @apiSuccess (Success 200) {Object} results.information Particpant information - * @apiSuccess (Success 200) {Boolean} results.isBot Is this particpant a bot ? + * @apiSuccess (Success 200) {Boolean} results.isBot Is this particpant a bot? * @apiSuccess (Success 200) {String} message Participant successfully rendered * * @apiError (Bad Request 400 for participant_id) {String} message Parameter participant_id is invalid */ { method: 'GET', - path: '/connectors/:connector_id/participants/:participant_id', + path: ['/connectors/:connectorId/participants/:participant_id'], validators: [], - handler: controllers.Participants.getParticipantByConnectorId, + authenticators: [], + handler: ParticipantController.show, }, ] diff --git a/src/routes/persistent_menus.js b/src/routes/persistent_menus.js new file mode 100644 index 0000000..b4203ce --- /dev/null +++ b/src/routes/persistent_menus.js @@ -0,0 +1,53 @@ +import PersistentMenuController from '../controllers/persistent_menus' + +export default [ + { + method: 'POST', + path: ['/connectors/:connectorId/persistentmenus'], + validators: [], + authenticators: [], + handler: PersistentMenuController.create, + }, + { + method: 'POST', + path: ['/connectors/:connectorId/persistentmenus/setdefault'], + validators: [], + authenticators: [], + handler: PersistentMenuController.setDefault, + }, + { + method: 'GET', + path: ['/connectors/:connectorId/persistentmenus/:language'], + validators: [], + authenticators: [], + handler: PersistentMenuController.show, + }, + { + method: 'GET', + path: ['/connectors/:connectorId/persistentmenus'], + validators: [], + authenticators: [], + handler: PersistentMenuController.index, + }, + { + method: 'PUT', + path: ['/connectors/:connectorId/persistentmenus/:language'], + validators: [], + authenticators: [], + handler: PersistentMenuController.update, + }, + { + method: 'DELETE', + path: ['/connectors/:connectorId/persistentmenus/:language'], + validators: [], + authenticators: [], + handler: PersistentMenuController.delete, + }, + { + method: 'DELETE', + path: ['/connectors/:connectorId/persistentmenus'], + validators: [], + authenticators: [], + handler: PersistentMenuController.deleteAll, + }, +] diff --git a/src/routes/webhooks.js b/src/routes/webhooks.js new file mode 100644 index 0000000..8d401a8 --- /dev/null +++ b/src/routes/webhooks.js @@ -0,0 +1,60 @@ +import WebhookController from '../controllers/webhooks' + +export default [ + { + method: 'POST', + path: ['/webhook/service/:channel_type'], + validators: [], + authenticators: [], + handler: WebhookController.serviceHandleMethodAction, + }, + { + method: 'GET', + path: ['/webhook/service/:channel_type'], + validators: [], + authenticators: [], + handler: WebhookController.serviceHandleMethodAction, + }, + { + method: 'POST', + path: ['/webhook/:channel_id'], + validators: [], + authenticators: [], + handler: WebhookController.handleMethodAction, + }, + { + method: 'GET', + path: ['/webhook/:channel_id'], + validators: [], + authenticators: [], + handler: WebhookController.handleMethodAction, + }, + { + method: 'POST', + path: ['/webhook/:channel_id/conversations'], + validators: [], + authenticators: [], + handler: WebhookController.createConversation, + }, + { + method: 'GET', + path: ['/webhook/:channel_id/conversations/:conversation_id/messages'], + validators: [], + authenticators: [], + handler: WebhookController.getMessages, + }, + { + method: 'GET', + path: ['/webhook/:channel_id/preferences'], + validators: [], + authenticators: [], + handler: WebhookController.getPreferences, + }, + { + method: 'GET', + path: ['/webhook/:channel_id/conversations/:conversation_id/poll'], + validators: [], + authenticators: [], + handler: WebhookController.poll, + }, +] diff --git a/src/services/Callr.service.js b/src/services/Callr.service.js deleted file mode 100644 index f9e028d..0000000 --- a/src/services/Callr.service.js +++ /dev/null @@ -1,152 +0,0 @@ -import callr from 'callr' -import crypto from 'crypto' - -import ServiceTemplate from './Template.service' -import { BadRequestError, ForbiddenError } from '../utils/errors' - -export default class CallrService extends ServiceTemplate { - - static async onChannelCreate (channel) { - const type = 'sms.mo' - const options = { hmac_secret: channel.password, hmac_algo: 'SHA256' } - const api = new callr.api(channel.userName, channel.password) - - try { - await new Promise((resolve, reject) => (api.call('webhooks.subscribe', type, channel.webhook, options) - .success(async (res) => { - channel.webhookToken = res.hash - await channel.save() - resolve(res) - }) - .error(reject))) - } catch (err) { - channel.isErrored = true - } - } - - static async onChannelUpdate (channel, oldChannel) { - const type = 'sms.mo' - const client = new callr.api(channel.userName, channel.password) - const channelOptions = { hmac_secret: channel.password } - const oldClient = new callr.api(oldChannel.userName, oldChannel.password) - - try { - await new Promise((resolve) => oldClient.call('webhooks.unsubscribe', oldChannel.webhookToken).success(resolve()).error(resolve())) - await new Promise((resolve, reject) => client.call('webhooks.subscribe', type, channel.webhook, channelOptions) - .success(async (res) => { - channel.webhookToken = res.hash - await channel.save() - resolve(res) - }) - .error(reject)) - channel.isErrored = false - } catch (err) { - channel.isErrored = true - } - } - - static onChannelDelete (channel) { - const api = new callr.api(channel.userName, channel.password) - - api.call('webhooks.unsubscribe', channel.webhookToken) - } - - static checkParamsValidity (channel) { - const { userName, password } = channel - - if (!password) { throw new BadRequestError('Parameter password is missing') } - if (!userName) { throw new BadRequestError('Parameter userName is missing') } - - return true - } - - static async beforePipeline (req, res, channel) { - return channel - } - - static checkSecurity (req, res, channel) { - const { password } = channel - const payload = JSON.stringify(req.body) - const webhookSig = req.headers['x-callr-hmacsignature'] - const hash = crypto.createHmac('SHA256', password).update(payload).digest('base64') - - if (hash !== webhookSig) { throw new ForbiddenError() } - res.status(200).send() - } - - static extractOptions (req) { - const { body } = req - - return { - chatId: body.data.to, - senderId: body.data.from, - } - } - - static parseChannelMessage (conversation, message, opts) { - const msg = { - attachment: { - type: 'text', - content: message.data.text, - }, - channelType: 'callr', - } - return [conversation, msg, opts] - } - - static formatMessage (conversation, message, opts) { - const reply = [] - let keyboards = null - - if (message.attachment.type === 'text' || message.attachment.type === 'picture' || message.attachment.type === 'video') { - reply.push({ type: message.attachment.type, chatId: opts.chatId, to: opts.senderId, body: message.attachment.content }) - } else if (message.attachment.type === 'quickReplies') { - const keyboards = [{ type: 'suggested' }] - keyboards[0].responses = message.attachment.content.buttons.map(button => ({ type: 'text', body: button.title })) - reply.push({ type: 'text', chatId: opts.chatId, to: opts.senderId, body: message.attachment.content.title, keyboards }) - } else if (message.attachment.type === 'card') { - if (message.attachment.content.buttons && message.attachment.content.buttons.length) { - keyboards = [{ type: 'suggested', responses: [] }] - message.attachment.content.buttons.forEach(button => { - if (button.type !== 'element_share') { keyboards[0].responses.push({ type: 'text', body: button.value }) } - }) - } - if (message.attachment.content.title) { - reply.push({ type: 'text', chatId: opts.chatId, to: opts.senderId, body: message.attachment.content.title }) - } - if (message.attachment.content.subtitle) { - reply.push({ type: 'text', chatId: opts.chatId, to: opts.senderId, body: message.attachment.content.subtitle }) - } - reply[reply.length - 1].keyboards = keyboards - } else { - throw new BadRequestError('Message type unsupported by CallR') - } - return reply - } - - static sendMessage (conversation, messages, opts) { - return new Promise(async (resolve, reject) => { - const { senderId } = opts - const { chatId, channel } = conversation - const api = new callr.api(channel.userName, channel.password) - - const reply = messages.reduce((str, message) => { - if (message.body && message.body.length) { - str = `${str}${message.body}${'\n'}` - } - - if (message.keyboards && message.keyboards) { - const buttons = message.keyboards[0].responses - buttons.forEach((button, i) => str = `${str}${i}${' - '}${button.body}${'\n'}`) - } - - return str - }, '') - - api.call('sms.send', chatId, senderId, reply, null) - .success(resolve) - .error(reject) - }) - } - -} diff --git a/src/services/Kik.service.js b/src/services/Kik.service.js deleted file mode 100644 index 0eba2ac..0000000 --- a/src/services/Kik.service.js +++ /dev/null @@ -1,179 +0,0 @@ -import request from 'superagent' - -import ServiceTemplate from './Template.service' -import { BadRequestError, ForbiddenError } from '../utils/errors' - -const agent = require('superagent-promise')(require('superagent'), Promise) - -/** - * Connector's Kik Service - */ -export default class KikService extends ServiceTemplate { - - /* - * Subscribe webhook - */ - static async onChannelCreate (channel) { - const data = { - webhook: channel.webhook, - features: { - receiveReadReceipts: false, - receiveIsTyping: false, - manuallySendReadReceipts: false, - receiveDeliveryReceipts: false, - }, - } - - try { - await new Promise((resolve, reject) => { - request.post('https://api.kik.com/v1/config') - .auth(channel.userName, channel.apiKey) - .send(data) - .end((err) => err ? reject(err) : resolve()) - }) - channel.isErrored = false - } catch (err) { - channel.isErrored = true - } - } - - static async onChannelUpdate (channel) { - await KikService.onChannelCreate(channel) - } - - /** - * Check if the message come from a valid webhook - */ - static checkSecurity (req, res, channel) { - if (`${config.base_url}/webhook/${channel._id}` !== channel.webhook || req.headers['x-kik-username'] !== channel.userName) { - throw new ForbiddenError() - } - res.status(200).send() - } - - /** - * Check if any params is missing - */ - static checkParamsValidity (channel) { - const { userName, apiKey } = channel - - if (!apiKey) { throw new BadRequestError('Parameter apiKey is missing') } - if (!userName) { throw new BadRequestError('Parameter userName is missing') } - - return true - } - - /** - * Extract information from the request before the pipeline - */ - static extractOptions (req) { - const { body } = req - - return { - chatId: body.messages[0].chatId, - senderId: body.messages[0].participants[0], - } - } - - /** - * send 200 to kik to stop pipeline - */ - static async beforePipeline (req, res, channel) { - return channel - } - - /** - * Parse the message to the connector format - */ - static parseChannelMessage (conversation, message, opts) { - const firtsMessage = message.messages[0] - const msg = { - attachment: {}, - channelType: 'kik', - } - - switch (firtsMessage.type) { - case 'text': - msg.attachment = { type: 'text', content: firtsMessage.body } - break - case 'link': - msg.attachment = { type: 'link', content: firtsMessage.url } - break - case 'picture': - msg.attachment = { type: 'picture', content: firtsMessage.picUrl } - break - case 'video': - msg.attachment = { type: 'video', content: firtsMessage.videoUrl } - break - default: - throw new BadRequestError('Format not supported') - } - return [conversation, msg, opts] - } - - /** - * Parse the message to the Connector format - */ - static formatMessage (conversation, message, opts) { - const reply = [] - let keyboards = null - - if (message.attachment.type === 'text') { - reply.push({ type: message.attachment.type, chatId: opts.chatId, to: opts.senderId, body: message.attachment.content }) - - } else if (message.attachment.type === 'picture') { - reply.push({ type: message.attachment.type, chatId: opts.chatId, to: opts.senderId, picUrl: message.attachment.content }) - - } else if (message.attachment.type === 'video') { - reply.push({ type: message.attachment.type, chatId: opts.chatId, to: opts.senderId, videoUrl: message.attachment.content }) - - } else if (message.attachment.type === 'quickReplies') { - keyboards = [{ type: 'suggested' }] - keyboards[0].responses = message.attachment.content.buttons.map(button => ({ type: 'text', body: button.title })) - reply.push({ type: 'text', chatId: opts.chatId, to: opts.senderId, body: message.attachment.content.title, keyboards }) - - } else if (message.attachment.type === 'card') { - if (message.attachment.content.buttons && message.attachment.content.buttons.length) { - keyboards = [{ type: 'suggested', responses: [] }] - message.attachment.content.buttons.forEach(button => { - if (button.type !== 'element_share') { keyboards[0].responses.push({ type: 'text', body: button.value }) } - }) - } - if (message.attachment.content.title) { - reply.push({ type: 'text', chatId: opts.chatId, to: opts.senderId, body: message.attachment.content.title }) - } - if (message.attachment.content.imageUrl) { - reply.push({ type: 'picture', chatId: opts.chatId, to: opts.senderId, picUrl: message.attachment.content.imageUrl }) - } - reply[reply.length - 1].keyboards = keyboards - } else if (message.attachment.type === 'carouselle') { - if (message.attachment.content && message.attachment.content.length) { - keyboards = [{ type: 'suggested' }] - keyboards[0].responses = message.attachment.content.map(c => ({ type: 'text', body: c.buttons[0].value })) - } - message.attachment.content.forEach(c => { - - if (c.title) { - reply.push({ type: 'text', chatId: opts.chatId, to: opts.senderId, body: c.buttons[0].title }) - } - if (c.imageUrl) { - reply.push({ type: 'picture', chatId: opts.chatId, to: opts.senderId, picUrl: c.imageUrl }) - } - }) - reply[reply.length - 1].keyboards = keyboards - } - - return reply - } - - /** - * Send the message to kik - */ - static async sendMessage (conversation, messages) { - for (const message of messages) { - await agent('POST', 'https://api.kik.com/v1/message') - .auth(conversation.channel.userName, conversation.channel.apiKey) - .send({ messages: [message] }) - } - } -} diff --git a/src/services/Messenger.service.js b/src/services/Messenger.service.js deleted file mode 100644 index 324a8e6..0000000 --- a/src/services/Messenger.service.js +++ /dev/null @@ -1,202 +0,0 @@ -import { getWebhookToken } from '../utils' -import { StopPipeline, BadRequestError } from '../utils/errors' -import ServiceTemplate from './Template.service' - -const agent = require('superagent-promise')(require('superagent'), Promise) - -export default class MessengerService extends ServiceTemplate { - - /** - * Suscribe webhook - */ - static connectWebhook (req, channel) { - return (req.query['hub.mode'] === 'subscribe' && req.query['hub.verify_token'] === getWebhookToken(channel._id, channel.slug)) - } - - /** - * Check to see if the message is form a valid webhook - */ - static checkSecurity (req, res) { - res.status(200).send() - } - - /** - * Check to see if the message is form a valid webhook - */ - static checkParamsValidity (channel) { - const { token, apiKey } = channel - - if (!token) { throw new BadRequestError('Parameter token is missing') } - if (!apiKey) { throw new BadRequestError('Parameter apiKey is missing') } - - return true - } - - /** - * Extract information from the request before the pipeline - */ - static extractOptions (req) { - const { body } = req - return { - chatId: `${body.entry[0].messaging[0].recipient.id}-${body.entry[0].messaging[0].sender.id}`, - senderId: body.entry[0].messaging[0].sender.id, - } - } - - /** - * Send directly a 200 to avoid the echo - */ - static async beforePipeline (req, res, channel) { - return channel - } - - /** - * Parse message to connector format - */ - static async parseChannelMessage (conversation, message, opts) { - const msg = { - attachment: {}, - channelType: 'messenger', - } - - if (message.entry[0].messaging[0].account_linking) { - msg.attachment.type = 'account_linking' - msg.attachment.status = message.entry[0].messaging[0].account_linking.status - - if (message.entry[0].messaging[0].account_linking.authorization_code) { - msg.attachment.content = message.entry[0].messaging[0].account_linking.authorization_code - } - - return Promise.all([conversation, msg, opts]) - } - - if (message.entry[0].messaging[0].postback) { - msg.attachment.type = 'payload' - msg.attachment.content = message.entry[0].messaging[0].postback.payload - return Promise.all([conversation, msg, opts]) - } - - if (!message.entry[0].messaging[0].message || (message.entry[0].messaging[0].message.is_echo && message.entry[0].messaging[0].message.app_id)) { - throw new StopPipeline() - } - - const facebookMessage = message.entry[0].messaging[0].message - const attachmentType = facebookMessage.attachments && facebookMessage.attachments[0].type - - if (attachmentType) { - msg.attachment.type = attachmentType === 'image' ? 'picture' : attachmentType - msg.attachment.content = facebookMessage.attachments[0].payload.url - } else { - msg.attachment.type = 'text' - - if (facebookMessage.quick_reply) { - msg.attachment.content = facebookMessage.quick_reply.payload - msg.attachment.is_button_click = true - } else { - msg.attachment.content = facebookMessage.text - } - } - - return Promise.all([conversation, msg, opts]) - } - - /* - * Parse message from bot-connector format to bot-connecto format - */ - static formatMessage (conversation, message, opts) { - let msg = null - - if (message.attachment.type !== 'text' && message.attachment.type !== 'quickReplies') { - let buttons = [] - msg = { - recipient: { id: opts.senderId }, - message: { - attachment: { - type: String, - payload: {}, - }, - }, - } - - if (message.attachment.type === 'picture') { - msg.message.attachment.type = 'image' - msg.message.attachment.payload.url = message.attachment.content - } else if (message.attachment.type === 'video' || message.attachment.type === 'audio') { - msg.message.attachment.type = message.attachment.type - msg.message.attachment.payload.url = message.attachment.content - } else if (message.attachment.type === 'card') { - const elements = [] - msg.message.attachment.type = 'template' - msg.message.attachment.payload.template_type = 'generic' - message.attachment.content.buttons.forEach(e => { - if (e.type === 'account_unlink') { - buttons.push({ type: e.type }) - } else if (e.type === 'web_url' || e.type === 'account_link') { - buttons.push({ type: e.type, title: e.title, url: e.value }) - } else if (e.type === 'postback' || e.type === 'phone_number' || e.type === 'element_share') { - buttons.push({ type: e.type, title: e.title, payload: e.value }) - } - }) - elements.push({ - title: message.attachment.content.title, - item_url: message.attachment.content.itemUrl, - image_url: message.attachment.content.imageUrl, - subtitle: message.attachment.content.subtitle, - buttons, - }) - msg.message.attachment.payload.elements = elements - } else if (message.attachment.type === 'carouselle') { - - const elements = [] - msg.message.attachment.type = 'template' - msg.message.attachment.payload.template_type = 'generic' - message.attachment.content.forEach(content => { - buttons = [] - content.buttons.forEach(e => { - if (e.type === 'web_url' || e.type === 'account_link') { - buttons.push({ type: e.type, title: e.title, url: e.value }) - } else if (e.type === 'postback' || e.type === 'phone_number' || e.type === 'element_share') { - buttons.push({ type: e.type, title: e.title, payload: e.value }) - } - }) - elements.push({ - subtitle: content.subtitle, - title: content.title, - item_url: content.itemUrl, - image_url: content.imageUrl, - buttons, - }) - }) - - msg.message.attachment.payload.elements = elements - } - - } else if (message.attachment.type === 'quickReplies') { - msg = { - recipient: { id: opts.senderId }, - message: { - text: message.attachment.content.title, - quick_replies: [], - }, - } - message.attachment.content.buttons.forEach(e => msg.message.quick_replies.push({ content_type: e.type ? e.type : 'text', title: e.title, payload: e.value })) - } else { - msg = { - recipient: { id: opts.senderId }, - message: { - text: message.attachment.content, - }, - } - } - return msg - } - - /* - * Send message back to facebook - */ - static async sendMessage (conversation, message) { - await agent('POST', `https://graph.facebook.com/v2.6/me/messages?access_token=${conversation.channel.token}`) - .send(message) - } - -} diff --git a/src/services/Slack.service.js b/src/services/Slack.service.js deleted file mode 100644 index 11214f7..0000000 --- a/src/services/Slack.service.js +++ /dev/null @@ -1,133 +0,0 @@ -import request from 'superagent' - -import ServiceTemplate from './Template.service' -import Logger from '../utils/Logger' -import { BadRequestError, ConnectorError } from '../utils/errors' - -export default class SlackService extends ServiceTemplate { - - static extractOptions (req) { - const { body } = req - - return { - chatId: body.event.channel, - senderId: body.event.user, - } - } - - static checkSecurity (req, res) { - res.status(200).send() - } - - static checkParamsValidity (channel) { - const { token } = channel - - if (!token) { throw new BadRequestError('Parameter token is missing') } - - return true - } - - /** - * Parse the message received by Slack to connector format - */ - static parseChannelMessage (conversation, message, opts) { - return new Promise((resolve, reject) => { - const parsedMessage = { - channelType: 'slack', - } - let attachment = {} - if (message.event.file) { - if (message.event.file.mimetype.startsWith('image')) { - attachment = { type: 'picture', content: message.event.file.url_private } - } else if (message.event.file.mimetype.startsWith('video')) { - attachment = { type: 'picture', content: message.event.file.url_private } - } else { - return reject(new ConnectorError('Sorry but we don\'t handle such type of file')) - } - } else { - attachment = { type: 'text', content: message.event.text } - } - parsedMessage.attachment = attachment - return resolve([conversation, parsedMessage, opts]) - }) - } - - // Transforms a message from connector universal format to slack format - static formatMessage (conversation, message) { - const { type, content } = message.attachment - let slackFormattedMessage = null - switch (type) { - case 'text': - slackFormattedMessage = { text: content } - break - case 'video': - slackFormattedMessage = { text: content } - break - case 'picture': - slackFormattedMessage = { text: content } - break - case 'quickReplies': - slackFormattedMessage = { - text: content.title, - } - slackFormattedMessage.attachments = [{ - fallback: 'Sorry but I can\'t display buttons', - attachment_type: 'default', - callback_id: 'callback_id', - actions: content.buttons.map(button => { - button.name = button.title - button.text = button.title - button.type = 'button' - delete button.title - return button - }), - }] - break - case 'card': - slackFormattedMessage = {} - slackFormattedMessage.attachments = [{ - title: content.title, - text: content.subtitle, - image_url: content.imageUrl, - fallback: 'Sorry but I can\'t display buttons', - attachment_type: 'default', - callback_id: 'callback_id', - actions: content.buttons.map(button => { - button.name = button.title - button.text = button.title - button.type = 'button' - delete button.title - return button - }), - }] - break - default: - throw new Error('Invalid message type') - } - return slackFormattedMessage - } - - /** - * Send a message to the Bot - */ - static sendMessage (conversation, message) { - return new Promise((resolve, reject) => { - const authParams = `token=${conversation.channel.token}&channel=${conversation.chatId}&as_user=true` - let params = '' - if (message.text) { - params = `&text=${message.text}` - } - if (message.attachments) { - params = `${params}&attachments=${JSON.stringify(message.attachments)}` - } - request.post(`https://slack.com/api/chat.postMessage?${authParams}${params}`) - .end((err) => { - if (err) { - Logger.error('Error while sending message to slack') - return reject(err) - } - resolve('Message sent') - }) - }) - } -} diff --git a/src/services/SlackApp.service.js b/src/services/SlackApp.service.js deleted file mode 100644 index de0baa9..0000000 --- a/src/services/SlackApp.service.js +++ /dev/null @@ -1,115 +0,0 @@ -import _ from 'lodash' -import request from 'superagent' - -import ServiceTemplate from './Template.service' -import Logger from '../utils/Logger' -import { StopPipeline, NotFoundError, BadRequestError } from '../utils/errors' - -export default class SlackAppService extends ServiceTemplate { - - static onChannelCreate (channel) { - channel.oAuthUrl = `${config.base_url}/oauth/slack/${channel._id}` - channel.save() - } - - static async beforePipeline (req, res, channel) { - /* Verification when filling the event subscription */ - if (req.body && req.body.type === 'url_verification') { - throw new StopPipeline(req.body.challenge) - } - - /* handle action (buttons) to format them */ - if (req.body.payload) { - req.body = SlackAppService.parsePayload(req.body) - } - - /* Search for the App children */ - channel = _.find(channel.children, child => child.slug === req.body.team_id) - if (!channel) { throw new NotFoundError('Channel') } - - /* check if event is only message */ - if (channel.type === 'slack' && req.body && req.body.event && req.body.event.type !== 'message') { - throw new StopPipeline() - } - - /* check if sender is the bot */ - if (req.body.event.user === channel.botuser) { - throw new StopPipeline() - } - - return channel - } - - static checkParamsValidity (channel) { - const { clientId, clientSecret } = channel - - if (!clientId) { throw new BadRequestError('Parameter clientId is missing') } - if (!clientSecret) { throw new BadRequestError('Parameter clientSecret is missing') } - - return true - } - - static parsePayload (body) { - const parsedBody = JSON.parse(body.payload) - - return ({ - team_id: parsedBody.team.id, - token: parsedBody.token, - event: { - type: 'message', - is_button_click: parsedBody.actions[0].type === 'button', - user: parsedBody.user.id, - text: parsedBody.actions[0].value, - ts: parsedBody.action_ts, - channel: parsedBody.channel.id, - event_ts: parsedBody.action_ts, - }, - type: 'event_callback', - }) - } - - static async onChannelDelete (channel) { - for (let child of channel.children) { - child = await models.Channel.findById(child) - if (child) { await child.remove() } - } - } - - static async receiveOauth (req, res) { - const { channel_id } = req.params - const { code } = req.query - const channel = await models.Channel.findById(channel_id) - - if (!channel) { - Logger.info(`Received request for oauth but no channel was found for id ${channel_id}`) - res.status(404).send() - return - } - res.status(200).send() - - request.post(`https://slack.com/api/oauth.access?client_id=${channel.clientId}&client_secret=${channel.clientSecret}&code=${code}`) - .end((err, res) => { - if (err || res.body.ok === false) { - Logger.error('Failed to identify to slack oauth') - } else { - const token = res.body.bot.bot_access_token - new models.Channel({ - token, - type: 'slack', - slug: res.body.team_id, - isActivated: true, - connector: channel.connector, - botuser: res.body.bot.bot_user_id, - app: channel_id, - }).save() - .then(channelChild => { - channel.children.push(channelChild._id) - channel.save() - }) - .catch(err => { - Logger.error(`An error occured while creating channel: ${err}`) - }) - } - }) - } -} diff --git a/src/services/Telegram.service.js b/src/services/Telegram.service.js deleted file mode 100644 index 7ae2679..0000000 --- a/src/services/Telegram.service.js +++ /dev/null @@ -1,144 +0,0 @@ -import _ from 'lodash' -import superAgent from 'superagent' -import superAgentPromise from 'superagent-promise' - -import ServiceTemplate from './Template.service' -import { ValidationError, BadRequestError } from '../utils/errors' - -const agent = superAgentPromise(superAgent, Promise) - -export default class TelegramService extends ServiceTemplate { - - static async setWebhook (token, webhook) { - const url = `https://api.telegram.org/bot${token}/setWebhook` - const { status } = await agent.post(url, { url: webhook }) - - if (status !== 200) { - throw new BadRequestError(`[Telegram][Status ${status}] Cannot set Webhook`) - } - } - - /* Telegram token is required */ - static checkParamsValidity (req) { - if (!req.token) { - throw new ValidationError('token', 'missing') - } - } - - /* Call when a channel is created, set webhook */ - static async onChannelCreate ({ token, webhook, ...channel }) { - try { - await TelegramService.setWebhook(token, webhook) - } catch (err) { - channel.isErrored = true - } - } - - /* Call when a channel is updated, update webhook */ - static onChannelUpdate = TelegramService.onChannelCreate - - /* Call when a channel is deleted */ - static async onChannelDelete ({ token, ...channel }) { - try { - const { status } = await agent.get(`https://api.telegram.org/bot${token}/deleteWebhook`) - - if (status !== 200) { - throw new BadRequestError(`[Telegram][Status ${status}] Cannot delete Webhook`) - } - } catch (err) { - channel.isErrored = true - } - } - - /* Call when a message is received, before the pipeline */ - static beforePipeline (req, res, channel) { - res.status(200).send({ status: 'success' }) - return channel - } - - // /* Call before entering the pipeline, to build the options object */ - static extractOptions ({ body }) { - return { - chatId: _.get(body, 'message.chat.id'), - senderId: _.get(body, 'message.chat.id'), - } - } - - /* Call to parse a message received from a channel */ - static parseChannelMessage (conversation, { message }, options) { - const type = Object.keys(message).slice(-1)[0] // Get the key name of last message element - const channelType = _.get(conversation, 'channel.type') - const content = _.get(message, `${type}`, '') - - return ([ - conversation, { - attachment: { type, content }, - channelType, - }, options, - ]) - } - - /* Call to format a message received by the bot */ - static formatMessage ({ channel, chatId }, { attachment }, { senderId }) { - const { type, content } = attachment - const buttons = _.get(content, 'buttons', []) - const reply = { - chatId, - type, - to: senderId, - token: _.get(channel, 'token'), - } - - switch (type) { - case 'picture': - return { ...reply, type: 'photo', body: content } - case 'quickReplies': - case 'card': - return { - ...reply, - type: 'card', - photo: _.get(content, 'imageUrl'), - body: [`*${_.get(content, 'title', '')}*`] - .concat('```') - .concat(buttons.map(({ title, value }) => `${value} - ${title}`)) - .concat('```') - .join('\n'), - } - case 'carouselle': - return { - ...reply, - body: content.map(({ imageUrl, buttons, title, subtitle }) => ({ - header: [`*${title}*`].concat(`[${subtitle}](${imageUrl})`).join('\n'), - text: ['```'].concat(buttons.map(({ title, value }) => `${value} - ${title}`)).concat('```').join('\n'), - })), - } - default: - return { ...reply, body: content } - } - } - - /* Call to send a message to a bot */ - static async sendMessage ({ channel }, { token, type, to, body, photo }) { - const url = `https://api.telegram.org/bot${token}` - const method = type === 'text' ? 'sendMessage' : `send${_.capitalize(type)}` - - try { - if (type === 'card') { - if (!_.isUndefined(photo)) { - await agent.post(`${url}/sendPhoto`, { chat_id: to, photo }) - } - await agent.post(`${url}/sendMessage`, { chat_id: to, text: body, parse_mode: 'Markdown' }) - } else if (type === 'carouselle') { - body.forEach(async ({ header, text }) => { - await agent.post(`${url}/sendMessage`, { chat_id: to, text: header, parse_mode: 'Markdown' }) - await agent.post(`${url}/sendMessage`, { chat_id: to, text, parse_mode: 'Markdown' }) - }) - } else { - await agent.post(`${url}/${method}`, { chat_id: to, [type]: body }) - } - } catch (err) { - channel.isErrored = true - } - } - -} diff --git a/src/services/Template.service.js b/src/services/Template.service.js deleted file mode 100644 index 88b99ee..0000000 --- a/src/services/Template.service.js +++ /dev/null @@ -1,38 +0,0 @@ -import { noop } from '../utils' - -export default class ServiceTemplate { - - /* Call when the Connector is launched */ - static onLaunch = noop - - /* Check parameter validity to create a Channel */ - static checkParamsValidity = noop - - /* Call when a channel is created */ - static onChannelCreate = noop - - /* Call when a channel is updated */ - static onChannelUpdate = noop - - /* Call when a channel is deleted */ - static onChannelDelete = noop - - /* Call when a message is received for security purpose */ - static checkSecurity = noop - - /* Call when a message is received, before the pipeline */ - static beforePipeline = noop - - /* Call before entering the pipeline, to build the options object */ - static extractOptions = noop - - /* Call to parse a message received from a channel */ - static parseChannelMessage = noop - - /* Call to format a message received by the bot */ - static formatMessage = noop - - /* Call to send a message to a bot */ - static sendMessage = noop - -} diff --git a/src/services/Twilio.service.js b/src/services/Twilio.service.js deleted file mode 100644 index 0d5cccb..0000000 --- a/src/services/Twilio.service.js +++ /dev/null @@ -1,133 +0,0 @@ -import _ from 'lodash' -import crypto from 'crypto' - -import ServiceTemplate from './Template.service' -import { - BadRequestError, - ForbiddenError, -} from '../utils/errors' - -const agent = require('superagent-promise')(require('superagent'), Promise) - -export default class TwilioService extends ServiceTemplate { - - /* Check parameter validity to create a Channel */ - static checkParamsValidity (channel) { - const { clientId, clientSecret, serviceId } = channel - channel.phoneNumber = channel.phoneNumber.split(' ').join('') - - if (!clientId) { throw new BadRequestError('Parameter is missing: Client Id') } - if (!clientSecret) { throw new BadRequestError('Parameter is missing: Client Secret') } - if (!serviceId) { throw new BadRequestError('Parameter is missing: Service Id') } - if (!channel.phoneNumber) { throw new BadRequestError('Parameter is missing: Phone Number') } - - return true - } - - static checkSecurity (req, res, channel) { - const signature = req.headers['x-twilio-signature'] - const webhook = channel.webhook - let str = webhook - _.forOwn(_.sortBy(Object.keys(req.body)), (value) => { - str += value - str += req.body[value] - }) - const hmac = crypto.createHmac('SHA1', channel.clientSecret).update(str).digest('base64') - if (signature !== hmac) { - throw new ForbiddenError() - } - } - - /* Call when a message is received, before the pipeline */ - static beforePipeline (req, res, channel) { - res.status(200).send() - return channel - } - - /* Call before entering the pipeline, to build the options object */ - static extractOptions (req) { - const { body } = req - - return { - chatId: `${body.To}${body.From}`, - senderId: body.From, - } - } - - /* Call to parse a message received from a channel */ - static parseChannelMessage (conversation, message, opts) { - const msg = { - attachment: { - type: 'text', - content: message.Body, - }, - channelType: 'twilio', - } - return [conversation, msg, opts] - } - - /* Call to format a message received by the bot */ - static formatMessage (conversation, message, opts) { - const { chatId } = conversation - const { type, content } = message.attachment - const to = opts.senderId - - const text = () => { return content } - - const quickReplies = () => { - if (!content.title || !content.buttons) { throw new BadRequestError('Missing buttons or title for quickReplies type') } - return [`${content.title}:`] - .concat(content.buttons.map(({ title }) => { - if (!title) { throw new BadRequestError('Missing title for quickReplies type') } - return title - })).join('\r\n') - } - - const card = () => { - if (!content.title || !content.buttons) { throw new BadRequestError('Missing buttons arguments or title for card type') } - return [`${content.title}:`] - .concat(content.buttons.map(({ title, value }) => { - if (!title || !value) { throw new BadRequestError('Missing title for card type') } - return `${value} - ${title}` - })).join('\r\n') - } - - const carouselle = () => { - const ret = [] - _.forEach(content, (card) => { - if (!card.title || !card.buttons) { throw new BadRequestError('Missing buttons arguments or title for carouselle type') } - if (card.subtitle) { card.title += `\r\n${card.subtitle}` } - ret.push([`${card.title}:`] - .concat(card.buttons.map(({ title, value }) => { - if (!title || !value) { throw new BadRequestError('Missing title for carouselle type') } - return `${value} - ${title}` - })).join('\r\n')) - }) - return ret.join('\r\n') - } - - const fns = { card, text, quickReplies, carouselle } - if (fns[type]) { return [{ chatId, to, body: fns[type](), type }] } - - throw new BadRequestError(`Message type ${type} unsupported by Twilio`) - } - - /* Call to send a message to a bot */ - static async sendMessage (conversation, messages) { - const data = { - From: conversation.channel.phoneNumber, - MessagingServiceSid: conversation.channel.serviceId, - To: '', - Body: '', - } - for (const message of messages) { - data.Body = message.body - data.To = message.to - await agent('POST', `https://api.twilio.com/2010-04-01/Accounts/${conversation.channel.clientId}/Messages.json`) - .auth(conversation.channel.clientId, conversation.channel.clientSecret) - .type('form') - .send(data) - } - } - -} diff --git a/src/services/index.js b/src/services/index.js deleted file mode 100644 index 4d18222..0000000 --- a/src/services/index.js +++ /dev/null @@ -1,17 +0,0 @@ -import Kik from './Kik.service' -import Slack from './Slack.service' -import SlackApp from './SlackApp.service' -import Messenger from './Messenger.service' -import Callr from './Callr.service' -import Twilio from './Twilio.service' -import Telegram from './Telegram.service' - -export default { - Kik, - Slack, - SlackApp, - Messenger, - Callr, - Twilio, - Telegram, -} diff --git a/src/test.js b/src/test.js deleted file mode 100644 index f3dfd83..0000000 --- a/src/test.js +++ /dev/null @@ -1,109 +0,0 @@ -import cors from 'cors' -import express from 'express' -import mongoose from 'mongoose' -import bodyParser from 'body-parser' - -/* Test Framework */ -import Mocha from 'mocha' - -import istanbul from 'istanbul' - -import configs from '../config' -import { createRouter } from './routes/' -import { initServices } from './utils/init' - -import path from 'path' -import recursive from 'recursive-readdir' - -import Logger from './utils/Logger' - -/* eslint-disable max-nested-callbacks*/ -global.Bot = require('./models/Bot.model') -global.Channel = require('./models/Channel.model') -global.Conversation = require('./models/Conversation.model') -global.Message = require('./models/Message.model') -global.Participant = require('./models/Participant.model') - -const app = express() - -// Load the configuration -const env = process.env.NODE_ENV || 'test' - -const config = configs[env] - -// Enable Cross Origin Resource Sharing -app.use(cors()) - -// Enable auto parsing of json content -app.use(bodyParser.json()) -app.use(bodyParser.urlencoded({ extended: true })) - -// Use native promise API with mongoose -mongoose.Promise = global.Promise - -// Mongoose connection -/* eslint-disable no-console */ -/* eslint-disable max-nested-callbacks */ -mongoose.connect(`mongodb://${config.db.host}:${config.db.port}/${config.db.dbName}`) -const db = mongoose.connection -db.on('error', err => { - Logger.error('FAILED TO CONNECT', err) - process.exit(1) -}) - -// Launch the application -db.once('open', () => { - createRouter(app) - initServices() - global.app = app - const port = config.server.port - app.listen(port, () => { - app.emit('ready') - Logger.success(`Test app listening on port ${port} !`) - Logger.info('[TEST] Launching test runner...') - - const mocha = new Mocha({ - reporter: 'dot', - timeout: '2000', - }) - const collector = new istanbul.Collector() - const reporter = new istanbul.Reporter() - const testsDirectory = './test' - - recursive(testsDirectory, (err, files) => { - if (err) { - process.exit(2) - } - // Files is an array of filename - Logger.info('[TEST] Listing test files...') - files.filter(file => { - return file.substr(-9) === '.tests.js' - }).forEach(file => { - mocha.addFile( - path.join('./', file) - ) - }) - - process.env.ROUTETEST = `http://localhost:${config.server.port}` - - mocha.run(errCount => { - mongoose.connection.db.dropDatabase() - - collector.add(global.__coverage__) - reporter.addAll(['text-summary', 'html']) - reporter.write(collector, true, () => { - Logger.info('\nCoverage report saved to coverage/index.html') - }) - - if (errCount > 0) { - Logger.info(`Total error${(errCount > 1 ? 's' : '')}: ${errCount}`) - process.exit(1) - } - process.exit(0) - }) - }) - }) -}) -/* eslint-enable max-nested-callbacks */ -/* eslint-enable no-console */ -/* eslint-enable max-nested-callbacks*/ diff --git a/src/utils/Logger.js b/src/utils/Logger.js deleted file mode 100644 index f2ba415..0000000 --- a/src/utils/Logger.js +++ /dev/null @@ -1,48 +0,0 @@ -const COLOR = { - BLACK: '30', - RED: '31', - GREEN: '32', - YELLOW: '33', - BLUE: '34', - PINK: '35', - CYAN: '36', - GREY: '37', -} - -/* eslint-disable no-console */ -class Logger { - static error (...messages) { - if (process.env.NODE_ENV === 'test') { return } - - messages.map(message => Logger.show(message, COLOR.RED)) - } - - static success (...messages) { - messages.map(message => Logger.show(message, COLOR.GREEN)) - } - - static warning (...messages) { - messages.map(message => Logger.show(message, COLOR.YELLOW)) - } - - static info (...messages) { - messages.map(message => Logger.show(message, COLOR.CYAN)) - } - - static log (...messages) { - messages.map(message => Logger.show(message)) - } - - static show (message, color) { - if (process.env.NODE_ENV !== 'test') { - if (!color) { - console.log(message) - } else { - console.log(`\x1b[${color}m`, `${message}`, '\x1b[0m') - } - } - } -} - -module.exports = Logger -/* eslint-esable no-console */ diff --git a/src/utils/errors.js b/src/utils/errors.js index 7352aad..4b44c73 100644 --- a/src/utils/errors.js +++ b/src/utils/errors.js @@ -1,103 +1,100 @@ -import { - Logger, +import { logger } from '../utils' - renderBadRequest, - renderForbidden, - renderUnauthorized, - renderNotFound, - renderConflict, - renderInternalServerError, - renderStopPipeline, - renderServiceUnavailable, -} from '../utils' +export class AppError extends Error { + constructor (message = '', results = null, status = 500) { + super(message) + this.constructor = AppError + // eslint-disable-next-line no-proto + this.__proto__ = AppError.prototype + this.name = this.constructor.name + Error.captureStackTrace(this, this.constructor) + this.status = status + this.content = { message, results } + } + render (res) { + return res.status(this.status).json(this.content) + } +} /** * 400 - Bad request */ -export class BadRequestError { - constructor (message = null, results = null) { - this.content = { message, results } +export class BadRequestError extends AppError { + constructor (message, results) { + super(message, results, 400) } } /** * 401 - Forbidden */ -export class ForbiddenError { - constructor (message = 'Request can not be processed with your role', results = null) { - this.content = { message, results } +export class ForbiddenError extends AppError { + constructor (message = 'Request can not be processed with your role', results) { + super(message, results, 401) } } /** * 403 - Unauthorized */ -export class UnauthorizedError { - constructor (message = 'Request can not be processed without authentication', results = null) { - this.content = { message, results } +export class UnauthorizedError extends AppError { + constructor (message = 'Request can not be processed without authentication', results) { + super(message, results, 403) } } /** * 404 - Not found */ -export class NotFoundError { - constructor (target = 'Model', results = null) { - this.content = { results, message: `${target} not found` } +export class NotFoundError extends AppError { + constructor (target = 'Model', results) { + const message = `${target} not found` + super(message, results, 404) } } -/** +/* * 409 - Conflict */ -export class ConflictError { - constructor (message, results = null) { - this.content = { results, message } +export class ConflictError extends AppError { + constructor (message, results) { + super(message, results, 409) } } -/** +/* * 503 - Service unavailable */ -export class ServiceError { - constructor (message, results = null) { - this.content = { message, results } +export class ServiceError extends AppError { + constructor (message, results) { + super(message, results, 503) } } /** - * Used to stop the pipeline + * 200 - Stop Pipeline */ export class StopPipeline { constructor (content) { + logger.warning('Abuse of JS exception mechanism') this.content = content } + render (res) { + return res.status(200).send(this.content) + } } /** * Render the appropriate error */ -export const renderConnectorError = (res, err) => { - if (res.headersSent) { return } - - if (err instanceof StopPipeline) { - return renderStopPipeline(res, err.content) - } - - if (err instanceof NotFoundError) { - return renderNotFound(res, err.content) - } else if (err instanceof BadRequestError) { - return renderBadRequest(res, err.content) - } else if (err instanceof ForbiddenError) { - return renderForbidden(res, err.content) - } else if (err instanceof UnauthorizedError) { - return renderUnauthorized(res, err.content) - } else if (err instanceof ConflictError) { - return renderConflict(res, err.content) - } else if (err instanceof ServiceError) { - return renderServiceUnavailable(res, err.content) +export const renderError = (res, err) => { + if (err instanceof AppError || err instanceof StopPipeline) { + return err.render(res) } + logger.error('Internal Server Error', (err && err.stack) || (err && err.message) || err) - Logger.error('Internal server error', (err && err.stack) || (err && err.message) || err) - return renderInternalServerError(res, err) + // Prevent sending twice a response in case of + // forwarding message from a channel to a bot + if (res.headersSent) { return } + return res.status(500).json(err) } diff --git a/src/utils/format.js b/src/utils/format.js index 86708f1..db85d31 100644 --- a/src/utils/format.js +++ b/src/utils/format.js @@ -1,19 +1,195 @@ import _ from 'lodash' +import { Validator } from 'jsonschema' -export const messageTypes = ['text', 'picture', 'video', 'quickReplies', 'card', 'carouselle', 'audio'] +import { BadRequestError } from './' + +const basicSchema = { + id: '/Basic', + type: 'object', + properties: { + type: { type: 'string' }, + content: { type: 'string' }, + }, + required: ['type', 'content'], +} + +const buttonSchema = { + id: '/Button', + type: 'object', + properties: { + type: { type: 'string' }, + title: { type: 'string' }, + value: { type: 'string' }, + }, +} + +const buttonsSchema = { + id: '/Buttons', + type: 'object', + properties: { + content: { + type: 'object', + properties: { + title: { type: 'string' }, + buttons: { + type: 'array', + items: { $ref: '/Button' }, + }, + }, + required: ['buttons'], + }, + }, + required: ['content'], +} + +const quickRepliesSchema = { + id: '/QuickReplies', + type: 'object', + properties: { + type: { type: 'string' }, + content: { + type: 'object', + properties: { + title: { type: 'string' }, + buttons: { + type: 'array', + items: { $ref: '/Button' }, + }, + }, + required: ['title', 'buttons'], + }, + }, + required: ['type', 'content'], +} + +const customSchema = { + id: '/Custom', + type: 'object', + properties: { + content: { + anyOf: [ + { + type: 'object', + patternProperties: { + '^([A-Za-z0-9-_]+)$': {}, + }, + additionalProperties: false, + }, + { + type: 'array', + }, + { + type: 'string', + }, + ], + }, + }, + required: ['content'], +} + +const val = new Validator() +val.addSchema(basicSchema, '/Basic') +val.addSchema(buttonSchema, '/Button') +val.addSchema(buttonsSchema, '/Buttons') +val.addSchema(quickRepliesSchema, '/QuickReplies') +val.addSchema(customSchema, '/Custom') + +export const messageTypes = [ + 'text', + 'conversation_start', + 'conversation_end', + 'picture', + 'video', + 'quickReplies', + 'card', + 'carouselle', + 'audio', + 'carousel', + 'list', + 'buttons', + 'custom', +] + +export const validate = (message, schema) => { + const { errors: [error] } = val.validate(message, schema) + + return error +} export function isValidFormatMessage (message) { if (!_.isObject(message) - || !message.type || !message.content - || messageTypes.indexOf(message.type) === -1) { + || !message.type || !message.content + || messageTypes.indexOf(message.type) === -1) { return false } - if (message.type === 'text' && !_.isString(message.content)) { return false } - if (message.type === 'picture' && !_.isString(message.content)) { return false } - if (message.type === 'video' && !_.isString(message.content)) { return false } - if (message.type === 'quickReplies' && !_.isObject(message.content)) { return false } - if (message.type === 'card' && !_.isObject(message.content)) { return false } + const { type, content } = message + if (type === 'text' && !_.isString(content)) { return false } + if (type === 'conversation_start' && !_.isString(content)) { return false } + if (type === 'conversation_end' && !_.isString(content)) { return false } + if (type === 'picture' && !_.isString(content)) { return false } + if (type === 'video' && !_.isString(content)) { return false } + if (type === 'quickReplies' && !_.isObject(content)) { return false } + if (type === 'card' && !_.isObject(content)) { return false } + if (type === 'buttons' && !_.isObject(content)) { return false } + if (type === 'custom' + && !_.isObject(content) + && !_.isArray(content) + ) { + return false + } return true } + +export const textFormatMessage = (message, separator = '\n', buttonSeparator = '- ') => { + const { attachment: { type, content } } = message + + let body = '' + switch (type) { + case 'text': + case 'picture': + case 'video': { + body = content + break + } + case 'list': { + const { elements } = content + body = _.reduce(elements, (acc, elem) => `${acc}${separator}` + + `${separator}${elem.title}` + + `${separator}${elem.subtitle}` + + `${separator}${elem.imageUrl}`, '') + break + } + case 'buttons': + case 'quickReplies': { + const { title, buttons } = content + body = `${title}${separator}` + .concat(buttons.map(b => `${buttonSeparator}${b.title}`) + .join(separator)) + break + } + case 'card': { + const { title, subtitle, imageUrl, buttons } = content + body = _.reduce(buttons, (acc, b) => + `${acc}${separator}${buttonSeparator}${b.title}`, + `${title}${separator}${subtitle}${separator}${imageUrl}`, '') + break + } + case 'carouselle': + case 'carousel': { + body = _.reduce(content, (acc, card) => { + const { title, subtitle, imageUrl, buttons } = card + // eslint-disable-next-line prefer-template + return acc + _.reduce(buttons, (acc, b) => + `${acc}${buttonSeparator}${b.title}${separator}`, + `${title}${separator}${subtitle}${separator}${imageUrl}${separator}`, '') + separator + }, '') + break + } + default: + throw new BadRequestError('Message type non-supported by text based service') + } + + return { type, body } +} diff --git a/src/utils/headers.js b/src/utils/headers.js new file mode 100644 index 0000000..2dade0a --- /dev/null +++ b/src/utils/headers.js @@ -0,0 +1,13 @@ +import _ from 'lodash' + +const AUTH_HEADERS = [ + 'authorization', + 'x-recast-user', +] + +const getAuthHeaders = (headers) => _.pickBy(headers, (_value, key) => AUTH_HEADERS.includes(key)) + +module.exports = { + AUTH_HEADERS, + getAuthHeaders, +} diff --git a/src/utils/index.js b/src/utils/index.js index 21f4785..e3f831d 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -1,18 +1,33 @@ export { noop, - invoke, - invokeSync, + arrayfy, isInvalidUrl, getWebhookToken, + getTwitterWebhookToken, + deleteTwitterWebhook, + postMediaToTwitterFromUrl, + sendToWatchers, + removeOldRedisMessages, + findUserRealName, + messageHistory, + formatMessageHistory, + formatUserMessage, } from './utils' export { + lineSendMessage, + lineGetUserProfile, +} from './line' + +export { + AppError, BadRequestError, ForbiddenError, UnauthorizedError, NotFoundError, ServiceError, - renderConnectorError, + renderError, + StopPipeline, } from './errors' export { @@ -27,8 +42,9 @@ export { renderInternalServerError, renderServiceUnavailable, renderStopPipeline, + renderPolledMessages, } from './responses' -export Logger from './Logger' -export { initServices } from './init' -export { messageTypes, isValidFormatMessage } from './format' +export { logger } from './log' +export { fmtConversationHeader, fmtMessageDate, sendMail, sendArchiveByMail } from './mail' +export { messageTypes, isValidFormatMessage, textFormatMessage } from './format' diff --git a/src/utils/init.js b/src/utils/init.js deleted file mode 100644 index 3bdf054..0000000 --- a/src/utils/init.js +++ /dev/null @@ -1,11 +0,0 @@ -import _ from 'lodash' - -/** - * Starts all services - */ -export function initServices () { - - _.forOwn(services, (service) => { - service.onLaunch() - }) -} diff --git a/src/utils/line.js b/src/utils/line.js new file mode 100644 index 0000000..f56a9c0 --- /dev/null +++ b/src/utils/line.js @@ -0,0 +1,31 @@ +import superagent from 'superagent' +import superagentPromise from 'superagent-promise' + +import { ServiceError } from './' + +const agent = superagentPromise(superagent, Promise) + +const getLineApiUrl = url => `https://api.line.me/${url}` + +export const lineSendMessage = async (clientToken, replyToken, messages) => { + // Line only supports up to 5 messages in one request, but the replyToken can + // only be used once. Therefore, we only send the first 5 messages for now. + await agent.post(getLineApiUrl('v2/bot/message/reply')) + .set('Content-Type', 'application/json') + .set('Authorization', `Bearer ${clientToken}`) + .send({ + replyToken, + messages: messages.slice(0, 5), + }) +} + +export const lineGetUserProfile = async (clientToken, userId) => { + try { + const { body } = await agent.get(getLineApiUrl(`v2/bot/profile/${userId}`)) + .set('Authorization', `Bearer ${clientToken}`) + + return body + } catch (error) { + throw new ServiceError('Error while retrieving line user profile information') + } +} diff --git a/src/utils/log.js b/src/utils/log.js new file mode 100644 index 0000000..a391cac --- /dev/null +++ b/src/utils/log.js @@ -0,0 +1,28 @@ +import winston from 'winston' + +export const winstonLogger = new winston.Logger({ + transports: [ + new winston.transports.Console({ + colorize: true, + handleExceptions: true, + level: 'info', + prettyPrint: true, + timestamp: true, + }), + ], + exitOnError: false, +}) + +winstonLogger.stream = { + write: (message) => { + winstonLogger.info(message) + }, +} + +export const logger = { + error: (...messages) => messages.map(message => winstonLogger.error(message)), + warning: (...messages) => messages.map(message => winstonLogger.warn(message)), + info: (...messages) => messages.map(message => winstonLogger.info(message)), + debug: (...messages) => messages.map(message => winstonLogger.debug(message)), +} + diff --git a/src/utils/mail.js b/src/utils/mail.js new file mode 100644 index 0000000..eee4bc1 --- /dev/null +++ b/src/utils/mail.js @@ -0,0 +1,51 @@ +import archiver from 'archiver' +import nodemailer from 'nodemailer' + +import { logger } from './index' +import config from '../../config' + +export const fmtMessageDate = (message) => { + const date = message.receivedAt || message.createdAt || new Date() + return `${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}` +} + +export const fmtConversationHeader = (conversation, participants) => { + const user = participants.find(p => !p.isBot) + const userInformations = user && user.data + ? 'Participant informations:\n' + + `- SenderId: ${user.senderId}\n` + + `${Object.keys(user.data).map(k => `- ${k} => ${user.data[k]}`).join('\n')}\n` + : `Participant informations:\n- SenderId: ${user && user.senderId}\n` + const conversationInformations = `Conversation informations: +- Chat ID: ${conversation._id} +- created: ${conversation.createdAt} + ` + + return `${conversationInformations}\n${userInformations}\n` +} + +export async function sendArchiveByMail (message) { + const archive = archiver('zip', { + zlib: { level: 9 }, + }) + + archive.on('error', err => { + logger.error(`Failed to create the zip archive: ${err}`) + throw err + }) + + for (const file of message.attachments) { + archive.append(file.content, { name: file.filename }) + } + archive.finalize() + + return sendMail({ + ...message, + attachments: [{ content: archive, filename: 'conversations.zip' }], + }) +} + +export function sendMail (message) { + return nodemailer.createTransport(config.mail) + .sendMail(message) +} diff --git a/src/utils/message_queue.js b/src/utils/message_queue.js new file mode 100644 index 0000000..b9df8cd --- /dev/null +++ b/src/utils/message_queue.js @@ -0,0 +1,54 @@ +import config from '../../config' +import kue from 'kue' +import { logger } from './index' + +export class MessageQueue { + constructor () { + this.pollWatchers = {} + // Redis queue to handle messages to send to long-polling sessions + this.queue = kue.createQueue({ redis: config.redis }) + } + + subscribeToEvents () { + const self = this + this.queue.on('error', (err) => { + logger.error(`Unexpected Redis/Kue error: ${err}`) + process.exit(1) + }) + // message received in a conversation + this.queue.on('job enqueue', (id, conversationId) => { + const watchers = self.pollWatchers[conversationId] + if (!watchers || Object.keys(watchers).length <= 0) { return } + kue.Job.get(id, (err, message) => { + if (err) { return logger.error(`Error while getting message from Redis: ${err}`) } + Object.values(watchers).forEach(watcher => watcher.handler(message.data)) + }) + }) + } + + removeWatcher (conversationId, watcherId) { + if (this.pollWatchers[conversationId]) { + delete this.pollWatchers[conversationId][watcherId] + if (Object.keys(this.pollWatchers[conversationId]).length <= 0) { + delete this.pollWatchers[conversationId] + } + } + } + + setWatcher (conversationId, watcherId, watcher) { + if (!this.pollWatchers[conversationId]) { + this.pollWatchers[conversationId] = {} + } + this.pollWatchers[conversationId][watcherId] = { handler: watcher } + } + + getQueue () { + return this.queue + } + +} + +const defaultQueue = new MessageQueue() +defaultQueue.subscribeToEvents() + +export default defaultQueue diff --git a/src/utils/responses.js b/src/utils/responses.js index 61760e3..af3ce8f 100644 --- a/src/utils/responses.js +++ b/src/utils/responses.js @@ -13,34 +13,12 @@ export const renderDeleted = (res, message) => { }) } -export const renderBadRequest = (res, content) => { - return res.status(400).json(content) -} - -export const renderForbidden = (res, content) => { - return res.status(401).json(content) -} - -export const renderUnauthorized = (res, content) => { - return res.status(403).json(content) -} - -export const renderNotFound = (res, content) => { - return res.status(404).json(content) -} - -export const renderConflict = (res, content) => { - return res.status(409).json(content) -} - -export const renderInternalServerError = (res, content) => { - return res.status(500).json(content) -} - -export const renderServiceUnavailable = (res, content) => { - return res.status(503).json(content) -} - -export const renderStopPipeline = (res, content) => { - return res.status(200).send(content) +export const renderPolledMessages = (res, messages, waitTime) => { + return res.status(200).json({ + message: `${messages.length} messages`, + results: { + messages, + waitTime, + }, + }) } diff --git a/src/utils/utils.js b/src/utils/utils.js index 416f31b..5434d4e 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -1,29 +1,244 @@ import is from 'is_js' import md5 from 'blueimp-md5' +import { createHmac } from 'crypto' +import request from 'request' +import Twit from 'twit' +import fileType from 'file-type' +import http from 'http' +import fs from 'fs' +import request2 from 'superagent' +import tmp from 'tmp' +import kue from 'kue' +import _ from 'lodash' +import { Message, Participant } from '../models' +import { logger } from './index' +import messageQueue from './message_queue' export function getWebhookToken (id, slug) { return md5(id.toString().split('').reverse().join(''), slug) } +export function getTwitterWebhookToken (first, second) { + const hmac = createHmac('sha256', first) + hmac.update(second) + return 'sha256='.concat(hmac.digest('base64')) +} + +export function deleteTwitterWebhook (T, webhookToken, envName) { + T.config.app_only_auth = false + return new Promise((resolve, reject) => { + T._buildReqOpts('DELETE', + `account_activity/all/${envName}/webhooks/${webhookToken}`, {}, false, (err, reqOpts) => { + if (err) { return reject(err) } + T._doRestApiRequest(reqOpts, {}, 'DELETE', (err) => { + if (err) { return reject(err) } + return resolve() + }) + }) + }) +} + +export function getFileType (url) { + return new Promise((resolve, reject) => { + http.get(url, res => { + res.once('data', chunk => { + res.destroy() + resolve(fileType(chunk)) + }) + res.once('error', () => { reject(new Error('could not get file type')) }) + }) + }) +} + +// We can return the media_id before we upload the file, +// but if there is an error while uploading, we can't handle it as easily +export function postMediaToTwitterFromUrl (channel, url) { + return new Promise((resolve, reject) => { + const T = new Twit({ + consumer_key: channel.consumerKey, + consumer_secret: channel.consumerSecret, + access_token: channel.accessToken, + access_token_secret: channel.accessTokenSecret, + timeout_ms: 60 * 1000, + }) + + request.head(url, (err, res) => { + if (err) { + return reject(err) + } + const length = parseInt(res.headers['content-length'], 10) + // max 15 MB imposed by twitter + if (length > 15 * 1024 * 1024) { + return reject(new Error('media too large, 15 MB limit')) + } + const parts = url.split('.') + const extension = '.'.concat(parts[parts.length - 1]) + let tmpfile = null + try { + tmpfile = tmp.fileSync({ postfix: extension }) + } catch (err2) { + return reject(err2) + } + request2.get(url).end((err, res) => { + if (err) { + return reject(err) + } + const content = res.body + try { + fs.writeFileSync(tmpfile.name, content, { encoding: 'binary' }) + } catch (err3) { + return reject(err3) + } + T.postMediaChunked({ file_path: tmpfile.name }, (err, data) => { + tmpfile.removeCallback() + if (err) { + return reject(err) + } + resolve(data.media_id_string) + }) + }) + }) + }) +} + export function noop () { return Promise.resolve() } /** - * * Invoke an async service method - * */ -export async function invoke (serviceName, methodName, args) { - return global.services[serviceName][methodName](...args) + * Check if an url is valid + */ +export const isInvalidUrl = url => (!url || (!is.url(url) && !(/localhost/).test(url))) + +export const arrayfy = (content) => [].concat.apply([], [content]) + +export function sendToWatchers (convId, msgs) { + return new Promise((resolve, reject) => { + messageQueue.getQueue().create(convId, msgs) + .save(err => { + if (err) { return reject(err) } + resolve() + }) + }) } -/** - * * Invoke a sync service method - * */ -export function invokeSync (serviceName, methodName, args) { - return global.services[serviceName][methodName](...args) +export function removeOldRedisMessages () { + const now = Date.now() + // -1 means that we don't limit the number of results, we could set that to 1000 + kue.Job.rangeByState('inactive', 0, -1, 'asc', (err, messages) => { + if (err) { return logger.error(`Error while getting messages from Redis: ${err}`) } + messages.forEach(msg => { + if (now - msg.created_at > 10 * 1000) { msg.remove() } + }) + }) } -/** - * * Check if an url is valid - * */ -export const isInvalidUrl = url => (!url || (!is.url(url) && !(/localhost/).test(url))) +export async function findUserRealName (conversation) { + const participants = await Participant.find({ conversation: conversation._id }) + const users = participants.filter(p => !p.isBot && !p.type === 'agent') + if (users.length === 0) { + return `Anonymous user from ${conversation.channel.type}` + } + const user = users[0] + + if (user && user.data && user.data.userName) { + return user.userName + } + if (user && user.data && user.data.first_name && user.data.last_name) { + return `${user.data.first_name} ${user.data.last_name}` + } + return `Anonymous user from ${conversation.channel.type}` +} + +export async function messageHistory (conversation) { + const lastMessages = await Message + .find({ conversation: conversation._id }) + .sort({ receivedAt: -1 }) + .populate('participant') + .exec() + + return lastMessages +} + +export function formatMessageHistory (history) { + return history + .map(m => { + const message = formatUserMessage(m) + switch (m.participant.type) { + case 'bot': + return `Bot: ${message}` + case 'agent': + return `Agent: ${message}` + default: + return `User: ${message}` + } + }) + .concat('This is the history of the conversation between the user and the bot:') + .reverse() +} + +export function formatUserMessage (message) { + switch (message.attachment.type) { + case 'text': + return message.attachment.content + case 'picture': + return '[Image]' + default: + return '[Rich message]' + } +} + +export function formatMarkdownHelper (message, linkMarkdown = false, boldItalicMarkdown = true) { + const applyRegex = (content) => { + const starsToHash = (result) => result.replace(/\*\*/gm, '#7Uk0I2smS') + const underscoresToHash = (result) => result.replace(/__/gm, '#7Uk0I2smS') + const modifySingleStars = (result) => result.replace(/^\*|\*$/gm, '_') + const modifyDoubleStars = (result) => result.replace(/^\*\*|\*\*([^*]|$)/gm, starsToHash) + const modifyDoubleUnderscores = (result) => result.replace(/^__|__([^_]|$)/gm, underscoresToHash) + const modifyTextLink = (result) => result.replace(/\[|\]/g, ' ') + const modifyEmptyLink = (result) => result.replace(/\(|\)|\[|\]/g, '') + const modifyStarsSpace = (result) => result.replace(/\*[\s]+|[\s]+\*/gm, '*') + const modifyUnderscoresSpace = (result) => result.replace(/_[\s]+|[\s]+_/gm, '_') + const modifyDoubleStarsSpace = (result) => result.replace(/\*\*[\s]+|[\s]+\*\*/gm, '**') + const modifyDoubleUnderscoresSpace = (result) => result.replace(/__[\s]+|[\s]+__/gm, '__') + let formattedContent = content + if (boldItalicMarkdown) { + formattedContent = formattedContent + .replace(/\*\*(.*?)\*\*([^*]|$)/gm, modifyDoubleStars) + .replace(/__(.*?)__([^_]|$)/gm, modifyDoubleUnderscores) + .replace(/\*(.*?)\*/gm, modifySingleStars) + .replace(/#7Uk0I2smS/gm, '*') + } + if (linkMarkdown) { + formattedContent = formattedContent + .replace(/\[.+\]\(.*\)/gm, modifyTextLink) + .replace(/\[\]\(.*\)/g, modifyEmptyLink) + } + return formattedContent + .replace(/\*(.*?)\*/gm, modifyStarsSpace) + .replace(/_(.*?)_/gm, modifyUnderscoresSpace) + .replace(/\*\*(.*?)\*\*/gm, modifyDoubleStarsSpace) + .replace(/__(.*?)__/gm, modifyDoubleUnderscoresSpace) + } + const type = _.get(message, 'attachment.type') + if (type === 'text') { + const content = _.get(message, 'attachment.content') + const regexedMessage = applyRegex(content) + message = _.set(message, 'attachment.content', regexedMessage) + } + if (type === 'quickReplies') { + const content = _.get(message, 'attachment.content.title') + const regexedMessage = applyRegex(content) + message = _.set(message, 'attachment.content.title', regexedMessage) + } + return message +} + +_.mixin({ + sortByKeys: (obj, comparator) => + _(obj).toPairs() + .sortBy( + pair => comparator ? comparator(pair[1], pair[0]) : 0 + ) + .fromPairs(), +}) diff --git a/src/validators/Channels.validators.js b/src/validators/Channels.validators.js deleted file mode 100644 index 1aaedbe..0000000 --- a/src/validators/Channels.validators.js +++ /dev/null @@ -1,21 +0,0 @@ -import filter from 'filter-object' - -import { invoke } from '../utils' -import { BadRequestError } from '../utils/errors' - -const permitted = '{type,slug,isActivated,token,userName,apiKey,webhook,clientId,clientSecret,password,phoneNumber,serviceId}' - -export async function createChannelByConnectorId (req) { - const { slug, type } = req.body - const newChannel = new models.Channel(filter(req.body, permitted)) - - if (!type) { - throw new BadRequestError('Parameter type is missing') - } else if (!slug) { - throw new BadRequestError('Parameter slug is missing') - } else if (!services[type]) { - throw new BadRequestError('Parameter type is invalid') - } - - await invoke(newChannel.type, 'checkParamsValidity', [newChannel]) -} diff --git a/src/validators/channels.js b/src/validators/channels.js new file mode 100644 index 0000000..4ba929b --- /dev/null +++ b/src/validators/channels.js @@ -0,0 +1,46 @@ +import filter from 'filter-object' + +import * as channelConstants from '../constants/channels' +import { BadRequestError } from '../utils/errors' +import { Channel } from '../models' +import { getChannelIntegrationByIdentifier } from '../channel_integrations' +import validatorConfig from './validators-config' + +const throwErrIfStringExceedsMax = (str, max, errDisplayName) => { + if (str && (str.length > max)) { + throw new BadRequestError(`${errDisplayName} should be at most ${max} characters long`) + } +} + +const throwErrIfPlaceholderExceedsMax = (placeholderText) => { + return () => throwErrIfStringExceedsMax( + placeholderText, validatorConfig.USER_INPUT_PLACEHOLDER_MAX, 'User input placeholder') +} + +export async function createChannelByConnectorId (req) { + const slug = req.body.slug + const type = req.body.type + const inputPlaceholder = req.body.userInputPlaceholder + const newChannel = new Channel(filter(req.body, channelConstants.permitted)) + + if (!type) { + throw new BadRequestError('Parameter type is missing') + } else if (!slug) { + throw new BadRequestError('Parameter slug is missing') + } else if (!getChannelIntegrationByIdentifier(type)) { + throw new BadRequestError('Parameter type is invalid') + } + throwErrIfPlaceholderExceedsMax(inputPlaceholder)() + + const channelIntegration = getChannelIntegrationByIdentifier(newChannel.type) + channelIntegration.validateChannelObject(newChannel) +} + +export const updateChannel = (req) => { + const slug = req.body.slug + + if (slug === '') { + throw new BadRequestError('Parameter slug cannot be empty') + } + throwErrIfPlaceholderExceedsMax(req.body.userInputPlaceholder)() +} diff --git a/src/validators/Connectors.validators.js b/src/validators/connectors.js similarity index 79% rename from src/validators/Connectors.validators.js rename to src/validators/connectors.js index eccc1b0..6cc09e4 100644 --- a/src/validators/Connectors.validators.js +++ b/src/validators/connectors.js @@ -2,7 +2,7 @@ import { isInvalidUrl } from '../utils' import { BadRequestError } from '../utils/errors' export const createConnector = (req) => { - const { url } = req.body + const url = req.body.url if (!url) { throw new BadRequestError('Parameter url is missing') @@ -11,8 +11,8 @@ export const createConnector = (req) => { } } -export const updateConnectorByBotId = (req) => { - const { url } = req.body +export const updateConnector = (req) => { + const url = req.body.url if (url && isInvalidUrl(url)) { throw new BadRequestError('Parameter url is invalid') diff --git a/src/validators/index.js b/src/validators/index.js new file mode 100644 index 0000000..2d0b124 --- /dev/null +++ b/src/validators/index.js @@ -0,0 +1,2 @@ +export ConnectorsValidators from './connectors' +export ChannelsValidators from './channels' diff --git a/src/validators/validators-config.js b/src/validators/validators-config.js new file mode 100644 index 0000000..bcdf9f1 --- /dev/null +++ b/src/validators/validators-config.js @@ -0,0 +1,5 @@ +const validatorConfig = { + USER_INPUT_PLACEHOLDER_MAX: 35, +} + +export default Object.freeze(validatorConfig) diff --git a/test/channel_integrations/modules.js b/test/channel_integrations/modules.js new file mode 100644 index 0000000..d031be5 --- /dev/null +++ b/test/channel_integrations/modules.js @@ -0,0 +1,93 @@ +import chai from 'chai' +import * as integrations_module from '../../src/channel_integrations' +const { getChannelIntegrationByIdentifier, getChannelIntegrationRoutes, + MODULES } = integrations_module +import Callr from '../../src/channel_integrations/callr/channel' + +const should = chai.should() +const expect = chai.expect + +/* eslint no-unused-expressions: 0 */ // --> OFF + +function validateRouteObject (route) { + expect(['GET', 'POST', 'PUT']).to.include(route.method) + expect(route.path).to.be.an('array').that.is.not.empty + expect(route.validators).to.be.an('array') + expect(route.authenticators).to.be.an('array') + expect(route.handler).to.be.a('function') +} + +const sourcePath = '../../src/channel_integrations' +// flatten +const IDENTIFIERS = [].concat.apply([], MODULES.map(moduleName => require(`${sourcePath}/${moduleName}`).identifiers)) + +MODULES.forEach(moduleName => describe(`${moduleName} channel integration module`, () => { + + let module + beforeEach(() => { + module = require(`${sourcePath}/${moduleName}`) + }) + + it('should have unique identifiers', () => { + module.identifiers.forEach(identifier => { + const count = IDENTIFIERS.filter(i => i === identifier).length + expect(count).to.equal(1) + }) + }) + + it('should export all mandatory fields', () => { + should.exist(module) + should.exist(module.channel) + should.exist(module.identifiers) + expect(module.identifiers).to.be.an('array') + module.identifiers.forEach(i => expect(i).to.be.a('string')) + }) + + it('should export optional fields with the correct type', () => { + if (module.routes) { expect(module.routes).to.be.an('array') } + }) + + it('should define valid routes', () => { + if (!module.routes) { return } + module.routes.forEach(validateRouteObject) + }) + + it('should define all mandatory service methods') + +})) + +describe('getChannelIntegrationByIdentifier', () => { + + it('should return undefined for unknown identifiers', () => { + expect(getChannelIntegrationByIdentifier('unknown')).to.be.undefined + }) + + describe('with correct identifier', () => { + + let integration + + beforeEach(() => { + integration = getChannelIntegrationByIdentifier('callr') + }) + + it('should return a channel integration instance', () => { + expect(integration).to.be.an.instanceof(Callr) + }) + + it('should return an instance which overrides abstract methods', () => { + const body = { body: { data: { from: 'me', to: 'you' } } } + expect(integration.populateMessageContext(body)).to.eql({ chatId: 'me', senderId: 'you' }) + }) + }) +}) + +describe('getChannelIntegrationRoutes', () => { + + it('should return a non-empty array', () => { + expect(getChannelIntegrationRoutes()).to.be.an('array').that.is.not.empty + }) + + it('should only contain proper route objects', () => { + getChannelIntegrationRoutes().forEach(validateRouteObject) + }) +}) diff --git a/test/controllers/Bots.controller.tests.js b/test/controllers/Bots.controller.tests.js deleted file mode 100644 index 671f539..0000000 --- a/test/controllers/Bots.controller.tests.js +++ /dev/null @@ -1,155 +0,0 @@ -import chai from 'chai' -import chaiHttp from 'chai-http' -import mongoose from 'mongoose' -import Bot from '../../src/models/Bot.model' -import Conversation from '../../src/models/Conversation.model' -import Channel from '../../src/models/Channel.model' - -import BotsController from '../../src/controllers/Bots.controller' - -import sinon from 'sinon' - -const assert = require('chai').assert -const expect = chai.expect -const should = chai.should() - -chai.use(chaiHttp) - -function clearDB () { - for (const i in mongoose.connection.collections) { - if (mongoose.connection.collections.hasOwnProperty(i)) { - mongoose.connection.collections[i].remove() - } - } -} - -const url = 'https://bonjour.com' -const baseUrl = 'http://localhost:8080' -const updatedUrl = 'https://aurevoir.com' - -describe('Bot controller', () => { - describe('POST: create a bot', () => { - after(async () => clearDB()) - it ('should be a 201', async () => { - const res = await chai.request(baseUrl) - .post('/bots').send({ url }) - const { message, results } = res.body - - assert.equal(res.status, 201) - assert.equal(results.url, url) - assert.equal(message, 'Bot successfully created') - }) - }) - - describe('GET: get bots', async () => { - after(async () => clearDB()) - afterEach(async () => clearDB()) - - it ('should be a 200 with bots', async () => { - await Promise.all([ - new Bot({ url }).save(), - new Bot({ url }).save(), - ]) - const res = await chai.request(baseUrl).get('/bots').send() - const { message, results } = res.body - - assert.equal(res.status, 200) - assert.equal(results.length, 2) - assert.equal(message, 'Bots successfully found') - }) - - it ('should be a 200 with no bots', async () => { - const res = await chai.request(baseUrl).get('/bots').send() - const { message, results } = res.body - - assert.equal(res.status, 200) - assert.equal(results.length, 0) - assert.equal(message, 'No Bots') - }) - }) - - describe('GET: get bot by id', () => { - after(async () => clearDB()) - - it ('should be a 200 with bots', async () => { - const bot = await new Bot({ url }).save() - const res = await chai.request(baseUrl).get(`/bots/${bot._id}`).send() - const { message, results } = res.body - - assert.equal(res.status, 200) - assert.equal(results.id, bot._id) - assert.equal(results.url, bot.url) - assert.equal(message, 'Bot successfully found') - }) - - it ('should be a 404 with no bots', async () => { - try { - await chai.request(baseUrl).get('/bots/582a4ced73b15653c074606b').send() - should.fail() - } catch (err) { - const res = err.response - const { message, results } = res.body - - assert.equal(res.status, 404) - assert.equal(results, null) - assert.equal(message, 'Bot not found') - } - }) - }) - - describe('PUT: update a bot', () => { - let bot = {} - before(async () => bot = await new Bot({ url }).save()) - after(async () => clearDB()) - - it ('should be a 200', async () => { - const res = await chai.request(baseUrl).put(`/bots/${bot._id}`) - .send({ url: updatedUrl }) - const { message, results } = res.body - - assert.equal(res.status, 200) - assert.equal(results.url, updatedUrl) - assert.equal(message, 'Bot successfully updated') - }) - - it ('should be a 404 with no bots', async () => { - try { - await chai.request(baseUrl).put('/bots/582a4ced73b15653c074606b').send({ url }) - should.fail() - } catch (err) { - const res = err.response - const { message, results } = res.body - - assert.equal(res.status, 404) - assert.equal(results, null) - assert.equal(message, 'Bot not found') - } - }) - }) - - describe('DELETE: delete a bot', () => { - it ('should be a 200', async () => { - let bot = await new Bot({ url }).save() - const res = await chai.request(baseUrl).del(`/bots/${bot._id}`).send() - const { message, results } = res.body - - assert.equal(res.status, 200) - assert.equal(results, null) - assert.equal(message, 'Bot successfully deleted') - }) - - it ('should be a 404 with no bots', async () => { - try { - await chai.request(baseUrl).del('/bots/582a4ced73b15653c074606b').send({ url }) - should.fail() - } catch (err) { - const res = err.response - const { message, results } = res.body - - assert.equal(res.status, 404) - assert.equal(results, null) - assert.equal(message, 'Bot not found') - } - }) - }) -}) diff --git a/test/controllers/Channels.controller.tests.js b/test/controllers/Channels.controller.tests.js deleted file mode 100644 index 3412a3b..0000000 --- a/test/controllers/Channels.controller.tests.js +++ /dev/null @@ -1,196 +0,0 @@ -import chai from 'chai' -import chaiHttp from 'chai-http' -import sinon from 'sinon' - -import model from '../../src/models' -import Bot from '../../src/models/Bot.model' -import Channel from '../../src/models/Channel.model' -import KikService from '../../src/services/Kik.service' - -import config from '../../config' - -const assert = require('chai').assert -const expect = chai.expect -const should = chai.should() - -chai.use(chaiHttp) - -const url = 'https://bonjour.com' -const baseUrl = 'http://localhost:8080' -const channelPayload = { - type: 'slack', - isActivated: true, - slug: 'slack-test', - token: 'test-token', -} - -describe('Channel controller', () => { - let bot = {} - before(async () => bot = await Bot({ url }).save()) - after(async () => await Bot.remove({})) - - describe('POST: create a channel', () => { - afterEach(async () => Promise.all([Channel.remove({})])) - - it ('should be a 201', async () => { - const res = await chai.request(baseUrl).post(`/bots/${bot._id}/channels`) - .send(channelPayload) - const { message, results } = res.body - - assert.equal(res.status, 201) - assert.equal(results.type, channelPayload.type) - assert.equal(results.isActivated, channelPayload.isActivated) - assert.equal(results.slug, channelPayload.slug) - assert.equal(results.token, channelPayload.token) - assert.equal(message, 'Channel successfully created') - }) - - it ('should be a 404 with no bots', async () => { - try { - const newBot = await new Bot({ url }).save() - await Bot.remove({ _id: newBot._id }) - const res = await chai.request(baseUrl).post(`/bots/${newBot._id}/channels`) - .send(channelPayload) - should.fail() - } catch (err) { - const res = err.response - const { message, results } = res.body - - assert.equal(res.status, 404) - assert.equal(results, null) - assert.equal(message, 'Bot not found') - } - }) - - it ('should be a 409 with a slug already existing', async () => { - const payload = { type: 'slack', isActivated: true, slug: 'test', token: 'test-token' } - const channel = await new Channel({ ...payload, bot: bot._id }).save() - try { - await chai.request(baseUrl).post(`/bots/${bot._id}/channels`).send(payload) - should.fail() - } catch (err) { - const res = err.response - - assert.equal(res.status, 409) - assert.equal(res.body.message, 'Channel slug already exists') - } - }) -}) - - describe('GET: get a bot\'s channels', () => { - afterEach(async () => Channel.remove({})) - - it ('should be a 200 with channels', async () => { - await Promise.all([ - new Channel({ bot: bot._id, ...channelPayload }).save(), - new Channel({ bot: bot._id, ...channelPayload }).save(), - ]) - const res = await chai.request(baseUrl).get(`/bots/${bot._id}/channels`).send() - const { message, results } = res.body - - assert.equal(res.status, 200) - assert.equal(results.length, 2) - assert.equal(message, 'Channels successfully rendered') - }) - - it ('should be a 200 with no channels', async () => { - const res = await chai.request(baseUrl).get(`/bots/${bot._id}/channels`).send() - const { message, results } = res.body - - assert.equal(res.status, 200) - assert.equal(results.length, 0) - assert.equal(message, 'No channels') - }) - }) - - describe('GET: get a bot\'s channel', () => { - afterEach(async () => Channel.remove({})) - - it('should be a 200 with a channel', async () => { - const channel = await new Channel({ bot: bot._id, ...channelPayload }).save() - const res = await chai.request(baseUrl).get(`/bots/${bot._id}/channels/${channel.slug}`).send() - const { message, results } = res.body - - assert.equal(res.status, 200) - assert.equal(results.id, channel._id) - assert.equal(results.slug, channel.slug) - assert.equal(message, 'Channel successfully rendered') - }) - - it('should be a 404 with no channels', async () => { - try { - const channel = await new Channel({ bot: bot._id, ...channelPayload }).save() - await Channel.remove({}) - const res = await chai.request(baseUrl).get(`/bots/${bot._id}/channels/${channel.slug}`).send() - should.fail() - } catch (err) { - const res = err.response - const { message, results } = res.body - - assert.equal(res.status, 404) - assert.equal(results, null) - assert.equal(message, 'Channel not found') - } - }) - }) - - describe('PUT: update a channel', () => { - afterEach(async () => Channel.remove({})) - - it('should be a 200 with a channel', async () => { - const channel = await new Channel({ bot: bot._id, ...channelPayload }).save() - const res = await chai.request(baseUrl).put(`/bots/${bot._id}/channels/${channel.slug}`).send({ slug: 'updatedSlug' }) - const { message, results } = res.body - - assert.equal(res.status, 200) - assert.equal(results.slug, 'updatedSlug') - assert.equal(message, 'Channel successfully updated') - }) - - it('should be a 404 with no channels', async () => { - try { - const channel = await new Channel({ bot: bot._id, ...channelPayload }).save() - await Channel.remove({}) - const res = await chai.request(baseUrl).put(`/bots/${bot._id}/channels/${channel.slug}`).send() - should.fail() - } catch (err) { - const res = err.response - const { message, results } = res.body - - assert.equal(res.status, 404) - assert.equal(results, null) - assert.equal(message, 'Channel not found') - } - }) - }) - - describe('DELETE: delete a channel', () => { - it('should be a 200 with a channel', async () => { - const channel = await new Channel({ bot: bot._id, ...channelPayload }).save() - bot.channels.push(channel._id) - await bot.save() - const res = await chai.request(baseUrl).del(`/bots/${bot._id}/channels/${channel.slug}`).send({ slug: 'updatedSlug' }) - const { message, results } = res.body - - assert.equal(res.status, 200) - assert.equal(results, null) - assert.equal(message, 'Channel successfully deleted') - }) - - it('should be a 404 with no channels', async () => { - try { - const channel = await new Channel({ bot: bot._id, ...channelPayload }).save() - await Channel.remove({}) - const res = await chai.request(baseUrl).del(`/bots/${bot._id}/channels/${channel.slug}`).send() - should.fail() - } catch (err) { - const res = err.response - const { message, results } = res.body - - assert.equal(res.status, 404) - assert.equal(results, null) - assert.equal(message, 'Channel not found') - } - }) - }) -}) diff --git a/test/controllers/Conversations.controllers.tests.js b/test/controllers/Conversations.controllers.tests.js deleted file mode 100644 index 60c5e7a..0000000 --- a/test/controllers/Conversations.controllers.tests.js +++ /dev/null @@ -1,150 +0,0 @@ -import chai from 'chai' -import chaiHttp from 'chai-http' -import config from '../../config' - -import model from '../../src/models' -import Bot from '../../src/models/Bot.model' -import Channel from '../../src/models/Channel.model' -import Conversation from '../../src/models/Conversation.model' - -const expect = chai.expect -const assert = chai.assert - -chai.use(chaiHttp) - -const url = 'http://bonjour.com' -const baseUrl = 'http://localhost:8080' -const channelPayload = { - isActivated: true, - slug: 'test', - type: 'slack', - token: 'test', -} -const conversationPayload = { - isActive: true, - chatId: 'test', -} - -describe('Conversation controller', () => { - let bot = {} - let channel = {} - let conversation = {} - before(async () => { - bot = await new Bot({ url }).save() - channel = await new Channel({ bot: bot._id, ...channelPayload }).save() - conversation = await new Conversation({ bot: bot._id, channel: channel._id, ...conversationPayload }).save() - bot.conversations.push(conversation._id) - await bot.save() - }) - after(async () => await Promise.all([ - Bot.remove({}), - Channel.remove({}), - Conversation.remove({}), - ])) - - describe('GET: get a bot conversations', () => { - it('should be a 200 with conversations', async () => { - const res = await chai.request(baseUrl).get(`/bots/${bot._id}/conversations`).send() - const { message, results } = res.body - - assert.equal(res.status, 200) - assert.equal(results.length, 1) - assert.equal(message, 'Conversations successfully rendered') - }) - - it('should be a 200 with no conversations', async () => { - const bot = await new Bot({ url }).save() - const res = await chai.request(baseUrl).get(`/bots/${bot._id}/conversations`).send() - const { message, results } = res.body - - assert.equal(res.status, 200) - assert.equal(results.length, 0) - assert.equal(message, 'No conversations') - }) - - it('should be a 404 with no bot', async () => { - try { - const bot = await new Bot({ url }).save() - await Bot.remove({ _id: bot._id }) - const res = await chai.request(baseUrl).get(`/bots/${bot._id}/conversations`).send() - } catch (err) { - const res = err.response - const { message, results } = res.body - - assert.equal(res.status, 404) - assert.equal(results, null) - assert.equal(message, 'Bot not found') - } - }) - }) - - describe('GET: get a conversation', () => { - it('should be a 200 with a conversation', async () => { - const res = await chai.request(baseUrl).get(`/bots/${bot._id}/conversations/${conversation._id}`).send() - const { message, results } = res.body - - assert.equal(res.status, 200) - assert.equal(results.id, conversation._id) - assert.equal(message, 'Conversation successfully rendered') - }) - - it('should be a 404 with no bot', async () => { - try { - const bot = await new Bot({ url }).save() - await Bot.remove({ _id: bot._id }) - const res = await chai.request(baseUrl).get(`/bots/${bot._id}/conversations/${conversation._id}`).send() - } catch (err) { - const res = err.response - const { message, results } = res.body - - assert.equal(res.status, 404) - assert.equal(results, null) - assert.equal(message, 'Bot not found') - } - }) - - it('should be a 404 with no bot', async () => { - try { - const conversation = await new Conversation({ bot: bot._id, channel: channel._id, ...conversationPayload }).save() - await Conversation.remove({ _id: conversation._id }) - const res = await chai.request(baseUrl).get(`/bots/${bot._id}/conversations/${conversation._id}`).send() - should.fail() - } catch (err) { - const res = err.response - const { message, results } = res.body - - assert.equal(res.status, 404) - assert.equal(results, null) - assert.equal(message, 'Conversation not found') - } - }) - }) - - describe('DELETE: delete a conversation', () => { - it('should be a 204 with a conversation', async () => { - const conversation = await new Conversation({ bot: bot._id, channel: channel._id, ...conversationPayload }).save() - const res = await chai.request(baseUrl).delete(`/bots/${bot._id}/conversations/${conversation._id}`).send() - const { message, results } = res.body - - assert.equal(res.status, 204) - assert.equal(results, null) - assert.equal(message, null) - }) - - it('should be a 404 with no conversation', async () => { - try { - const conversation = await new Conversation({ bot: bot._id, channel: channel._id, ...conversationPayload }).save() - await Conversation.remove({ _id: conversation._id }) - const res = await chai.request(baseUrl).delete(`/bots/${bot._id}/conversations/${conversation._id}`).send() - should.fail() - } catch (err) { - const res = err.response - const { message, results } = res.body - - assert.equal(res.status, 404) - assert.equal(results, null) - assert.equal(message, 'Conversation not found') - } - }) - }) -}) diff --git a/test/controllers/Messages.controller.tests.js b/test/controllers/Messages.controller.tests.js deleted file mode 100644 index 375e1fb..0000000 --- a/test/controllers/Messages.controller.tests.js +++ /dev/null @@ -1,136 +0,0 @@ -import chai from 'chai' -import chaiHttp from 'chai-http' -import mongoose from 'mongoose' - -import Bot from '../../src/models/Bot.model' -import Conversation from '../../src/models/Conversation.model' -import Channel from '../../src/models/Channel.model' - -import MessagesController from '../../src/controllers/Messages.controller' - -import sinon from 'sinon' - -const assert = require('chai').assert -const expect = chai.expect -const should = chai.should() - -chai.use(chaiHttp) - -function clearDB () { - for (const i in mongoose.connection.collections) { - if (mongoose.connection.collections.hasOwnProperty(i)) { - mongoose.connection.collections[i].remove() - } - } -} - -const url = 'https://bonjour.com' -const baseUrl = 'http://localhost:8080' -const updatedUrl = 'https://aurevoir.com' - -describe('Messages controller', () => { - describe('postMessages', () => { - after(async () => clearDB()) - - it('should send messages to all bots conversation', async () => { - let bot = await new Bot({ url: 'url' }).save() - const channel1 = await new Channel({ - bot: bot, - slug: 'channel-1', - type: 'slack', - token: 'abcd', - isActivated: true, - }).save() - const convers1 = await new Conversation({ - channel: channel1, - bot: bot, - isActive: true, - chatId: '123', - }).save() - channel1.conversations = [convers1._id] - await channel1.save() - - const channel2 = await new Channel({ - bot: bot, - slug: 'channel-2', - type: 'slack', - token: 'abcd', - isActivated: true, - }).save() - const convers2 = await new Conversation({ - channel: channel2, - bot: bot, - isActive: true, - chatId: '1234', - }).save() - channel2.conversations = [convers2._id] - await channel2.save() - - bot.channels = [channel1, channel2] - bot.conversations = [convers1, convers2] - await bot.save() - - // With valid json parameter - let stub = sinon.stub(MessagesController, 'postToConversation', () => { true }) - let res = await chai.request(baseUrl).post(`/bots/${bot._id.toString()}/messages`).send({ - messages: [{ - type: 'text', - content: 'Hello' - }], - }) - expect(res.status).to.equal(201) - expect(res.body.message).to.equal('Messages successfully posted') - expect(stub.callCount).to.equal(2) - stub.restore() - - // With valid string parameter - stub = sinon.stub(MessagesController, 'postToConversation', () => { true }) - res = await chai.request(baseUrl).post(`/bots/${bot._id.toString()}/messages`).send({ - messages: JSON.stringify([{ type: 'text', content: 'Hello' }]), - }) - expect(res.status).to.equal(201) - expect(res.body.message).to.equal('Messages successfully posted') - expect(stub.callCount).to.equal(2) - stub.restore() - - // With invalid string parameter - stub = sinon.stub(MessagesController, 'postToConversation', () => { throw new Error('error') }) - try { - res = await chai.request(baseUrl).post(`/bots/${bot._id.toString()}/messages`).send({ - messages: '[,{ "type": "text",, "content": "Hello" }]]' - }) - should.fail() - } catch (err) { - res = err.response - const { message, results } = res.body - - expect(res.status).to.equal(400) - expect(res.body.message).to.equal("Invalid 'messages' parameter") - expect(stub.callCount).to.equal(0) - } finally { - stub.restore() - } - - // With error in postToConversation - stub = sinon.stub(MessagesController, 'postToConversation', () => { throw new Error('error') }) - try { - res = await chai.request(baseUrl).post(`/bots/${bot._id.toString()}/messages`).send({ - messages: [{ - type: 'text', - content: 'Hello' - }] - }) - should.fail() - } catch (err) { - res = err.response - const { message, results } = res.body - - expect(res.status).to.equal(500) - expect(res.body.message).to.equal('Error while posting message') - expect(stub.callCount).to.equal(1) - } finally { - stub.restore() - } - }) - }) -}) diff --git a/test/controllers/Participants.controller.tests.js b/test/controllers/Participants.controller.tests.js deleted file mode 100644 index 3350346..0000000 --- a/test/controllers/Participants.controller.tests.js +++ /dev/null @@ -1,463 +0,0 @@ -import mongoose from 'mongoose' -import chai from 'chai' -import chaiHttp from 'chai-http' - -import Bot from '../../src/models/Bot.model.js' -import Channel from '../../src/models/Channel.model.js' -import Conversation from '../../src/models/Conversation.model.js' -import Participant from '../../src/models/Participant.model.js' - -chai.use(chaiHttp) - -const expect = chai.expect - -let bot = null -let channel = null -let conversation1 = null -let conversation2 = null -let participant1 = null -let participant2 = null -let participant3 = null -let participant4 = null - -function clearDB () { - for (const i in mongoose.connection.collections) { - if (mongoose.connection.collections.hasOwnProperty(i)) { - mongoose.connection.collections[i].remove() - } - } -} - -describe('Participant controller', () => { - describe('should list bot participants', () => { - describe('with no participant', () => { - before(done => { - bot = new Bot() - channel = new Channel() - conversation1 = new Conversation() - - bot.url = 'http://fallback.fr' - - channel.bot = bot._id - channel.slug = 'kik-1' - channel.type = 'kik' - channel.isActivated = true - - bot.channels.push(channel._id) - - conversation1.channel = channel._id - conversation1.bot = bot._id - conversation1.isActive = true - conversation1.chatId = 'myChatId' - - bot.conversations.push(conversation1._id) - - conversation1.save(() => { - channel.save(() => { - bot.save(() => { - done() - }) - }) - }) - }) - - after(done => { - bot = null - channel = null - conversation1 = null - clearDB() - done() - }) - - it('should be a 400 with a not valid bot_id', done => { - chai.request('http://localhost:8080') - .get(`/bots/1/participants`) - .send() - .end((err, res) => { - chai.should(err).exist - chai.expect(res.status).to.equal(400) - chai.expect(res.body.results).to.equal(null) - chai.expect(res.body.message).to.equal('Parameter bot_id is invalid') - done() - }) - }) - - it('should be a 200', done => { - chai.request('http://localhost:8080') - .get(`/bots/${bot._id}/participants`) - .send() - .end((err, res) => { - chai.should(err).not.exist - chai.expect(res.status).to.equal(200) - chai.expect(res.body.results).to.be.an('array') - chai.expect(res.body.results.length).to.equal(0) - chai.expect(res.body.message).to.equal('No participants') - done() - }) - }) - }) - - describe('with many conversations and many participants', () => { - before(done => { - bot = new Bot() - channel = new Channel() - conversation1 = new Conversation() - conversation2 = new Conversation() - participant1 = new Participant() - participant2 = new Participant() - participant3 = new Participant() - participant4 = new Participant() - - bot.url = 'http://fallback.fr' - - channel.bot = bot._id - channel.slug = 'kik-1' - channel.type = 'kik' - channel.isActivated = true - - bot.channels.push(channel._id) - - conversation1.channel = channel._id - conversation1.bot = bot._id - conversation1.isActive = true - conversation1.chatId = 'myChatId' - - bot.conversations.push(conversation1._id) - - conversation2.channel = channel._id - conversation2.bot = bot._id - conversation2.isActive = true - conversation2.chatId = 'myChatId2' - - bot.conversations.push(conversation2._id) - - participant1.isBot = true - - conversation1.participants.push(participant1._id) - - participant2.isBot = false - - - conversation1.participants.push(participant2._id) - - participant3.isBot = true - - conversation2.participants.push(participant3._id) - - participant4.isBot = false - - - conversation2.participants.push(participant4._id) - - participant1.save(() => { - participant2.save(() => { - participant3.save(() => { - participant4.save(() => { - conversation1.save(() => { - conversation2.save(() => { - channel.save(() => { - bot.save(() => { - done() - }) - }) - }) - }) - }) - }) - }) - }) - }) - - after(done => { - bot = null - channel = null - conversation1 = null - conversation2 = null - participant1 = null - participant2 = null - participant3 = null - participant4 = null - clearDB() - done() - }) - - it('should be a 400 with a not valid bot_id', done => { - chai.request('http://localhost:8080') - .get(`/bots/1/participants`) - .send() - .end((err, res) => { - chai.should(err).exist - chai.expect(res.status).to.equal(400) - chai.expect(res.body.results).to.equal(null) - chai.expect(res.body.message).to.equal('Parameter bot_id is invalid') - done() - }) - }) - - it('should be a 200', done => { - chai.request('http://localhost:8080') - .get(`/bots/${bot._id}/participants`) - .send() - .end((err, res) => { - chai.should(err).not.exist - chai.expect(res.status).to.equal(200) - chai.expect(res.body.results).to.be.an('array') - chai.expect(res.body.results.length).to.equal(4) - chai.expect(res.body.results[0]).to.be.an('object') - chai.expect(res.body.results[0].id.toString()).to.equal(participant1._id.toString()) - chai.expect(res.body.results[0].isBot).to.equal(participant1.isBot) - chai.expect(res.body.results[1]).to.be.an('object') - chai.expect(res.body.results[1].id.toString()).to.equal(participant2._id.toString()) - chai.expect(res.body.results[1].isBot).to.equal(participant2.isBot) - chai.expect(res.body.results[2]).to.be.an('object') - chai.expect(res.body.results[2].id.toString()).to.equal(participant3._id.toString()) - chai.expect(res.body.results[2].isBot).to.equal(participant3.isBot) - chai.expect(res.body.results[3]).to.be.an('object') - chai.expect(res.body.results[3].id.toString()).to.equal(participant4._id.toString()) - chai.expect(res.body.results[3].isBot).to.equal(participant4.isBot) - chai.expect(res.body.message).to.equal('Participants successfully rendered') - done() - }) - }) - }) - }) - - describe('should index bot participants', () => { - describe('with no participant', () => { - before(done => { - bot = new Bot() - channel = new Channel() - conversation1 = new Conversation() - - bot.url = 'http://fallback.fr' - - channel.bot = bot._id - channel.slug = 'kik-1' - channel.type = 'kik' - channel.isActivated = true - - bot.channels.push(channel._id) - - conversation1.channel = channel._id - conversation1.bot = bot._id - conversation1.isActive = true - conversation1.chatId = 'myChatId' - - bot.conversations.push(conversation1._id) - - conversation1.save(() => { - channel.save(() => { - bot.save(() => { - done() - }) - }) - }) - }) - - after(done => { - bot = null - channel = null - conversation1 = null - clearDB() - done() - }) - - it('should be a 400 with a not valid bot_id', done => { - chai.request('http://localhost:8080') - .get(`/bots/1/participants/1`) - .send() - .end((err, res) => { - chai.should(err).exist - chai.expect(res.status).to.equal(400) - chai.expect(res.body.results).to.equal(null) - chai.expect(res.body.message).to.equal('Parameter bot_id is invalid') - done() - }) - }) - - it('should be a 400 with a not valid participant_id', done => { - chai.request('http://localhost:8080') - .get(`/bots/${bot._id}/participants/1`) - .send() - .end((err, res) => { - chai.should(err).exist - chai.expect(res.status).to.equal(400) - chai.expect(res.body.results).to.equal(null) - chai.expect(res.body.message).to.equal('Parameter participant_id is invalid') - done() - }) - }) - - it('should be a 404 with a not valid participant_id', done => { - chai.request('http://localhost:8080') - .get(`/bots/${bot._id}/participants/507f1f77bcf86cd799439011`) - .send() - .end((err, res) => { - chai.should(err).exist - chai.expect(res.status).to.equal(404) - chai.expect(res.body.results).to.equal(null) - chai.expect(res.body.message).to.equal('Participant not found') - done() - }) - }) - }) - - describe('with many conversations and many participants', () => { - before(done => { - bot = new Bot() - channel = new Channel() - conversation1 = new Conversation() - conversation2 = new Conversation() - participant1 = new Participant() - participant2 = new Participant() - participant3 = new Participant() - participant4 = new Participant() - - bot.url = 'http://fallback.fr' - - channel.bot = bot._id - channel.slug = 'kik-1' - channel.type = 'kik' - channel.isActivated = true - - bot.channels.push(channel._id) - - conversation1.channel = channel._id - conversation1.bot = bot._id - conversation1.isActive = true - conversation1.chatId = 'myChatId' - - bot.conversations.push(conversation1._id) - - conversation2.channel = channel._id - conversation2.bot = bot._id - conversation2.isActive = true - conversation2.chatId = 'myChatId2' - - bot.conversations.push(conversation2._id) - - participant1.isBot = true - - conversation1.participants.push(participant1._id) - - participant2.isBot = false - - - conversation1.participants.push(participant2._id) - - participant3.isBot = true - - conversation2.participants.push(participant3._id) - - participant4.isBot = false - - - conversation2.participants.push(participant4._id) - - participant1.save(() => { - participant2.save(() => { - participant3.save(() => { - participant4.save(() => { - conversation1.save(() => { - conversation2.save(() => { - channel.save(() => { - bot.save(() => { - done() - }) - }) - }) - }) - }) - }) - }) - }) - }) - - after(done => { - bot = null - channel = null - conversation1 = null - conversation2 = null - participant1 = null - participant2 = null - participant3 = null - participant4 = null - clearDB() - done() - }) - - it('should be a 404 with a not found participant_id', done => { - chai.request('http://localhost:8080') - .get(`/bots/${bot._id}/participants/507f191e810c19729de860ea`) - .send() - .end((err, res) => { - chai.should(err).exist - chai.expect(res.status).to.equal(404) - chai.expect(res.body.results).to.equal(null) - chai.expect(res.body.message).to.equal('Participant not found') - done() - }) - }) - - it('should be a 200', done => { - chai.request('http://localhost:8080') - .get(`/bots/${bot._id}/participants/${participant1._id}`) - .send() - .end((err, res) => { - chai.should(err).not.exist - chai.expect(res.status).to.equal(200) - chai.expect(res.body.results).to.be.an('object') - chai.expect(res.body.results.id.toString()).to.equal(participant1._id.toString()) - chai.expect(res.body.results.isBot).to.equal(participant1.isBot) - chai.expect(res.body.message).to.equal('Participant successfully rendered') - done() - }) - }) - - it('should be a 200', done => { - chai.request('http://localhost:8080') - .get(`/bots/${bot._id}/participants/${participant2._id}`) - .send() - .end((err, res) => { - chai.should(err).not.exist - chai.expect(res.status).to.equal(200) - chai.expect(res.body.results).to.be.an('object') - chai.expect(res.body.results.id.toString()).to.equal(participant2._id.toString()) - chai.expect(res.body.results.isBot).to.equal(participant2.isBot) - chai.expect(res.body.message).to.equal('Participant successfully rendered') - done() - }) - }) - - it('should be a 200', done => { - chai.request('http://localhost:8080') - .get(`/bots/${bot._id}/participants/${participant3._id}`) - .send() - .end((err, res) => { - chai.should(err).not.exist - chai.expect(res.status).to.equal(200) - chai.expect(res.body.results).to.be.an('object') - chai.expect(res.body.results.id.toString()).to.equal(participant3._id.toString()) - chai.expect(res.body.results.isBot).to.equal(participant3.isBot) - chai.expect(res.body.message).to.equal('Participant successfully rendered') - done() - }) - }) - - it('should be a 200', done => { - chai.request('http://localhost:8080') - .get(`/bots/${bot._id}/participants/${participant4._id}`) - .send() - .end((err, res) => { - chai.should(err).not.exist - chai.expect(res.status).to.equal(200) - chai.expect(res.body.results).to.be.an('object') - chai.expect(res.body.results.id.toString()).to.equal(participant4._id.toString()) - chai.expect(res.body.results.isBot).to.equal(participant4.isBot) - chai.expect(res.body.message).to.equal('Participant successfully rendered') - done() - }) - }) - }) - }) -}) diff --git a/test/controllers/application.js b/test/controllers/application.js new file mode 100644 index 0000000..c67bbe6 --- /dev/null +++ b/test/controllers/application.js @@ -0,0 +1,25 @@ +import expect from 'expect.js' +import superagent from 'superagent' +import superagentPromise from 'superagent-promise' + +import '../util/start_application' + +const agent = superagentPromise(superagent, Promise) + +describe('App Controller', () => { + + describe('GET /', () => { + it('should be 200', async () => { + const res = await agent.get(`${process.env.ROUTETEST}/v1`) + expect(res.status).to.be(200) + }) + }) + + describe('POST /', () => { + it('should be 200', async () => { + const res = await agent.post(`${process.env.ROUTETEST}/v1`) + expect(res.status).to.be(200) + }) + }) + +}) diff --git a/test/controllers/channels.js b/test/controllers/channels.js new file mode 100644 index 0000000..64ae6ab --- /dev/null +++ b/test/controllers/channels.js @@ -0,0 +1,334 @@ +import expect from 'expect.js' +import should from 'should' +import superagent from 'superagent' +import superagentPromise from 'superagent-promise' + +import config from '../../config' +import channelFactory from '../factories/channel' +import connectorFactory from '../factories/connector' +import { Connector, Channel } from '../../src/models' +import '../util/start_application' +const agent = superagentPromise(superagent, Promise) + +let connector = null +let channel = null + +describe('Channels Controller', () => { + after(async () => { + await Connector.remove() + await Channel.remove() + }) + + // Channel Creation + + describe('Create', () => { + afterEach(async () => { + await Promise.all([ + Connector.remove(), + Channel.remove(), + ]) + }) + + it('should 200 with valid parameters', async () => { + connector = await connectorFactory.build() + const payload = { type: 'recastwebchat', slug: 'my-awesome-channel' } + const res = await agent.post(`${process.env.ROUTETEST}/v1/connectors/${connector._id}/channels`) + .send(payload) + const { results, message } = res.body + + expect(res.status).to.be(201) + expect(message).to.be('Channel successfully created') + expect(results.type).to.be(payload.type) + expect(results.slug).to.be(payload.slug) + }) + + it('should 400 with missing type', async () => { + try { + connector = await connectorFactory.build() + const payload = { slug: 'my-awesome-channel' } + await agent.post(`${process.env.ROUTETEST}/v1/connectors/${connector._id}/channels`) + .send(payload) + + should.fail() + } catch (err) { + const { message, results } = err.response.body + + expect(err.status).to.be(400) + expect(message).to.be('Parameter type is missing') + expect(results).to.be(null) + } + }) + + it('should 400 with invalid type', async () => { + try { + connector = await connectorFactory.build() + const payload = { slug: 'my-awesome-channel', type: 'yolo' } + await agent.post(`${process.env.ROUTETEST}/v1/connectors/${connector._id}/channels`) + .send(payload) + + should.fail() + } catch (err) { + const { message, results } = err.response.body + + expect(err.status).to.be(400) + expect(message).to.be('Parameter type is invalid') + expect(results).to.be(null) + } + }) + + it('should 400 with missing slug', async () => { + try { + connector = await connectorFactory.build() + const payload = { type: 'messenger' } + await agent.post(`${process.env.ROUTETEST}/v1/connectors/${connector._id}/channels`) + .send(payload) + + should.fail() + } catch (err) { + const { message, results } = err.response.body + + expect(err.status).to.be(400) + expect(message).to.be('Parameter slug is missing') + expect(results).to.be(null) + } + }) + + it('should 404 without connector', async () => { + try { + const payload = { type: 'recastwebchat', slug: 'yoloswag' } + await agent.post(`${process.env.ROUTETEST}/v1/connectors/d29dd4f8-aa8e-4224-81f9-c4da94db18b8/channels`) + .send(payload) + + should.fail() + } catch (err) { + const { message, results } = err.response.body + + expect(err.status).to.be(404) + expect(message).to.be('Connector not found') + expect(results).to.be(null) + } + }) + }) + + // Channel Update + + describe('Update', () => { + afterEach(async () => { + await Promise.all([ + Connector.remove(), + Channel.remove(), + ]) + }) + + it('should 200 with valid parameters', async () => { + connector = await connectorFactory.build() + channel = await channelFactory.build(connector) + const payload = { slug: 'my-awesome-channel-updated' } + const res = await agent.put(`${process.env.ROUTETEST}/v1/connectors/${connector._id}/channels/${channel.slug}`) + .send(payload) + const { results, message } = res.body + + expect(res.status).to.be(200) + expect(message).to.be('Channel successfully updated') + expect(results.slug).to.be(payload.slug) + expect(results.id).to.be(channel._id) + expect(results.type).to.be(channel.type) + }) + + it('should 404 without connector', async () => { + try { + const payload = { slug: 'updated-slug' } + await agent.put(`${process.env.ROUTETEST}/v1/connectors/d29dd4f8-aa8e-4224-81f9-c4da94db18b8/channels/lol`) + .send(payload) + + should.fail() + } catch (err) { + const { message, results } = err.response.body + + expect(err.status).to.be(404) + expect(message).to.be('Connector not found') + expect(results).to.be(null) + } + }) + + it('should 404 without channel', async () => { + try { + connector = await connectorFactory.build() + const payload = { slug: 'updated-slug' } + await agent.put(`${process.env.ROUTETEST}/v1/connectors/${connector._id}/channels/lol`) + .send(payload) + + should.fail() + } catch (err) { + const { message, results } = err.response.body + + expect(err.status).to.be(404) + expect(message).to.be('Channel not found') + expect(results).to.be(null) + } + }) + }) + + // Channel Delete + + describe('DELETE', () => { + afterEach(async () => { + await Promise.all([ + Connector.remove(), + Channel.remove(), + ]) + }) + + it('should 200 with a channel', async () => { + connector = await connectorFactory.build() + channel = await channelFactory.build(connector) + const res = await agent.del(`${process.env.ROUTETEST}/v1/connectors/${connector._id}/channels/${channel.slug}`) + .send() + const { results, message } = res.body + + expect(res.status).to.be(200) + expect(message).to.be('Channel successfully deleted') + expect(results).to.be(null) + }) + + it('should 404 without connector', async () => { + try { + await agent.del(`${process.env.ROUTETEST}/v1/connectors/d29dd4f8-aa8e-4224-81f9-c4da94db18b8/channels/lol`) + .send() + + should.fail() + } catch (err) { + const { message, results } = err.response.body + + expect(err.status).to.be(404) + expect(message).to.be('Connector not found') + expect(results).to.be(null) + } + }) + + it('should 404 without channel', async () => { + try { + connector = await connectorFactory.build() + await agent.del(`${process.env.ROUTETEST}/v1/connectors/${connector._id}/channels/lol`) + .send() + + should.fail() + } catch (err) { + const { message, results } = err.response.body + + expect(err.status).to.be(404) + expect(message).to.be('Channel not found') + expect(results).to.be(null) + } + }) + }) + + // Channel Show + + describe('SHOW', () => { + afterEach(async () => { + await Promise.all([ + Connector.remove(), + Channel.remove(), + ]) + }) + + it('should 200 with a channel', async () => { + connector = await connectorFactory.build() + channel = await channelFactory.build(connector) + const res = await agent.get(`${process.env.ROUTETEST}/v1/connectors/${connector._id}/channels/${channel.slug}`) + .send() + const { results, message } = res.body + + expect(res.status).to.be(200) + expect(message).to.be('Channel successfully rendered') + expect(results.type).to.be(channel.type) + expect(results.id).to.be(channel.id) + expect(results.slug).to.be(channel.slug) + }) + + it('should 404 without connector', async () => { + try { + await agent.get(`${process.env.ROUTETEST}/v1/connectors/d29dd4f8-aa8e-4224-81f9-c4da94db18b8/channels/lol`) + .send() + + should.fail() + } catch (err) { + const { message, results } = err.response.body + + expect(err.status).to.be(404) + expect(message).to.be('Connector not found') + expect(results).to.be(null) + } + }) + + it('should 404 without channel', async () => { + try { + connector = await connectorFactory.build() + await agent.get(`${process.env.ROUTETEST}/v1/connectors/${connector._id}/channels/lol`) + .send() + + should.fail() + } catch (err) { + const { message, results } = err.response.body + + expect(err.status).to.be(404) + expect(message).to.be('Channel not found') + expect(results).to.be(null) + } + }) + }) + + // Channel Index + + describe('INDEX', () => { + afterEach(async () => { + Promise.all([ + Connector.remove(), + Channel.remove(), + ]) + }) + + it('should 200 with channels', async () => { + connector = await connectorFactory.build() + channel = await channelFactory.build(connector) + const res = await agent.get(`${process.env.ROUTETEST}/v1/connectors/${connector._id}/channels`) + .send() + const { results, message } = res.body + + expect(res.status).to.be(200) + expect(message).to.be('Channels successfully rendered') + expect(results.length).to.equal(1) + expect(results[0].id).to.equal(channel._id) + expect(results[0].slug).to.equal(channel.slug) + expect(results[0].type).to.equal(channel.type) + }) + + it('should 200 without channels', async () => { + connector = await connectorFactory.build() + const res = await agent.get(`${process.env.ROUTETEST}/v1/connectors/${connector._id}/channels`) + .send() + const { results, message } = res.body + + expect(res.status).to.be(200) + expect(message).to.be('No channels') + expect(results.length).to.equal(0) + }) + + it('should 404 without connector', async () => { + try { + await agent.get(`${process.env.ROUTETEST}/v1/connectors/${connector._id}/channels`) + .send() + + should.fail() + } catch (err) { + const { message, results } = err.response.body + + expect(err.status).to.be(404) + expect(message).to.be('Connector not found') + expect(results).to.be(null) + } + }) + }) + +}) diff --git a/test/controllers/connectors.js b/test/controllers/connectors.js new file mode 100644 index 0000000..6315dec --- /dev/null +++ b/test/controllers/connectors.js @@ -0,0 +1,146 @@ +import expect from 'expect.js' +import should from 'should' +import superagent from 'superagent' +import superagentPromise from 'superagent-promise' +import '../util/start_application' +import { Connector } from '../../src/models' +import config from '../../config' +import connectorFactory from '../factories/Connector' + +let connector = null + +const agent = superagentPromise(superagent, Promise) + +describe('Connector Controller', () => { + beforeEach(async () => { + connector = await connectorFactory.build() + }) + + afterEach(async () => { + await Connector.remove() + }) + + it('should Create with valid parameters', async () => { + await Connector.remove() + const payload = { url: 'https://mynewconnector.com' } + const res = await agent.post(`${process.env.ROUTETEST}/v1/connectors`) + .send(payload) + const { results, message } = res.body + + expect(res.status).to.be(201) + expect(message).to.be('Connector successfully created') + expect(results.url).to.be('https://mynewconnector.com') + }) + + it('should not Create with missing url', async () => { + try { + await Connector.remove() + const payload = { } + await agent.post(`${process.env.ROUTETEST}/v1/connectors`) + .send(payload) + + should.fail() + } catch (err) { + const { message, results } = err.response.body + + expect(err.status).to.be(400) + expect(message).to.be('Parameter url is missing') + expect(results).to.be(null) + } + }) + + it('should not Create with invalid url', async () => { + try { + await Connector.remove() + const payload = { url: 'lol' } + await agent.post(`${process.env.ROUTETEST}/v1/connectors`) + .send(payload) + + should.fail() + } catch (err) { + const { message, results } = err.response.body + + expect(err.status).to.be(400) + expect(message).to.be('Parameter url is invalid') + expect(results).to.be(null) + } + }) + + it('should Update with valid parameters', async () => { + const payload = { url: 'https://myupdatedconnector.com' } + const res = await agent.put(`${process.env.ROUTETEST}/v1/connectors/${connector._id}`) + .send(payload) + const { results, message } = res.body + + expect(res.status).to.be(200) + expect(message).to.be('Connector successfully updated') + expect(results.url).to.be(payload.url) + }) + + it('should not Update with invalid url', async () => { + try { + const payload = { url: 'Invalidurl' } + await agent.put(`${process.env.ROUTETEST}/v1/connectors/${connector._id}`) + .send(payload) + + should.fail() + } catch (err) { + const { message, results } = err.response.body + + expect(err.status).to.be(400) + expect(message).to.be('Parameter url is invalid') + expect(results).to.be(null) + } + }) + + it('should Show a valid bot', async () => { + const res = await agent.get(`${process.env.ROUTETEST}/v1/connectors/${connector._id}`) + const { results, message } = res.body + + expect(res.status).to.be(200) + expect(message).to.be('Connector successfully found') + expect(results.id).to.be(connector._id) + expect(results.url).to.be(connector.url) + expect(results.isTyping).to.be(connector.isTyping) + }) + + it('should not Show an invalid bot', async () => { + try { + await Connector.remove() + await agent.get(`${process.env.ROUTETEST}/v1/connectors/d29dd4f8-aa8e-4224-81f9-c4da94db18b8`) + + should.fail() + } catch (err) { + const { message, results } = err.response.body + + expect(err.status).to.be(404) + expect(message).to.be('Connector not found') + expect(results).to.be(null) + } + }) + + it('should Delete a valid connector', async () => { + const res = await agent.del(`${process.env.ROUTETEST}/v1/connectors/${connector._id}`) + const { results, message } = res.body + + expect(res.status).to.be(200) + expect(message).to.be('Connector successfully deleted') + expect(results).to.be(null) + }) + + it('should not delete an invalid bot', async () => { + try { + await Connector.remove() + await agent.del(`${process.env.ROUTETEST}/v1/connectors/d29dd4f8-aa8e-4224-81f9-c4da94db18b8`) + + should.fail() + } catch (err) { + const { message, results } = err.response.body + + expect(err.status).to.be(404) + expect(message).to.be('Connector not found') + expect(results).to.be(null) + } + }) + +}) diff --git a/test/controllers/conversations.js b/test/controllers/conversations.js new file mode 100644 index 0000000..45a4b23 --- /dev/null +++ b/test/controllers/conversations.js @@ -0,0 +1,197 @@ +import expect from 'expect.js' +import should from 'should' +import superagent from 'superagent' +import superagentPromise from 'superagent-promise' +import '../util/start_application' +import config from '../../config/test' +import channelFacto from '../factories/Channel' +import connectorFacto from '../factories/Connector' +import conversationFacto from '../factories/Conversation' +import { Connector, Channel, Conversation } from '../../src/models' + +const agent = superagentPromise(superagent, Promise) + +let connector = null +let channel = null +let conversation = null + +describe('Conversations controller', () => { + after(async () => { + await Promise.all([ + Conversation.remove(), + Connector.remove(), + Channel.remove(), + ]) + }) + + // Index conversations + + describe('GET', () => { + afterEach(async () => { + await Promise.all([ + Conversation.remove(), + Connector.remove(), + Channel.remove(), + ]) + }) + + it('should 200 with conversations', async () => { + connector = await connectorFacto.build() + channel = await channelFacto.build(connector) + conversation = await conversationFacto.build(connector, channel) + + const res = await agent.get(`${process.env.ROUTETEST}/v1/connectors/${connector._id}/conversations`) + .send() + const { results, message } = res.body + + expect(res.status).to.be(200) + expect(message).to.be('Conversations successfully found') + expect(results.length).to.be(1) + expect(results[0].id).to.equal(conversation._id) + }) + + it('should 200 without conversations', async () => { + connector = await connectorFacto.build() + channel = await channelFacto.build(connector) + + const res = await agent.get(`${process.env.ROUTETEST}/v1/connectors/${connector._id}/conversations`) + .send() + const { results, message } = res.body + + expect(res.status).to.be(200) + expect(message).to.be('No conversations') + expect(results.length).to.be(0) + }) + + it('should 404 without connector', async () => { + try { + await agent.get(`${process.env.ROUTETEST}/v1/connectors/dec04e80-424d-4a9f-bb80-6d40511e246b/conversations`) + .send() + + should.fail() + } catch (err) { + const { message, results } = err.response.body + + expect(err.status).to.be(404) + expect(message).to.be('Connector not found') + expect(results).to.be(null) + } + }) + }) + + // Show a conversation + + describe('GET', () => { + afterEach(async () => { + await Promise.all([ + Conversation.remove(), + Connector.remove(), + Channel.remove(), + ]) + }) + + it('should 200 with a conversation', async () => { + connector = await connectorFacto.build() + channel = await channelFacto.build(connector) + conversation = await conversationFacto.build(connector, channel) + + const res = await agent.get(`${process.env.ROUTETEST}/v1/connectors/${connector._id}/conversations/${conversation._id}`) + .send() + const { results, message } = res.body + + expect(res.status).to.be(200) + expect(message).to.be('Conversation successfully found') + expect(results.id).to.equal(conversation._id) + }) + + it('should 404 without a conversation', async () => { + try { + connector = await connectorFacto.build() + await agent.get(`${process.env.ROUTETEST}/v1/connectors/${connector._id}/conversations/invalid_id`) + .send() + + should.fail() + } catch (err) { + const { message, results } = err.response.body + + expect(err.status).to.be(404) + expect(message).to.be('Conversation not found') + expect(results).to.be(null) + } + }) + + it('should 404 without connector', async () => { + try { + await agent.get(`${process.env.ROUTETEST}/v1/connectors/dec04e80-424d-4a9f-bb80-6d40511e246/conversations/invalid_id`) + .send() + + should.fail() + } catch (err) { + const { message, results } = err.response.body + + expect(err.status).to.be(404) + expect(message).to.be('Connector not found') + expect(results).to.be(null) + } + }) + }) + + // Delete a conversation + + describe('DELETE', () => { + afterEach(async () => { + await Promise.all([ + Connector.remove(), + Channel.remove(), + Conversation.remove(), + ]) + }) + + it('should 200 with a conversation', async () => { + connector = await connectorFacto.build() + channel = await channelFacto.build(connector) + conversation = await conversationFacto.build(connector, channel) + + const res = await agent.del(`${process.env.ROUTETEST}/v1/connectors/${connector._id}/conversations/${conversation._id}`) + .send() + const { results, message } = res.body + + expect(res.status).to.be(200) + expect(message).to.be('Conversation successfully deleted') + expect(results).to.equal(null) + }) + + it('should 404 without a conversation', async () => { + try { + connector = await connectorFacto.build() + await agent.del(`${process.env.ROUTETEST}/v1/connectors/${connector._id}/conversations/invalid_id`) + .send() + + should.fail() + } catch (err) { + const { message, results } = err.response.body + + expect(err.status).to.be(404) + expect(message).to.be('Conversation not found') + expect(results).to.be(null) + } + }) + + it('should 404 without a connector', async () => { + try { + await agent.del(`${process.env.ROUTETEST}/v1/connectors/dec04e80-424d-4a9f-bb80-6d40511e246/conversations/invalid_id`) + .send() + + should.fail() + } catch (err) { + const { message, results } = err.response.body + + expect(err.status).to.be(404) + expect(message).to.be('Connector not found') + expect(results).to.be(null) + } + }) + }) + +}) + diff --git a/test/controllers/messages.js b/test/controllers/messages.js new file mode 100644 index 0000000..e4d0e08 --- /dev/null +++ b/test/controllers/messages.js @@ -0,0 +1,8 @@ +import expect from 'expect.js' +import should from 'should' +import agent from 'superagent' + +describe('Messages Controller Testing', () => { + // DO NOT IMPLEMENT ANY TESTS FOR THIS CONTROLLER + // WITHOUT MOCKING EXTERNAL SERVICES +}) diff --git a/test/controllers/oauth.js b/test/controllers/oauth.js new file mode 100644 index 0000000..5773e00 --- /dev/null +++ b/test/controllers/oauth.js @@ -0,0 +1,8 @@ +import expect from 'expect.js' +import should from 'should' +import agent from 'superagent' + +describe('Oauth Controller Testing', () => { + // DO NOT IMPLEMENT ANY TESTS FOR THIS CONTROLLER + // WITHOUT MOCKING EXTERNAL SERVICES +}) diff --git a/test/controllers/participants.js b/test/controllers/participants.js new file mode 100644 index 0000000..e7bee54 --- /dev/null +++ b/test/controllers/participants.js @@ -0,0 +1,199 @@ +import expect from 'expect.js' +import agent from 'superagent' +import should from 'should' + +import config from '../../config/test' +import '../util/start_application' +import Connector from '../../src/models/connector' +import Conversation from '../../src/models/conversation' +import Participant from '../../src/models/participant' + +let connector = null +let conversation = null +let participant = null +let participant2 = null + +let connector2 = null +let conversation2 = null +let participant3 = null + +describe('Participants Controller Testing', () => { + describe('Get all connector\'s participants', () => { + describe('GET /participants', () => { + before(done => { + connector = new Connector() + connector2 = new Connector() + conversation = new Conversation() + conversation2 = new Conversation() + participant = new Participant() + participant2 = new Participant() + participant3 = new Participant() + + connector.url = 'http://myurl.com' + + conversation.channel = 'directline' + conversation.connector = connector._id + conversation.chatId = 'mychatid' + + participant.senderId = 'part1_senderId', + participant.data = { name: 'part1' }, + participant.isBot = true, + participant.conversation = conversation._id + + participant2.senderId = 'part2_senderId', + participant2.data = { name: 'part2' }, + participant2.isBot = false, + participant2.conversation = conversation._id + + connector2.url = 'http://myurl2.com' + + conversation2.channel = 'directline' + conversation2.connector = connector2._id + conversation2.chatId = 'mychatid2' + + participant3.senderId = 'part3_senderId', + participant3.data = { name: 'part3' }, + participant3.isBot = true, + participant3.conversation = conversation2._id + + connector.save(() => { + connector2.save(() => { + conversation.save(() => { + conversation2.save(() => { + participant.save(() => { + participant2.save(() => { + participant3.save(() => { + done() + }) + }) + }) + }) + }) + }) + }) + }) + + after(done => { + connector = null + connector2 = null + conversation = null + conversation2 = null + participant = null + participant2 = null + participant3 = null + Connector.remove(() => { + Conversation.remove(() => { + Participant.remove(() => { + done() + }) + }) + }) + }) + + it('should work with developer_token', (done) => { + agent.get(`${process.env.ROUTETEST}/v1/connectors/${connector2._id}/participants`) + .send() + .end((err, result) => { + should.not.exist(err) + expect(result.status).to.be(200) + expect(result.body.results).not.to.be(null) + expect(result.body.results.length).to.be(1) + expect(result.body.results[0].id).to.be(participant3._id) + expect(result.body.results[0].isBot).to.be(participant3.isBot) + expect(result.body.results[0].senderId).to.be(participant3.senderId) + + done() + }) + }) + }) + }) + + describe('Get a connector\'s participant', () => { + describe('GET /participants/:participant_id', () => { + before(done => { + connector = new Connector() + connector2 = new Connector() + conversation = new Conversation() + conversation2 = new Conversation() + participant = new Participant() + participant2 = new Participant() + participant3 = new Participant() + + connector.url = 'http://myurl.com' + + conversation.channel = 'directline' + conversation.connector = connector._id + conversation.chatId = 'mychatid' + + participant.senderId = 'part1_senderId' + participant.data = { name: 'part1' } + participant.isBot = true + participant.conversation = conversation._id + + participant2.senderId = 'part2_senderId' + participant2.data = { name: 'part2' } + participant2.isBot = false + participant2.conversation = conversation2._id + + connector2.url = 'http://myurl2.com' + + conversation2.channel = 'directline' + conversation2.connector = connector2._id + conversation2.chatId = 'mychatid2' + + participant3.senderId = 'part3_senderId' + participant3.data = { name: 'part3' } + participant3.isBot = true + participant3.conversation = conversation2._id + + connector.save(() => { + connector2.save(() => { + conversation.save(() => { + conversation2.save(() => { + participant.save(() => { + participant2.save(() => { + participant3.save(() => { + done() + }) + }) + }) + }) + }) + }) + }) + }) + + after(done => { + connector = null + connector2 = null + conversation = null + conversation2 = null + participant = null + participant2 = null + participant3 = null + Connector.remove(() => { + Conversation.remove(() => { + Participant.remove(() => { + done() + }) + }) + }) + }) + + it('should work with developer_token', (done) => { + agent.get(`${process.env.ROUTETEST}/v1/connectors/${connector2._id}/participants/${participant3._id}`) + .send() + .end((err, result) => { + should.not.exist(err) + expect(result.status).to.be(200) + expect(result.body.results).not.to.be(null) + expect(result.body.results.id).to.be(participant3._id) + expect(result.body.results.isBot).to.be(participant3.isBot) + expect(result.body.results.senderId).to.be(participant3.senderId) + + done() + }) + }) + }) + }) +}) diff --git a/test/controllers/persistent_menus.js b/test/controllers/persistent_menus.js new file mode 100644 index 0000000..03a1dfd --- /dev/null +++ b/test/controllers/persistent_menus.js @@ -0,0 +1,264 @@ +import expect from 'expect.js' +import should from 'should' +import superagent from 'superagent' +import superagentPromise from 'superagent-promise' +import '../util/start_application' +import connectorFacto from '../factories/Connector' +import persistentmenuFacto from '../factories/Persistent_menu' +import { Connector, Channel, PersistentMenu } from '../../src/models' + +const agent = superagentPromise(superagent, Promise) + +let connector = null + +describe('Persistent menus controller', () => { + + describe('Create', () => { + afterEach(async () => { + await Promise.all([ + Connector.remove(), + PersistentMenu.remove(), + ]) + }) + + it('should 200 with valid parameters', async () => { + connector = await connectorFacto.build() + + const menu = { + menu: { + call_to_actions: [ + { + type: 'web_url', + payload: 'http://google.com', + title: 'Lien vers Google en allemand', + }], + }, + language: 'de', + } + const res = await agent.post(`${process.env.ROUTETEST}/v1/connectors/${connector._id}/persistentmenus`) + .send(menu) + const { results, message } = res.body + + expect(res.status).to.be(201) + expect(message).to.be('PersistentMenu successfully created') + expect(JSON.stringify(results.menu)).to.be(JSON.stringify(menu.menu)) + expect(results.language).to.be(menu.menu.language) + }) + + it('should 409 if menu already exists for a language', async () => { + connector = await connectorFacto.build() + + const menu = { + menu: {}, + language: 'de', + } + await persistentmenuFacto.build(connector, { locale: 'de' }) + try { + await agent.post(`${process.env.ROUTETEST}/v1/connectors/${connector._id}/persistentmenus`) + .send(menu) + + should.fail() + } catch (err) { + const { message } = err.response.body + expect(err.status).to.be(409) + expect(message).to.be('A persistent menu already exists for this language') + } + }) + + it('should 404 with no connector', async () => { + try { + await agent.post(`${process.env.ROUTETEST}/v1/connectors/d29dd4f8-aa8e-4224-81f9-c4da94db18b8/persistentmenus`) + } catch (err) { + const { message } = err.response.body + expect(message).to.be('Connector not found') + expect(err.status).to.be(404) + } + }) + }) + + describe('Get', () => { + afterEach(async () => { + await Promise.all([ + Connector.remove(), + Channel.remove(), + PersistentMenu.remove(), + ]) + }) + it('should 200 with one menu', async () => { + connector = await connectorFacto.build() + const data = { + menu: { + some: 'menu', + }, + language: 'de', + } + await persistentmenuFacto.build(connector, { menu: data.menu, locale: data.language }) + const res = await agent.get(`${process.env.ROUTETEST}/v1/connectors/${connector._id}/persistentmenus/de`) + + const { results, message } = res.body + expect(message).to.be('PersistentMenu successfully rendered') + expect(JSON.stringify(results.menu)).to.be(JSON.stringify(data.menu)) + expect(results.locale).to.be(data.language) + }) + + it('should 200 with multiple menus', async () => { + connector = await connectorFacto.build() + await persistentmenuFacto.build(connector, { locale: 'en' }) + await persistentmenuFacto.build(connector, { locale: 'fr' }) + await persistentmenuFacto.build(connector, { locale: 'de' }) + + const res = await agent.get(`${process.env.ROUTETEST}/v1/connectors/${connector._id}/persistentmenus`) + + const { results, message } = res.body + expect(results.length).to.be(3) + expect(message).to.be('Persistent menus successfully rendered') + }) + + it('should 404 with no menu for this language', async () => { + connector = await connectorFacto.build() + try { + await agent.get(`${process.env.ROUTETEST}/v1/connectors/${connector._id}/persistentmenus/de`) + } catch (err) { + const { message } = err.response.body + expect(message).to.be('PersistentMenu not found') + expect(err.status).to.be(404) + } + }) + + it('should 404 with no connector', async () => { + try { + await agent.get(`${process.env.ROUTETEST}/v1/connectors/d29dd4f8-aa8e-4224-81f9-c4da94db18b8/persistentmenus/de`) + } catch (err) { + const { message } = err.response.body + expect(message).to.be('Connector not found') + expect(err.status).to.be(404) + } + }) + + }) + + describe('Update', () => { + afterEach(async () => { + await Promise.all([ + Connector.remove(), + PersistentMenu.remove(), + ]) + }) + + it('should 200 with valid parameters', async () => { + connector = await connectorFacto.build() + await persistentmenuFacto.build(connector, { locale: 'en' }) + + const data = { + menu: { + awesome: 'menu', + }, + } + const res = await agent.put(`${process.env.ROUTETEST}/v1/connectors/${connector._id}/persistentmenus/en`) + .send(data) + + const { results, message } = res.body + expect(res.status).to.be(200) + expect(message).to.be('PersistentMenu successfully updated') + expect(JSON.stringify(results.menu)).to.be(JSON.stringify(data.menu)) + }) + + it('should 404 with no connector', async () => { + try { + const data = { menu: {} } + await agent.put(`${process.env.ROUTETEST}/v1/connectors/d29dd4f8-aa8e-4224-81f9-c4da94db18b8/persistentmenus/de`) + .send(data) + } catch (err) { + const { message } = err.response.body + expect(message).to.be('Connector not found') + expect(err.status).to.be(404) + } + }) + }) + + describe('DeleteAll', () => { + afterEach(async () => { + await Promise.all([ + Connector.remove(), + PersistentMenu.remove(), + ]) + }) + + it('should 200', async () => { + connector = await connectorFacto.build() + + await persistentmenuFacto.build(connector, { locale: 'de' }) + await persistentmenuFacto.build(connector, { locale: 'en' }) + + const res = await agent.del(`${process.env.ROUTETEST}/v1/connectors/${connector._id}/persistentmenus`) + + const { results, message } = res.body + expect(res.status).to.be(200) + expect(message).to.be('Persistent Menu successfully deleted') + expect(results).to.be(null) + }) + + it('should 404 with no connector', async () => { + try { + const data = { menu: {} } + await agent.del(`${process.env.ROUTETEST}/v1/connectors/d29dd4f8-aa8e-4224-81f9-c4da94db18b8/persistentmenus`) + .send(data) + } catch (err) { + const { message } = err.response.body + expect(message).to.be('Connector not found') + expect(err.status).to.be(404) + } + }) + }) + + describe('Delete', () => { + afterEach(async () => { + await Promise.all([ + Connector.remove(), + PersistentMenu.remove(), + ]) + }) + + it('should 200', async () => { + connector = await connectorFacto.build() + + await persistentmenuFacto.build(connector, { locale: 'de' }) + + const res = await agent.del(`${process.env.ROUTETEST}/v1/connectors/${connector._id}/persistentmenus/de`) + + const { results, message } = res.body + expect(res.status).to.be(200) + expect(message).to.be('Persistent Menu successfully deleted') + expect(results).to.be(null) + }) + + it('should 404 with no connector', async () => { + try { + const data = { menu: {} } + + await persistentmenuFacto.build(connector, { locale: 'de' }) + + await agent.del(`${process.env.ROUTETEST}/v1/connectors/d29dd4f8-aa8e-4224-81f9-c4da94db18b8/persistentmenus/de`) + .send(data) + } catch (err) { + const { message } = err.response.body + expect(message).to.be('Connector not found') + expect(err.status).to.be(404) + } + }) + + it('should 404 with non-existing menu', async () => { + try { + const data = { menu: {} } + connector = await connectorFacto.build() + + await agent.del(`${process.env.ROUTETEST}/v1/connectors/${connector._id}/persistentmenus/de`) + .send(data) + } catch (err) { + const { message } = err.response.body + expect(message).to.be('PersistentMenu not found') + expect(err.status).to.be(404) + } + }) + }) +}) diff --git a/test/controllers/webhooks.js b/test/controllers/webhooks.js new file mode 100644 index 0000000..8624de5 --- /dev/null +++ b/test/controllers/webhooks.js @@ -0,0 +1,564 @@ +import expect from 'expect.js' +import should from 'should' +import superagent from 'superagent' +import superagentPromise from 'superagent-promise' + +import channelFactory from '../factories/Channel' +import connectorFactory from '../factories/Connector' +import conversationFactory from '../factories/Conversation' +import messageFactory from '../factories/Message' +import participantFactory from '../factories/Participant' +import nock from 'nock' +import '../util/start_application' +import { Participant, Connector, Channel, Message, Conversation } from '../../src/models' + +const agent = superagentPromise(superagent, Promise) + +const expectPollResult = (res, messages) => { + const { results, message } = res.body + expect(res.status).to.be(200) + expect(message).to.be(`${messages.length} messages`) + expect(results.waitTime).to.be(0) + expect(results.messages.length).to.be(messages.length) + results.messages.forEach((msg, ind) => { + const expected = messages[ind] + expect(msg.attachment.content).to.be(expected) + expect(typeof msg.participant).to.be('object') + }) +} + +const sendUserMessage = (channel, conversation, text) => { + return agent.post(`${process.env.ROUTETEST}/v1/webhook/${channel._id}`) + .send({ chatId: conversation.chatId, message: { attachment: { type: 'text', content: text } } }) +} + +const sendBotMessages = (conversation, msgs) => { + return agent.post(`${process.env.ROUTETEST}/v1/connectors/${conversation.connector._id}/conversations/${conversation._id}/messages`) + .send({ messages: msgs.map(m => ({ type: 'text', content: m })) }) +} + +const pollConversation = (channel, conversation, last_message_id) => { + let pollUrl = `${process.env.ROUTETEST}/v1/webhook/${channel._id}/conversations/${conversation._id}/poll` + if (last_message_id) { + pollUrl += `?last_message_id=${last_message_id}` + } + return agent.get(pollUrl) + .set({ Authorization: channel.token }) + .send() +} + +describe('Webhooks Controller Testing', () => { + afterEach(async () => { + Promise.all([ + Connector.remove(), + Channel.remove(), + Conversation.remove(), + Message.remove(), + Participant.remove(), + ]) + }) + + describe('POST forwardMessage', () => { + it('should 200 with a valid message', async () => { + const connector = await connectorFactory.build() + nock(connector.url).post('/').reply(200, {}) + const channel = await channelFactory.build(connector, { isActivated: true }) + const res = await agent.post(`${process.env.ROUTETEST}/v1/webhook/${channel._id}`) + .send({ + chatId: 123, + message: { attachment: { type: 'text', content: { value: 'a message' } } }, + }) + + expect(res.status).to.be(200) + }) + + it('should 400 with a deactivated channel', async () => { + try { + const connector = await connectorFactory.build() + const channel = await channelFactory.build(connector, { isActivated: false }) + await agent.post(`${process.env.ROUTETEST}/v1/webhook/${channel._id}`) + .send() + + should.fail() + } catch (err) { + const { message, results } = err.response.body + + expect(err.status).to.be(400) + expect(message).to.be('Channel is not activated') + expect(results).to.be(null) + } + }) + + it('should 404 without a channel', async () => { + try { + await agent.post(`${process.env.ROUTETEST}/v1/webhook/lol`) + .send() + + should.fail() + } catch (err) { + const { message, results } = err.response.body + + expect(err.status).to.be(404) + expect(message).to.be('Channel not found') + expect(results).to.be(null) + } + }) + }) + + describe('GET subscribeWebhook', () => { + it('should 400 with an invalid webhook', async () => { + try { + const connector = await connectorFactory.build() + const channel = await channelFactory.build(connector, { isActivated: false }) + await agent.get(`${process.env.ROUTETEST}/v1/webhook/${channel._id}`) + .send() + + should.fail() + } catch (err) { + const { message, results } = err.response.body + + expect(err.status).to.be(400) + expect(message).to.be('Unimplemented service method') + expect(results).to.be(null) + } + }) + + it('should 404 without a channel', async () => { + try { + await agent.get(`${process.env.ROUTETEST}/v1/webhook/lol`) + .send() + + should.fail() + } catch (err) { + const { message, results } = err.response.body + + expect(err.status).to.be(404) + expect(message).to.be('Channel not found') + expect(results).to.be(null) + } + }) + }) + + describe('POST createConversation', () => { + it('should 200 with a valid channel token', async () => { + const connector = await connectorFactory.build() + const channel = await channelFactory.build(connector) + const res = await agent.post(`${process.env.ROUTETEST}/v1/webhook/${channel._id}/conversations`) + .set({ Authorization: channel.token }) + .send({ messages: [{ type: 'text', content: 'yolo' }] }) + const { message } = res.body + + expect(res.status).to.be(201) + expect(message).to.be('Conversation successfully created') + }) + + it('should 401 with an invalid channel token', async () => { + try { + const connector = await connectorFactory.build() + const channel = await channelFactory.build(connector) + await agent.post(`${process.env.ROUTETEST}/v1/webhook/${channel._id}/conversations`) + .send() + + should.fail() + } catch (err) { + const { message, results } = err.response.body + + expect(err.status).to.be(401) + expect(message).to.be('Request can not be processed with your role') + expect(results).to.be(null) + } + }) + + it('should 400 with an invalid channel type', async () => { + try { + const connector = await connectorFactory.build() + const channel = await channelFactory.build(connector, { type: 'slack' }) + await agent.post(`${process.env.ROUTETEST}/v1/webhook/${channel._id}/conversations`) + .send() + + should.fail() + } catch (err) { + const { message, results } = err.response.body + + expect(err.status).to.be(400) + expect(message).to.be('Invalid channel type') + expect(results).to.be(null) + } + }) + + it('should 404 without a channel', async () => { + try { + await agent.post(`${process.env.ROUTETEST}/v1/webhook/invalid_id/conversations`) + .send() + + should.fail() + } catch (err) { + const { message, results } = err.response.body + + expect(err.status).to.be(404) + expect(message).to.be('Channel not found') + expect(results).to.be(null) + } + }) + }) + + describe('GET getMessages', () => { + it('should 200 with a valid channel token', async () => { + const connector = await connectorFactory.build() + const channel = await channelFactory.build(connector) + const conversation = await conversationFactory.build(connector, channel) + const participant = await participantFactory.build(conversation) + await messageFactory.build(conversation, participant) + await messageFactory.build(conversation, participant) + const response = await agent.get(`${process.env.ROUTETEST}/v1/webhook/${channel._id}/conversations/${conversation._id}/messages`) + .set({ Authorization: channel.token }) + .send() + + expect(response.status).to.be(200) + expect(response.body.message).to.be('Messages successfully fetched') + expect(response.body.results).to.have.length(2) + }) + + it('should 404 with wrong channel', async () => { + const connector = await connectorFactory.build() + const channel = await channelFactory.build(connector) + const conversation = await conversationFactory.build(connector, channel) + + try { + await agent.get(`${process.env.ROUTETEST}/v1/webhook/randomWrongId/conversations/${conversation._id}/messages`) + .set({ Authorization: channel.token }) + .send() + } catch (err) { + expect(err.status).to.be(404) + expect(err.response.body.message).to.be('Channel not found') + expect(err.response.body.results).to.be(null) + } + }) + + it('should 404 with wrong conversation', async () => { + const connector = await connectorFactory.build() + const channel = await channelFactory.build(connector) + + try { + await agent.get(`${process.env.ROUTETEST}/v1/webhook/${channel._id}/conversations/randomWrongId/messages`) + .set({ Authorization: channel.token }) + .send() + } catch (err) { + expect(err.status).to.be(404) + expect(err.response.body.message).to.be('Conversation not found') + expect(err.response.body.results).to.be(null) + } + }) + + it('should 401 without token', async () => { + const connector = await connectorFactory.build() + const channel = await channelFactory.build(connector) + const conversation = await conversationFactory.build(connector, channel) + + try { + await agent.get(`${process.env.ROUTETEST}/v1/webhook/${channel._id}/conversations/${conversation._id}/messages`) + expect().fail('should have failed because of missing token') + } catch (err) { + if (!err.status) { throw err } + expect(err.status).to.be(401) + expect(err.response.body.message).to.be('Request can not be processed with your role') + expect(err.response.body.results).to.be(null) + } + }) + + it('should fail with wrong token', async () => { + const connector = await connectorFactory.build() + const channel = await channelFactory.build(connector) + const otherChannel = await channelFactory.build(connector) + const conversation = await conversationFactory.build(connector, channel) + + try { + await agent.get(`${process.env.ROUTETEST}/v1/webhook/${channel._id}/conversations/${conversation._id}/messages`) + .set({ Authorization: otherChannel.token }) + .send() + + expect().fail('should have failed because of wrong token') + } catch (err) { + if (!err.status) { throw err } + expect(err.status).to.be(401) + expect(err.response.body.message).to.be('Request can not be processed with your role') + expect(err.response.body.results).to.be(null) + } + }) + }) + + describe('GET preferences', () => { + after(async () => { + await Promise.all([ + Connector.remove(), + Channel.remove(), + ]) + }) + + it('should 200 and return preferences', async () => { + const connector = await connectorFactory.build() + const preferences = { + accentColor: '#000000', + complementaryColor: '#100000', + botMessageColor: '#200000', + botMessageBackgroundColor: '#300000', + backgroundColor: '#400000', + headerLogo: '#500000', + headerTitle: '#600000', + botPicture: '#700000', + userPicture: '#800000', + onboardingMessage: '#900000', + expanderLogo: '#110000', + expanderTitle: '#120000', + conversationTimeToLive: 12, + characterLimit: 42, + userInputPlaceholder: 'Write a reply', + } + const channel = await channelFactory.build(connector, { type: 'webchat', ...preferences }) + + const res = await agent.get(`${process.env.ROUTETEST}/v1/webhook/${channel._id}/preferences`) + .set({ Authorization: channel.token }) + .send() + const { results, message } = res.body + expect(res.status).to.be(200) + expect(message).to.be('Preferences successfully rendered') + expect(results.accentColor).to.be(preferences.accentColor) + expect(results.complementaryColor).to.be(preferences.complementaryColor) + expect(results.botMessageColor).to.be(preferences.botMessageColor) + expect(results.botMessageBackgroundColor).to.be(preferences.botMessageBackgroundColor) + expect(results.backgroundColor).to.be(preferences.backgroundColor) + expect(results.headerLogo).to.be(preferences.headerLogo) + expect(results.headerTitle).to.be(preferences.headerTitle) + expect(results.botPicture).to.be(preferences.botPicture) + expect(results.userPicture).to.be(preferences.userPicture) + expect(results.onboardingMessage).to.be(preferences.onboardingMessage) + expect(results.expanderLogo).to.be(preferences.expanderLogo) + expect(results.expanderTitle).to.be(preferences.expanderTitle) + expect(results.conversationTimeToLive).to.be(preferences.conversationTimeToLive) + expect(results.characterLimit).to.be(preferences.characterLimit) + }) + + it('should 401 with an invalid channel token', async () => { + try { + const connector = await connectorFactory.build() + const preferences = {} + const channel = await channelFactory.build(connector, { type: 'webchat', ...preferences }) + await agent.get(`${process.env.ROUTETEST}/v1/webhook/${channel._id}/preferences`) + .send() + + should.fail() + } catch (err) { + const { message, results } = err.response.body + + expect(err.status).to.be(401) + expect(message).to.be('Request can not be processed with your role') + expect(results).to.be(null) + } + }) + + it('should fail for wrong channel type (other than webchat)') + + it('should 404 with non-existing channel', async () => { + try { + const connector = await connectorFactory.build() + const preferences = {} + const channel = await channelFactory.build(connector, { type: 'webchat', ...preferences }) + await agent.get(`${process.env.ROUTETEST}/v1/webhook/non-existing/preferences`) + .send() + + should.fail() + } catch (err) { + const { message, results } = err.response.body + + expect(err.status).to.be(404) + expect(message).to.be('Channel not found') + expect(results).to.be(null) + } + }) + }) + + describe('GET poll', () => { + + let connector + beforeEach(async () => { + connector = await connectorFactory.build() + nock(connector.url).post('/').reply(200, {}) + }) + + afterEach(async () => { + await Promise.all([ + Connector.remove(), + Channel.remove(), + Conversation.remove(), + ]) + }) + + it('should 200 and return immediately with existing messages', async () => { + const channel = await channelFactory.build(connector, { type: 'webchat' }) + const conversation = await conversationFactory.build(connector, channel) + + await sendUserMessage(channel, conversation, 'yolo') + nock(connector.url).post('/').reply(200, {}) + await sendUserMessage(channel, conversation, 'lolz') + await new Promise((resolve) => setTimeout(resolve, 200)) + + const res = await pollConversation(channel, conversation) + expectPollResult(res, ['yolo', 'lolz']) + }) + + it('should 200 and return new incoming message from user', async () => { + const channel = await channelFactory.build(connector, { type: 'webchat' }) + const conversation = await conversationFactory.build(connector, channel) + + await sendUserMessage(channel, conversation, 'yolo') + + // make sure we receive old messages first + const res = await pollConversation(channel, conversation) + expectPollResult(res, ['yolo']) + const last_message_id = res.body.results.messages[0].id + + nock(connector.url).post('/').reply(200, {}) + await sendUserMessage(channel, conversation, 'haha') + + const res2 = await pollConversation(channel, conversation, last_message_id) + expectPollResult(res2, ['haha']) + }) + + it('should 200 and return new incoming message from bot', async () => { + const channel = await channelFactory.build(connector, { type: 'webchat' }) + const conversation = await conversationFactory.build(connector, channel) + + await sendUserMessage(channel, conversation, 'yolo') + + // make sure we receive old messages first + const res = await pollConversation(channel, conversation) + expectPollResult(res, ['yolo']) + const last_message_id = res.body.results.messages[0].id + + await sendBotMessages(conversation, ['megalol']) + + const res2 = await pollConversation(channel, conversation, last_message_id) + expectPollResult(res2, ['megalol']) + }) + + it('should 200 and return new incoming messages from bot and user', async () => { + const channel = await channelFactory.build(connector, { type: 'webchat' }) + const conversation = await conversationFactory.build(connector, channel) + + await sendUserMessage(channel, conversation, 'lool') + + const res = await pollConversation(channel, conversation) + expectPollResult(res, ['lool']) + const last_message_id = res.body.results.messages[0].id + + setTimeout(async () => { await sendBotMessages(conversation, ['mdr', 'ptdr']) }, 300) + + const res2 = await pollConversation(channel, conversation, last_message_id) + expectPollResult(res2, ['mdr']) + const last_message_id2 = res2.body.results.messages[0].id + + const res3 = await pollConversation(channel, conversation, last_message_id2) + expectPollResult(res3, ['ptdr']) + }) + + it('should 200 and forward conversation start', async () => { + const channel = await channelFactory.build(connector, { + type: 'webchat', + forwardConversationStart: true, + }) + nock(connector._doc.url).post('/').reply(200, { results: {}, message: 'Success' }) + const res = await agent.post(`${process.env.ROUTETEST}/v1/webhook/${channel._id}/conversations`) + .set({ Authorization: channel.token }) + .send() + const conversation = res.body.results + conversation._id = conversation.id + + const res2 = await pollConversation(channel, conversation) + expectPollResult(res2, ['']) + }) + + it('should ??? if conversation start fails') + + it('should not return when no message arrives', async () => { + const channel = await channelFactory.build(connector, { type: 'webchat' }) + const conversation = await conversationFactory.build(connector, channel) + + let over = false + pollConversation(channel, conversation).end(() => { over = true }) + await new Promise((resolve) => setTimeout(resolve, 600)) + expect(over).to.be(false) + }) + + it('should 401 with an invalid channel token', async () => { + try { + const channel = await channelFactory.build(connector) + const conversation = await conversationFactory.build(connector, channel) + await agent.get(`${process.env.ROUTETEST}/v1/webhook/${channel._id}/conversations/${conversation._id}/poll`) + .send() + + should.fail() + } catch (err) { + const { message, results } = err.response.body + + expect(err.status).to.be(401) + expect(message).to.be('Request can not be processed with your role') + expect(results).to.be(null) + } + }) + + it('should 400 for invalid channel type', async () => { + const channel = await channelFactory.build(connector, { type: 'slackapp' }) + const conversation = await conversationFactory.build(connector, channel) + + try { + await agent.get(`${process.env.ROUTETEST}/v1/webhook/${channel._id}/conversations/${conversation._id}/poll`) + .set({ Authorization: channel.token }) + .send() + + expect().fail() + } catch (err) { + if (!err.status) { throw err } + expect(err.status).to.be(400) + expect(err.response.body.message).to.be('Invalid channel type') + } + }) + + it('should 404 with non-existing channel', async () => { + try { + const channel = await channelFactory.build(connector) + const conversation = await conversationFactory.build(connector, channel) + await agent.get(`${process.env.ROUTETEST}/v1/webhook/not_existing/conversations/${conversation._id}/poll`) + .send() + + should.fail() + } catch (err) { + const { message, results } = err.response.body + + expect(err.status).to.be(404) + expect(message).to.be('Channel not found') + expect(results).to.be(null) + } + }) + + it('should fail for non-existing conversation') + + it('should 404 with non-existing last_message', async () => { + try { + const channel = await channelFactory.build(connector) + const conversation = await conversationFactory.build(connector, channel) + await agent.get(`${process.env.ROUTETEST}/v1/webhook/${channel._id}/conversations/${conversation._id}/poll?last_message_id=non-existing`) + .set({ Authorization: channel.token }) + .send() + + should.fail() + } catch (err) { + const { message, results } = err.response.body + + expect(err.status).to.be(404) + expect(message).to.be('Message not found') + expect(results).to.be(null) + } + }) + }) + +}) diff --git a/test/factories/channel.js b/test/factories/channel.js new file mode 100644 index 0000000..7f5f5b5 --- /dev/null +++ b/test/factories/channel.js @@ -0,0 +1,22 @@ +import crypto from 'crypto' +import Channel from '../../src/models/channel' + +const build = async (connector, opts = {}) => { + const data = { + connector: connector._id, + slug: opts.slug || crypto.randomBytes(20).toString('hex'), + type: opts.type || 'recastwebchat', + isActivated: opts.isActivated === false ? opts.isActivated : true, + token: crypto.randomBytes(20).toString('hex'), + } + Object.keys(opts).forEach(k => { + if (data[k] === undefined) { + data[k] = opts[k] + } + }) + const channel = new Channel(data) + + return channel.save() +} + +module.exports = { build } diff --git a/test/factories/connector.js b/test/factories/connector.js new file mode 100644 index 0000000..7ecda7d --- /dev/null +++ b/test/factories/connector.js @@ -0,0 +1,13 @@ +import crypto from 'crypto' +import Connector from '../../src/models/connector' + +const build = async (opts = {}) => { + const connector = new Connector({ + url: opts.url || `https://${crypto.randomBytes(20).toString('hex')}.fr`, + isActive: opts.isActive || true, + }) + + return connector.save() +} + +module.exports = { build } diff --git a/test/factories/conversation.js b/test/factories/conversation.js new file mode 100644 index 0000000..e73b5cd --- /dev/null +++ b/test/factories/conversation.js @@ -0,0 +1,14 @@ +import crypto from 'crypto' +import Conversation from '../../src/models/conversation' + +const build = async (connector, channel, opts = {}) => { + const conversation = new Conversation({ + channel, + connector, + chatId: opts.chatId || crypto.randomBytes(20).toString('hex'), + }) + + return conversation.save() +} + +module.exports = { build } diff --git a/test/factories/message.js b/test/factories/message.js new file mode 100644 index 0000000..dd4695f --- /dev/null +++ b/test/factories/message.js @@ -0,0 +1,15 @@ +import Message from '../../src/models/message' + +function build (conversation, participant, opts = {}) { + const message = new Message({ + attachement: opts.attachment || { type: 'text', content: 'this is a text message' }, + conversation: conversation._id, + participant: participant._id, + }) + + return message.save() +} + +export default { + build, +} diff --git a/test/factories/participant.js b/test/factories/participant.js new file mode 100644 index 0000000..5735b7d --- /dev/null +++ b/test/factories/participant.js @@ -0,0 +1,17 @@ +import Participant from '../../src/models/participant' + +function build (conversation, opts = {}) { + const participant = new Participant({ + conversation: conversation._id, + senderId: opts.senderId || 'senderId', + data: opts.data || { name: 'someParticipant' }, + isBot: opts.isBot || false, + type: opts.type || 'user', + }) + + return participant.save() +} + +export default { + build, +} diff --git a/test/factories/persistent_menu.js b/test/factories/persistent_menu.js new file mode 100644 index 0000000..15ad9fd --- /dev/null +++ b/test/factories/persistent_menu.js @@ -0,0 +1,20 @@ +import PersistentMenu from '../../src/models/persistent_menu' + +const build = async (connector, opts = {}) => { + const data = { + connector_id: connector._id, + menu: opts.menu || {}, + default: opts.default || false, + locale: opts.locale || 'en', + } + Object.keys(opts).forEach(k => { + if (data[k] === undefined) { + data[k] = opts[k] + } + }) + const persistentMenu = new PersistentMenu(data) + + return persistentMenu.save() +} + +module.exports = { build } diff --git a/test/mocks/http.js b/test/mocks/http.js deleted file mode 100644 index 746f576..0000000 --- a/test/mocks/http.js +++ /dev/null @@ -1,13 +0,0 @@ -import nock from 'nock' - -const scope = nock('https://api.kik.com') - -scope.post('/v1/config') -.reply(200, { - good: true, -}) - -scope.post('/v1/message') -.reply(200, { - good: true, -}) diff --git a/test/models/.gitkeep b/test/models/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/test/models/Bot.model.tests.js b/test/models/Bot.model.tests.js deleted file mode 100644 index f3fced0..0000000 --- a/test/models/Bot.model.tests.js +++ /dev/null @@ -1,104 +0,0 @@ -import chai from 'chai' -import chaiHttp from 'chai-http' -import mongoose from 'mongoose' - -import Bot from '../../src/models/Bot.model' - -const assert = require('chai').assert -const expect = chai.expect - -chai.use(chaiHttp) - -const fakeBot = { url: 'https://recast.ai' } -const fakeId = '57fe26383750e0379bee8aca' - -function clearDB () { - for (const i in mongoose.connection.collections) { - if (mongoose.connection.collections.hasOwnProperty(i)) { - mongoose.connection.collections[i].remove() - } - } -} - -describe('Bot Model', () => { - describe('Create bot', () => { - describe('Create a bot when db is empty:', () => { - after(async () => clearDB()) - - it('can create bot when no one exists', async () => { - const bot = await new Bot({ url: fakeBot.url }).save() - assert.equal(bot.url, fakeBot.url) - }) - }) - - describe('Create a bot when db have a bot', () => { - before(async () => new Bot({ url: fakeBot.url }).save()) - after(async () => clearDB()) - - it('can create bot when one exists', async () => { - const bot = await new Bot({ url: fakeBot.url }).save() - assert.equal(bot.url, fakeBot.url) - }) - }) - }) - - describe('List bot', () => { - describe('List bot when no one exist', () => { - after(async () => clearDB()) - - it('can list bot when no one exists', async () => { - const bots = await Bot.find({}).exec() - expect(bots).to.have.length(0) - }) - }) - - describe('List bots when two exist', () => { - before(async () => Promise.all([ - new Bot({ url: 'https://hello.com' }).save(), - new Bot({ url: 'https://bye.com' }).save(), - ])) - after(async () => clearDB()) - - it('can list bots when two exists', async () => { - const bots = await Bot.find({}) - expect(bots).to.have.length(2) - }) - }) - }) - - describe('Update bot', () => { - describe('can update bot when one bot exist', () => { - let bot = {} - before(async () => bot = await new Bot({ url: fakeBot.url }).save()) - after(async () => clearDB()) - - it('can updated bots when one bot exists', async () => { - const updatedBot = await Bot.findOneAndUpdate({ _id: bot._id }, { $set: { url: 'https://updated.com' } }, { new: true }).exec() - assert.equal(updatedBot.url, 'https://updated.com') - }) - }) - }) - - describe('Delete bot', () => { - describe('can delete bot when no bot exist', () => { - after(async () => clearDB()) - - it('can delete bot when id not found', async () => { - const deletedBots = await Bot.remove({ _id: fakeId }) - assert.equal(deletedBots.result.n, 0) - }) - }) - - describe('can delete bot when one bot exist', () => { - let bot = {} - before(async () => bot = await new Bot({ url: fakeBot.url }).save()) - after(async () => clearDB()) - - it('can delete bot when one exists', async () => { - const deletedBots = await Bot.remove({ _id: bot._id }) - assert.equal(deletedBots.result.n, 1) - }) - }) - }) - -}) diff --git a/test/models/Channel.model.tests.js b/test/models/Channel.model.tests.js deleted file mode 100644 index 57b2a0a..0000000 --- a/test/models/Channel.model.tests.js +++ /dev/null @@ -1,75 +0,0 @@ -import chai from 'chai' -import chaiHttp from 'chai-http' - -import Bot from '../../src/models/Bot.model' -import Channel from '../../src/models/Channel.model' - -const assert = require('chai').assert -const expect = chai.expect - -chai.use(chaiHttp) - -const fakeChannel = { - type: 'slack', - isActivated: true, - slug: 'slug-test1', - token: '1234567890', -} - -describe('Channel Model', () => { - let bot = {} - before(async () => bot = await new Bot({ url: 'https://bonjour.com' })) - - describe('Create Channel', () => { - after(async () => Channel.remove({})) - - it('can create a new Channel', async () => { - const channel = await new Channel({ bot: bot._id, ...fakeChannel }).save() - assert.equal(channel.type, fakeChannel.type) - assert.equal(channel.isActivated, fakeChannel.isActivated) - assert.equal(channel.slug, fakeChannel.slug) - assert.equal(channel.token, fakeChannel.token) - }) - }) - - describe('List Channel', () => { - after(async () => Channel.remove({})) - - it('can list channels when no one exists', async () => { - const channels = await Channel.find({}).exec() - expect(channels).to.have.length(0) - }) - - it('can list 1 channel', async () => { - await new Channel({ bot: bot._id, ...fakeChannel }).save() - const channels = await Channel.find({}).exec() - - expect(channels).to.have.length(1) - }) - }) - - describe('Update Channel', () => { - after(async () => Channel.remove({})) - - it('can update 1 channel', async () => { - const channel = await new Channel({ bot: bot._id, ...fakeChannel }).save() - const updatedChannel = await Channel.findOneAndUpdate({ _id: channel._id }, { $set: { isActivated: false } }, { new: true }).exec() - - assert.equal(updatedChannel.isActivated, false) - }) - }) - - describe('Delete Channel', () => { - after(async () => Channel.remove({})) - - it('can remove channel #2', async () => { - const [channel1] = await Promise.all([ - new Channel({ bot: bot._id, ...fakeChannel }).save(), - new Channel({ bot: bot._id, ...fakeChannel }).save(), - ]) - - const deletedChannel = await channel1.remove() - assert.equal(deletedChannel._id, channel1._id) - }) - }) -}) diff --git a/test/models/Conversation.model.tests.js b/test/models/Conversation.model.tests.js deleted file mode 100644 index 6b7be20..0000000 --- a/test/models/Conversation.model.tests.js +++ /dev/null @@ -1,95 +0,0 @@ -import chai from 'chai' -import chaiHttp from 'chai-http' - -import Bot from '../../src/models/Bot.model' -import Channel from '../../src/models/Channel.model' -import Conversation from '../../src/models/Conversation.model' - -const assert = chai.assert -const expect = chai.expect - -chai.use(chaiHttp) - -let bot = null -let channel = null -let payload = null - -describe('Conversation Model', () => { - before(async () => { - bot = await new Bot({ url: 'https://bonjour.com' }).save() - channel = await new Channel({ bot: bot._id, type: 'kik', slug: 'kik', isActivated: true }).save() - - payload = { - channel: channel._id, - bot: bot._id, - isActive: true, - chatId: 'testChatId', - } - }) - - after(async () => { - await Promise.all([ - Bot.remove({}), - Channel.remove({}), - Conversation.remove({}), - ]) - }) - - describe('Create conversation', () => { - after(async () => Conversation.remove({})) - - it('can create conversation when no one created', async () => { - const conversation = await new Conversation(payload) - - assert.equal(conversation.bot, payload.bot) - assert.equal(conversation.channel, payload.channel) - assert.equal(conversation.chatId, payload.chatId) - assert.equal(conversation.isActive, payload.isActive) - }) - }) - - describe('List Conversations', () => { - after(async () => Conversation.remove({})) - - it('can list Conversations when no one exists', async () => { - const conversations = await Conversation.find({}).exec() - expect(conversations).to.have.length(0) - }) - - it('can list 1 conversation', async () => { - await Promise.all([ - new Conversation(payload).save(), - new Conversation(payload).save(), - ]) - - const conversations = await Conversation.find({}).exec() - expect(conversations).to.have.length(2) - }) - }) - - describe('Update Conversation', () => { - after(async () => Conversation.remove({})) - - it('can update a conversation', async () => { - const newPayload = { isActive: false } - - const conversation = await new Conversation(payload).save() - const updatedConversation = await Conversation.findOneAndUpdate({ _id: conversation._id }, { $set: newPayload }, { new: true }) - - assert.equal(conversation.bot.toString(), updatedConversation.bot.toString()) - assert.equal(conversation.chatId, updatedConversation.chatId) - assert.equal(updatedConversation.isActive, false) - }) - }) - - describe('Delete Channel', () => { - after(async () => Conversation.remove({})) - - it('can remove conversation', async () => { - await new Conversation(payload).save() - const deletedConversations = await Conversation.remove({}) - - assert.equal(deletedConversations.result.n, 1) - }) - }) -}) diff --git a/test/models/Participant.model.tests.js b/test/models/Participant.model.tests.js deleted file mode 100644 index 4c36d4f..0000000 --- a/test/models/Participant.model.tests.js +++ /dev/null @@ -1,88 +0,0 @@ -import chai from 'chai' -import chaiHttp from 'chai-http' -import mongoose from 'mongoose' - -import Participant from '../../src/models/Participant.model' - -chai.use(chaiHttp) - -const assert = require('chai').assert - -const fakeParticipant = { - isBot: false, - senderId: '1234', -} - -function clearDB () { - for (const i in mongoose.connection.collections) { - if (mongoose.connection.collections.hasOwnProperty(i)) { - mongoose.connection.collections[i].remove() - } - } -} - -describe('Participant Model', () => { - describe('Create a participant', () => { - after(async () => clearDB()) - - it('can create bot when no one exists', async () => { - const participant = await new Participant(fakeParticipant).save() - - assert.equal(participant.isBot, fakeParticipant.isBot) - assert.equal(participant.senderId, fakeParticipant.senderId) - }) - }) - - describe('List participant', () => { - describe('with no participants', () => { - after(async () => clearDB()) - - it('can list participants', async () => { - const participants = await Participant.find({}).exec() - - chai.expect(participants.length).to.equal(0) - }) - }) - - describe('with participants', () => { - before(async () => Promise.all([ - new Participant(fakeParticipant).save(), - new Participant(fakeParticipant).save(), - ])) - after(async () => clearDB()) - - it('can index participants', async () => { - const participants = await Participant.find({}).exec() - - chai.expect(participants.length).to.equal(2) - }) - }) - }) - - describe('Update a participant:', () => { - describe('with participants', () => { - let participant = {} - before(async () => participant = await new Participant(fakeParticipant).save()) - after(async () => clearDB()) - - it('can update a participant', async () => { - const updatedParticipant = await Participant.findOneAndUpdate({ _id: participant._id }, { $set: { isBot: true } }, { new: true }) - assert.equal(updatedParticipant.isBot, true) - }) - }) - }) - - describe('Delete a participant:', () => { - describe('with participants', () => { - let participant = {} - before(async () => participant = await new Participant(fakeParticipant).save()) - after(async () => clearDB()) - - it('can remove a specific participant', async () => { - const deletedParticipants = await Participant.remove({ _id: participant._id }) - - assert.equal(deletedParticipants.result.n, 1) - }) - }) - }) -}) diff --git a/test/routes/.gitkeep b/test/routes/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/test/routes/Bots.routes.tests.js b/test/routes/Bots.routes.tests.js deleted file mode 100644 index 3b0f9c3..0000000 --- a/test/routes/Bots.routes.tests.js +++ /dev/null @@ -1,32 +0,0 @@ -import chai from 'chai' -import fetchMethod from '../services/fetchMethod.service' - -import config from '../../config/test' - -import botController from '../../src/controllers/Bots.controller' - -describe("Routes", () => { - it("GET /bots/:bot_id", done => { - chai.expect(fetchMethod('GET', '/bots/:bot_id')).to.equal(botController.getBotById) - done() - }) - - it("GET /bots", done => { - chai.expect(fetchMethod('GET', '/bots')).to.equal(botController.getBots) - done() - }) - - it("POST /bots", done => { - chai.expect(fetchMethod('POST', '/bots')).to.equal(botController.createBot) - done() - }) - - it("PUT /bots/:bot_id", done => { - chai.expect(fetchMethod('PUT', '/bots/:bot_id')).to.equal(botController.updateBotById) - done() - }) - it("DELETE /bots/:bot_id", done => { - chai.expect(fetchMethod('DELETE', '/bots/:bot_id')).to.equal(botController.deleteBotById) - done() - }) -}) diff --git a/test/routes/Channels.routes.tests.js b/test/routes/Channels.routes.tests.js deleted file mode 100644 index 22ff44e..0000000 --- a/test/routes/Channels.routes.tests.js +++ /dev/null @@ -1,27 +0,0 @@ -import chai from 'chai' -import fetchMethod from '../services/fetchMethod.service' -import config from '../../config/test' -import channelController from '../../src/controllers/Channels.controller' - -describe("Routes", () => { - it("POST /bots/:bot_id/channels", done => { - chai.expect(fetchMethod('POST', '/bots/:bot_id/channels')).to.equal(channelController.createChannelByBotId) - done() - }) - it("GET /bots/:bot_id/channels", done => { - chai.expect(fetchMethod('GET', '/bots/:bot_id/channels')).to.equal(channelController.getChannelsByBotId) - done() - }) - it("GET /bots/:bot_id/channels/:channel_slug", done => { - chai.expect(fetchMethod('GET', '/bots/:bot_id/channels/:channel_slug')).to.equal(channelController.getChannelByBotId) - done() - }) - it("PUT /bots/:bot_id/channels/:channel_slug", done => { - chai.expect(fetchMethod('PUT', '/bots/:bot_id/channels/:channel_slug')).to.equal(channelController.updateChannelByBotId) - done() - }) - it("DELETE /bots/:bot_id/channels/:channel_slug", done => { - chai.expect(fetchMethod('DELETE', '/bots/:bot_id/channels/:channel_slug')).to.equal(channelController.deleteChannelByBotId) - done() - }) -}) diff --git a/test/routes/Conversations.routes.tests.js b/test/routes/Conversations.routes.tests.js deleted file mode 100644 index 9ce5676..0000000 --- a/test/routes/Conversations.routes.tests.js +++ /dev/null @@ -1,29 +0,0 @@ -import config from '../../config' -import ConversationController from '../../src/controllers/Conversations.controller' -import fetchMethod from '../services/fetchMethod.service' -const chai = require('chai') -const chaiHttp = require('chai-http') - -const expect = chai.expect -chai.use(chaiHttp) - -const Bot = require('../../src/models/Bot.model.js') - -describe('Conversation Routes', () => { - - it('Should call function getConversationsByBotId: GET /bots/:bot_id/conversations', (done) => { - chai.expect(fetchMethod('GET', '/bots/:bot_id/conversations')).to.equal(ConversationController.getConversationsByBotId) - done() - }) - - it('Should call function getConversationByBotId: GET /bots/:bot_id/conversations/conversation_id', (done) => { - //chai.expect(fetchMethod('GET', '/bots/:bot_id/conversations/conversation_id')).to.equal(ConversationController.getConversationByBotId) - chai.expect(fetchMethod('GET', '/bots/:bot_id/conversations/:conversation_id')).to.equal(ConversationController.getConversationByBotId) - done() - }) - - it('Should call function deleteConversationByBotId: DELETE /bots/:bot_id/conversations/:conversation_id', (done) => { - chai.expect(fetchMethod('DELETE', '/bots/:bot_id/conversations/:conversation_id')).to.equal(ConversationController.deleteConversationByBotId) - done() - }) -}) diff --git a/test/routes/Participants.routes.tests.js b/test/routes/Participants.routes.tests.js deleted file mode 100644 index 9f7b48e..0000000 --- a/test/routes/Participants.routes.tests.js +++ /dev/null @@ -1,16 +0,0 @@ -import chai from 'chai' -import fetchMethod from '../services/fetchMethod.service' - -import ParticipantsController from '../../src/controllers/Participants.controller' - -describe("Routes", () => { - it("GET /bots/:bot_id/participants", done => { - chai.expect(fetchMethod('GET', '/bots/:bot_id/participants')).to.equal(ParticipantsController.getParticipantsByBotId) - done() - }) - - it("GET /bots/:bot_id/participants/:participant_id", done => { - chai.expect(fetchMethod('GET', '/bots/:bot_id/participants/:participant_id')).to.equal(ParticipantsController.getParticipantByBotId) - done() - }) -}) diff --git a/test/routes/application.js b/test/routes/application.js new file mode 100644 index 0000000..2991fe1 --- /dev/null +++ b/test/routes/application.js @@ -0,0 +1,23 @@ +import expect from 'expect.js' + +import { fetchMethod } from '../tools' + +import AppController from '../../src/controllers/application' +import AppRoutes from '../../src/routes/application' + +describe('App routes', () => { + + describe('GET /', () => { + it('should call AppController#index', async () => { + expect(fetchMethod(AppRoutes, 'GET', '/')).to.equal(AppController.index) + }) + }) + + describe('POST /', () => { + it('should call AppController#index', async () => { + expect(fetchMethod(AppRoutes, 'POST', '/')).to.equal(AppController.index) + }) + }) + +}) + diff --git a/test/routes/channels.js b/test/routes/channels.js new file mode 100644 index 0000000..26251a2 --- /dev/null +++ b/test/routes/channels.js @@ -0,0 +1,46 @@ +import expect from 'expect.js' + +import { fetchMethod } from '../tools' + +import ChannelsController from '../../src/controllers/channels' +import ChannelsRoutes from '../../src/routes/channels' + +describe('Channels Routes', () => { + + describe('POST /channels', () => { + it('should call ChannelsController#create', async () => { + expect(fetchMethod(ChannelsRoutes, 'POST', '/connectors/:connectorId/channels')).to.equal(ChannelsController.create) + }) + }) + + describe('GET /channels', () => { + it('should call ChannelsController#index', async () => { + expect(fetchMethod(ChannelsRoutes, 'GET', '/connectors/:connectorId/channels')).to.equal(ChannelsController.index) + }) + }) + + describe('GET /channels/:channel_slug', () => { + it('should call ChannelsController#show', async () => { + expect( + fetchMethod(ChannelsRoutes, 'GET', '/connectors/:connectorId/channels/:channel_slug') + ).to.equal(ChannelsController.show) + }) + }) + + describe('PUT /channels/:channel_slug', () => { + it('should call ChannelsController#update', async () => { + expect( + fetchMethod(ChannelsRoutes, 'PUT', '/connectors/:connectorId/channels/:channel_slug') + ).to.equal(ChannelsController.update) + }) + }) + + describe('DELETE /channels/:channel_slug', () => { + it('should call ChannelsController#delete', async () => { + expect( + fetchMethod(ChannelsRoutes, 'DELETE', '/connectors/:connectorId/channels/:channel_slug') + ).to.equal(ChannelsController.delete) + }) + }) + +}) diff --git a/test/routes/connectors.js b/test/routes/connectors.js new file mode 100644 index 0000000..5d33de2 --- /dev/null +++ b/test/routes/connectors.js @@ -0,0 +1,38 @@ +import expect from 'expect.js' + +import { fetchMethod } from '../tools' + +import Connectors from '../../src/controllers/connectors' +import ConnectorsRoutes from '../../src/routes/connectors' + +describe('Connectors Routes', () => { + + describe('GET /connectors/:bot_id', () => { + it('should call ConnectorsController#show', async () => { + expect(fetchMethod(ConnectorsRoutes, 'GET', '/connectors/:connectorId')).to.equal(Connectors.show) + }) + }) + + describe('POST /connectors', () => { + it('should call ConnectorsController#create', async () => { + expect(fetchMethod(ConnectorsRoutes, 'POST', '/connectors')).to.equal(Connectors.create) + }) + }) + + describe('PUT /connectors/:connectorId', () => { + it('should call ConnectorsController#update', async () => { + expect( + fetchMethod(ConnectorsRoutes, 'PUT', '/connectors/:connectorId') + ).to.equal(Connectors.update) + }) + }) + + describe('DELETE /connectors/:connectorId', () => { + it('should call ConnectorsController#delete', async () => { + expect( + fetchMethod(ConnectorsRoutes, 'DELETE', '/connectors/:connectorId') + ).to.equal(Connectors.delete) + }) + }) + +}) diff --git a/test/routes/conversations.js b/test/routes/conversations.js new file mode 100644 index 0000000..31b368a --- /dev/null +++ b/test/routes/conversations.js @@ -0,0 +1,42 @@ +import expect from 'expect.js' + +import { fetchMethod } from '../tools' + +import ConversationsController from '../../src/controllers/conversations' +import ConversationsRoutes from '../../src/routes/conversations' + +describe('Conversations Routes', () => { + + describe('GET /conversations', () => { + it('should call ConversationsController#index', async () => { + expect( + fetchMethod(ConversationsRoutes, 'GET', '/connectors/:connectorId/conversations') + ).to.equal(ConversationsController.index) + }) + }) + + describe('GET /conversations/:conversation_id', () => { + it('should call ConversationsController#show', async () => { + expect( + fetchMethod(ConversationsRoutes, 'GET', '/connectors/:connectorId/conversations/:conversation_id') + ).to.equal(ConversationsController.show) + }) + }) + + describe('DELETE /conversations/:conversation_id', () => { + it('should call ConversationsController#delete', async () => { + expect( + fetchMethod(ConversationsRoutes, 'DELETE', '/connectors/:connectorId/conversations/:conversation_id') + ).to.equal(ConversationsController.delete) + }) + }) + + describe('POST /conversations/dump', () => { + it('should call ConversationsController#dumpDelete', async () => { + expect( + fetchMethod(ConversationsRoutes, 'POST', '/connectors/:connectorId/conversations/dump') + ).to.equal(ConversationsController.dumpDelete) + }) + }) + +}) diff --git a/test/routes/messages.js b/test/routes/messages.js new file mode 100644 index 0000000..748837d --- /dev/null +++ b/test/routes/messages.js @@ -0,0 +1,26 @@ +import expect from 'expect.js' + +import { fetchMethod } from '../tools' + +import MessagesController from '../../src/controllers/messages' +import MessagesRoutes from '../../src/routes/messages' + +describe('Messages Routes', () => { + + describe('POST /connectors/:connectorId/conversations/:conversationId/messages', () => { + it('should call MessagesController#postMessage', async () => { + expect( + fetchMethod(MessagesRoutes, 'POST', '/connectors/:connectorId/conversations/:conversationId/messages') + ).to.equal(MessagesController.postMessage) + }) + }) + + describe('POST /connectors/:connectorId/messages', () => { + it('should call MessagesController#postMessages', async () => { + expect( + fetchMethod(MessagesRoutes, 'POST', '/connectors/:connectorId/messages') + ).to.equal(MessagesController.broadcastMessage) + }) + }) + +}) diff --git a/test/routes/participants.js b/test/routes/participants.js new file mode 100644 index 0000000..2c12400 --- /dev/null +++ b/test/routes/participants.js @@ -0,0 +1,22 @@ +import expect from 'expect.js' + +import { fetchMethod } from '../tools' + +import ParticipantsController from '../../src/controllers/participants' +import ParticipantsRoutes from '../../src/routes/participants' + +describe('Participants Routes Testing', () => { + + describe('GET /connectors/:connectorId/participants', () => { + it('should call ParticipantsController#getParticipantsByConnectorId', async () => { + expect(fetchMethod(ParticipantsRoutes, 'GET', '/connectors/:connectorId/participants')).to.equal(ParticipantsController.index) + }) + }) + + describe('GET /connectors/:connectorId/participants/:participant_id', () => { + it('should call ParticipantsController#getParticipantByConnectorId', async () => { + expect(fetchMethod(ParticipantsRoutes, 'GET', '/connectors/:connectorId/participants/:participant_id')).to.equal(ParticipantsController.show) + }) + }) + +}) diff --git a/test/routes/persistent_menus.js b/test/routes/persistent_menus.js new file mode 100644 index 0000000..3570dc0 --- /dev/null +++ b/test/routes/persistent_menus.js @@ -0,0 +1,51 @@ +import expect from 'expect.js' + +import { fetchMethod } from '../tools' + +import PersistentMenuController from '../../src/controllers/persistent_menus' +import PersistentMenusRoutes from '../../src/routes/persistent_menus' + +describe('Persistent Menu Routes', () => { + + describe('GET /connectors/:connectorId/persistentmenus', () => { + it('should call PersistentMenuController#index', async () => { + expect(fetchMethod(PersistentMenusRoutes, 'GET', '/connectors/:connectorId/persistentmenus')).to.equal(PersistentMenuController.index) + }) + }) + + describe('GET /connectors/:connectorId/persistentmenus/:language', () => { + it('should call PersistentMenuController#show', async () => { + expect(fetchMethod(PersistentMenusRoutes, 'GET', '/connectors/:connectorId/persistentmenus/:language')).to.equal(PersistentMenuController.show) + }) + }) + + describe('POST /connectors/:connectorId/persistentmenus', () => { + it('should call PersistentMenuController#create', async () => { + expect(fetchMethod(PersistentMenusRoutes, 'POST', '/connectors/:connectorId/persistentmenus')).to.equal(PersistentMenuController.create) + }) + }) + + describe('POST /connectors/:connectorId/persistentmenus/setDefault', () => { + it('should call PersistentMenuController#setdefault', async () => { + expect(fetchMethod(PersistentMenusRoutes, 'POST', '/connectors/:connectorId/persistentmenus/setdefault')).to.equal(PersistentMenuController.setDefault) + }) + }) + + describe('DELETE /connectors/:connectorId/persistentmenus', () => { + it('should call PersistentMenuController#deleteAll', async () => { + expect(fetchMethod(PersistentMenusRoutes, 'DELETE', '/connectors/:connectorId/persistentmenus')).to.equal(PersistentMenuController.deleteAll) + }) + }) + + describe('DELETE /connectors/:connectorId/persistentmenus/:language', () => { + it('should call PersistentMenuController#delete', async () => { + expect(fetchMethod(PersistentMenusRoutes, 'DELETE', '/connectors/:connectorId/persistentmenus/:language')).to.equal(PersistentMenuController.delete) + }) + }) + + describe('PUT /connectors/:connectorId/persistentmenus/:language', () => { + it('should call PersistentMenuController#update', async () => { + expect(fetchMethod(PersistentMenusRoutes, 'PUT', '/connectors/:connectorId/persistentmenus/:language')).to.equal(PersistentMenuController.update) + }) + }) +}) diff --git a/test/routes/webhooks.js b/test/routes/webhooks.js new file mode 100644 index 0000000..1deb475 --- /dev/null +++ b/test/routes/webhooks.js @@ -0,0 +1,46 @@ +import expect from 'expect.js' + +import { fetchMethod } from '../tools' + +import WebhookController from '../../src/controllers/webhooks' +import WebhooksRoutes from '../../src/routes/webhooks' + +describe('Webhooks Routes', () => { + + describe('POST /webhook/:channel_id', () => { + it('should call WebhooksController#handleMethodAction', async () => { + expect(fetchMethod(WebhooksRoutes, 'POST', '/webhook/:channel_id')).to.equal(WebhookController.handleMethodAction) + }) + }) + + describe('GET /webhook/:channel_id', () => { + it('should call WebhooksController#handleMethodAction', async () => { + expect(fetchMethod(WebhooksRoutes, 'GET', '/webhook/:channel_id')).to.equal(WebhookController.handleMethodAction) + }) + }) + + describe('POST /webhook/:channel_id/conversations', () => { + it('should call WebhooksController#createConversation', async () => { + expect(fetchMethod(WebhooksRoutes, 'POST', '/webhook/:channel_id/conversations')).to.equal(WebhookController.createConversation) + }) + }) + + describe('GET /webhook/:channel_id/conversations/:conversation_id/messages', () => { + it('should call WebhooksController#getMessages', async () => { + expect(fetchMethod(WebhooksRoutes, 'GET', '/webhook/:channel_id/conversations/:conversation_id/messages')).to.equal(WebhookController.getMessages) + }) + }) + + describe('GET /webhook/:channel_id/conversations/:conversation_id/poll', () => { + it('should call WebhooksController#poll', async () => { + expect(fetchMethod(WebhooksRoutes, 'GET', '/webhook/:channel_id/conversations/:conversation_id/poll')).to.equal(WebhookController.poll) + }) + }) + + describe('GET /webhook/:channel_id/preferences', () => { + it('should call WebhooksController#preferences', async () => { + expect(fetchMethod(WebhooksRoutes, 'GET', '/webhook/:channel_id/preferences')).to.equal(WebhookController.getPreferences) + }) + }) + +}) diff --git a/test/services/Facebook.service.tests.js b/test/services/Facebook.service.tests.js deleted file mode 100644 index 6877c00..0000000 --- a/test/services/Facebook.service.tests.js +++ /dev/null @@ -1,345 +0,0 @@ -import mongoose from 'mongoose' -import chai from 'chai' -import chaiHttp from 'chai-http' - -import FacebookService from '../../src/services/Messenger.service' -import { - invoke, - invokeSync, - getWebhookToken, -} from '../../src/utils/index.js' -chai.use(chaiHttp) -const expect = chai.expect -const should = chai.should() - -let payload -let res -const opts = { - senderId:'774961692607582', - chatId:'913902005381557'} - - - describe('FacebookService', () => { - - describe('Suscribe webhook', () => { - - it('should be Ok simple suscribe', async () => { - const req = { - query: {} - } - const channel = { - slug: '12345', - _id: '1231234' - } - - req.query['hub.mode'] = 'subscribe' - req.query['hub.verify_token'] = getWebhookToken(channel._id, channel.slug) - - const res = invokeSync('messenger', 'connectWebhook',[req, channel]) - expect(res).to.equal(true) - }) - - it('should be Ok bad token', async () => { - const req = { - query: {} - } - const channel = { - slug: '12345666', - _id: '12341234', - } - req.query['hub.mode'] = 'subscribe' - req.query['hub.verify_token'] = 'qwerqwerqwer' - - - const res = invokeSync('messenger', 'connectWebhook',[req, channel]) - expect(res).to.equal(false) - }) - }) - - xdescribe('Check Security', () => { - - it('should be Ok security check', async () => { - const req = { - headers: { - host: 'a02780b6.ngrok.io', - } - } - req.headers['X-Hub-Signature'] = '1234=1234s' - const channel = { - apiKey: '1234', - webhook: 'https://a02780b6.ngrok.io', - } - invoke('messenger', 'checkSecurity',[req, channel]).then(res => { - ; - }) - }) - - }) - - describe('Check all params are valide', () => { - - it('should be Ok all parameter', async () => { - const channel = { - apiKey: '1234', - webhook: 'https://a02780b6.ngrok.io', - token: 'qrqwerqwerjqkwerfqweiroqwoejqweiruqweprqwnje riqwerhqwpierhquwepriqnweorhuqweprhqnsdfjqpweiryqhsndfkqwuler', - } - const res = invokeSync('messenger', 'checkParamsValidity',[channel]) - expect(res).to.equal(true) - }) - - it('should not be OK messing token', async () => { - const channel = { - apiKey: '1234', - webhook: 'https://a02780b6.ngrok.io', - } - let err = null - try{ - const a = invokeSync('messenger', 'checkParamsValidity',[channel]) - } catch(e) { err = e } finally { expect(err).exist } - }) - - it('should not be OK messing webhhok', async () => { - const channel = { - apiKey: '1234', - token: '1234123412341234123412341234123412341234', - } - let err = null - try{ - const a = invokeSync('messenger', 'checkParamsValidity',[channel]) - } catch(e) { err = e } finally { expect(err).exist } - }) - - it('should not be OK messing apiKey', async () => { - const channel = { - webhook: '1234', - token: '1234123412341234123412341234123412341234', - } - let err = null - try{ - const a = invokeSync('messenger', 'checkParamsValidity',[channel]) - } catch(e) { err = e } finally { expect(err).exist } - }) - }) - - describe('Check extaOptions', () => { - - it('should be OK all option', async () => { - const req = { - body: { entry:[{messaging:[{recipient:{id:'12341234'},sender:{id:'sendeid'}}]}]} - } - const a = invokeSync('messenger', 'extractOptions',[req]) - expect(a.chatId).to.equal(req.body.entry[0].messaging[0].recipient.id) - expect(a.senderId).to.equal(req.body.entry[0].messaging[0].sender.id) - }) - }) - - describe('Parsing message', () => { - - it('should be Ok simple text', async () => { - payload = - { - object: 'page', - entry: [{ - id: '913902005381557', - time: 1476866470972, - messaging: [{ - sender: { id: '774961692607582' }, - recipient: { id: '913902005381557' }, - timestamp: 1476866467894, - message: { mid: 'mid.1476866467894:06f1095d94', seq: 2814, text: 'hello' } - }] - }] - } - const message = payload.entry[0].messaging[0] - const res = await invoke('messenger', 'parseChannelMessage',['conversation',payload, {id:'774961692607582',chatId:'913902005381557'}]) - expect(res[2].chatId).to.equal(message.recipient.id) - expect(res[2].id).to.equal(message.sender.id) - expect(res[1].attachment.text).to.equal(message.message.text) - }) - - it('should be Ok simple video', async () => { - - payload = - { - object: 'page', - entry: [{ - id: '913902005381557', - time: 1476866470972, - messaging: [{ - sender: { id: '774961692607582' }, - recipient: { id: '913902005381557' }, - timestamp: 1476866467894, - message: { attachment: [{ type: 'video', payload: { url: 'http://www.w3schools.com/css/paris.jpg' } }] - } - }] - }] - } - const message = payload.entry[0].messaging[0] - const res = await invoke('messenger', 'parseChannelMessage',['conversation',payload, {id:'774961692607582',chatId:'913902005381557'}]) - expect(res[2].id).to.equal(message.sender.id) - expect(res[1].attachment.text).to.equal(message.message.text) - }) - - it('should be Ok simple picture', async () => { - payload = - payload = - { - object: 'page', - entry: [{ - id: '913902005381557', - time: 1476866470972, - messaging: [{ - sender: { id: '774961692607582' }, - recipient: { id: '913902005381557' }, - timestamp: 1476866467894, - message: { - attachment: - [{ type: 'image', - payload: { url: 'http://www.w3schools.com/css/paris.jpg' } - }] - } - }] - }] - } - const message = payload.entry[0].messaging[0] - const res = await invoke('messenger', 'parseChannelMessage',['conversation',payload, {id:'774961692607582',chatId:'913902005381557'}]) - expect(res[2].chatId).to.equal(message.recipient.id) - expect(res[2].id).to.equal(message.sender.id) - expect(res[1].attachment.text).to.equal(message.message.text) - }) - - it('should be Ok First text', async () => { - payload = - { - object: 'page', - entry: [{ - id: '913902005381557', - time: 1476866470972, - messaging: [{ - sender: { id: '774961692607582' }, - recipient: { id: '913902005381557' }, - timestamp: 1476866467894, - postback: { mid: 'mid.1476866467894:06f1095d94', seq: 2814, text: 'hello' } - }] - }] - } - const message = payload.entry[0].messaging[0] - const res = await invoke('messenger', 'parseChannelMessage',['conversation',payload, {id:'774961692607582',chatId:'913902005381557'}]) - expect(res[2].chatId).to.equal(message.recipient.id) - expect(res[2].id).to.equal(message.sender.id) - expect(res[1].attachment.text).to.equal('start_conversation') - }) -}) - - -describe('Formatting message', () => { - it('should be Ok simple text', async () => { - payload = { - attachment: { - type: 'text', - content: 'Yo les loozers', - }, - } - - const res = await invoke('messenger', 'formatMessage',['conversation',payload, opts]) - expect(res.recipient.id).to.equal(opts.senderId) - expect(res.message.text).to.equal(payload.attachment.content) - }) - - it('should be Ok picture', async () => { - payload = { - attachment: { - type: 'picture', - content: 'http://www.w3schools.com/css/paris.jpg', - }, - } - - const res = await invoke('messenger', 'formatMessage',['conversation',payload, opts]) - expect(res.recipient.id).to.equal(opts.senderId) - expect(res.message.attachment.type).to.equal('image') - expect(res.message.attachment.payload.url).to.equal(payload.attachment.content) - }) - - it('should be Ok video', async () => { - payload = { - attachment: { - type: 'video', - content: 'https://www.youtube.com/watch?v=Ly7uj0JwgKg&list=FLa_5ITc5wcz3ZvbG1aFkWfg&index=51', - }, - } - const res = await invoke('messenger', 'formatMessage',['conversation',payload, opts]) - expect(res.recipient.id).to.equal(opts.senderId) - expect(res.message.attachment.type).to.equal(payload.attachment.type) - expect(res.message.attachment.payload.url).to.equal(payload.attachment.content) - }) - - it('should be Ok audio', async () => { - payload = { - attachment: { - type: 'audio', - content: 'https://www.youtube.com/watch?v=Ly7uj0JwgKg&list=FLa_5ITc5wcz3ZvbG1aFkWfg&index=51', - }, - } - const res = await invoke('messenger', 'formatMessage',['conversation',payload, opts]) - expect(res.recipient.id).to.equal(opts.senderId) - expect(res.message.attachment.type).to.equal(payload.attachment.type) - expect(res.message.attachment.payload.url).to.equal(payload.attachment.content) - }) - - it('should be Ok quickreplies', async () => { - payload = { - attachment: { - type: 'quickReplies', - content: { - title: 'i am the title', - template_type: 'button', - buttons: [{title:'1', value:'DEVELOPER_DEFINED_PAYLOAD_FOR_PICKING_RED', type: 'location'}, {title:'2',value:'DEVELOPER_DEFINED_PAYLOAD_FOR_PICKING_RED', type: 'text'}], - } - }, - } - const res = await invoke('messenger', 'formatMessage',['conversation',payload, opts]) - expect(res.recipient.id).to.equal(opts.senderId) - expect(res.message.quick_replies[0].content_type).to.equal(payload.attachment.content.buttons[0].type) - expect(res.message.quick_replies[0].title).to.equal(payload.attachment.content.buttons[0].title) - expect(res.message.quick_replies[0].payload).to.equal(payload.attachment.content.buttons[0].value) - expect(res.message.quick_replies[1].content_type).to.equal(payload.attachment.content.buttons[1].type) - expect(res.message.quick_replies[1].title).to.equal(payload.attachment.content.buttons[1].title) - expect(res.message.quick_replies[1].payload).to.equal(payload.attachment.content.buttons[1].value) - - }) - - it('should be Ok card', async () => { - - payload = { - attachment: { - type: 'card', - content: { - title: 'i am the title', - subtitle: "Soft white cotton t-shirt is back in style", - imageUrl: 'https://3.bp.blogspot.com/-W__wiaHUjwI/Vt3Grd8df0I/AAAAAAAAA78/7xqUNj8ujtY/s1600/image02.png', - itemUrl: 'https://www.google.fr/', - template_type: 'button', - buttons: [{title:'2',value:'https://www.google.fr/', type: 'web_url'},{type:'phone_number',title:'bruno',value:"+33675855738"}, {type:'element_share'}], - } - }, - } - const res = await invoke('messenger', 'formatMessage',['conversation',payload, opts]) - expect(res.recipient.id).to.equal(opts.senderId) - expect(res.message.attachment.type).to.equal('template') - expect(res.message.attachment.payload.template_type).to.equal('generic') - expect(res.message.attachment.payload.elements[0].image_url).to.equal(payload.attachment.content.imageUrl) - expect(res.message.attachment.payload.elements[0].item_url).to.equal(payload.attachment.content.itemUrl) - expect(res.message.attachment.payload.elements[0].subtitle).to.equal(payload.attachment.content.subtitle) - expect(res.message.attachment.payload.elements[0].buttons[0].title).to.equal(payload.attachment.content.buttons[0].title) - expect(res.message.attachment.payload.elements[0].buttons[0].url).to.equal(payload.attachment.content.buttons[0].value) - expect(res.message.attachment.payload.elements[0].buttons[0].type).to.equal(payload.attachment.content.buttons[0].type) - - expect(res.message.attachment.payload.elements[0].buttons[1].title).to.equal(payload.attachment.content.buttons[1].title) - expect(res.message.attachment.payload.elements[0].buttons[1].payload).to.equal(payload.attachment.content.buttons[1].value) - expect(res.message.attachment.payload.elements[0].buttons[1].type).to.equal(payload.attachment.content.buttons[1].type) - - expect(res.message.attachment.payload.elements[0].buttons[2].title).to.equal(payload.attachment.content.buttons[2].title) - }) -}) -}) diff --git a/test/services/Kik.service.tests.js b/test/services/Kik.service.tests.js deleted file mode 100644 index 54efb08..0000000 --- a/test/services/Kik.service.tests.js +++ /dev/null @@ -1,339 +0,0 @@ -import mongoose from 'mongoose' -import chai from 'chai' -import chaiHttp from 'chai-http' - -import KikService from '../../src/services/Kik.service' - -import { - invoke, - invokeSync, -} from '../../src/utils/index.js' - -chai.use(chaiHttp) -const expect = chai.expect -const should = chai.should() - -const opts = { - senderId:'774961692607582', - chatId:'913902005381557'} - - - let payload - let res - -describe('KikService', () => { - - describe('check checkSecurity', () => { - - it('should be Ok security check', async () => { - const req = { - headers: { - host: 'a02780b6.ngrok.io', - } - } - req.headers['x-kik-username'] = 'mybot' - const channel = { - userName: 'mybot', - webhook: 'https://a02780b6.ngrok.io', - } - const res = invokeSync('kik', 'checkSecurity',[req, channel]) - expect(res).to.equal(true) - }) - - it('should not be Ok security check', async () => { - const req = { - headers: { - host: 'a02780b6.ngrok.io', - } - } - req.headers['x-kik-username'] = 'qwer' - const channel = { - userName: 'mybot', - webhook: 'https://a02780b6.ngrok.io', - } - const res = invokeSync('kik', 'checkSecurity',[req, channel]) - expect(res).to.equal(false) - }) - - }) - - describe('Check all params are valide', () => { - it('should be Ok all parameter', async () => { - const channel = { - apiKey: '1234', - webhook: 'https://a02780b6.ngrok.io', - userName: 'user name', - } - invoke('kik', 'checkParamsValidity',[channel]).then(res => { - expect(res).to.equal(true) - }) - }) - it('should not be OK messing userName', async () => { - const channel = { - apiKey: '1234', - webhook: 'https://a02780b6.ngrok.io', - } - let err = null - try{ - const a = invokeSync('kik', 'checkParamsValidity',[channel]) - } catch(e) { err = e } finally { expect(err).exist } - }) - it('should not be OK messing webhhok', async () => { - const channel = { - apiKey: '1234', - userName: 'user name', - } - let err = null - try{ - const a = invokeSync('kik', 'checkParamsValidity',[channel]) - } catch(e) { err = e } finally { expect(err).exist } - }) - it('should not be OK messing apiKey', async () => { - const channel = { - webhook: '1234', - userName: 'user name', - } - let err = null - try{ - const a = invokeSync('kik', 'checkParamsValidity',[channel]) - } catch(e) { err = e } finally { expect(err).exist } - }) - }) - - describe('Check extaOptions', () => { - it('should be OK all option', async () => { - const req = { - body: {messages:[{chatId: '12341234', participants:['recast.ai']}]} - } - const a = invokeSync('kik', 'extractOptions',[req]) - expect(a.chatId).to.equal(req.body.messages[0].chatId) - expect(a.senderId).to.equal(req.body.messages[0].participants[0]) - }) - }) - - describe('Parsing message', () => { - it('should be Ok simple text', async ()=> { - payload = { - messages: [ - { - chatId: "0ee6d46753bfa6ac2f089149959363f3f59ae62b10cba89cc426490ce38ea92d", - id: "0115efde-e54b-43d5-873a-5fef7adc69fd", - type: "text", - from: "laura", - participants: ["laura"], - body: "omg r u real?", - timestamp: 1439576628405, - readReceiptRequested: true, - mention: null - } - ] - } - const res = await invoke('kik', 'parseChannelMessage',['conversation', payload, opts]) - expect(res[2].chatId).to.equal(opts.chatId) - expect(res[2].senderId).to.equal(opts.senderId) - expect(res[1].attachment.value).to.equal(payload.messages[0].body) - }) - - it('should be default when bad parameter', async ()=> { - payload = { - messages: [ - { - chatId: "0ee6d46753bfa6ac2f089149959363f3f59ae62b10cba89cc426490ce38ea92d", - id: "0115efde-e54b-43d5-873a-5fef7adc69fd", - type: "dblablalbl", - from: "laura", - participants: ["laura"], - body: "omg r u real?", - timestamp: 1439576628405, - readReceiptRequested: true, - mention: null - } - ] - } - const res = await invoke('kik', 'parseChannelMessage',['conversation', payload, opts]) - expect(res[2].chatId).to.equal(opts.chatId) - expect(res[2].senderId).to.equal(opts.senderId) - expect(res[1].attachment.value).to.equal('we don\'t handle this type') - }) - - it('should be Ok video', async() => { - payload = { - messages: [ - { - chatId: "b3be3bc15dbe59931666c06290abd944aaa769bb2ecaaf859bfb65678880afab", - type: "video", - from: "laura", - participants: ["laura"], - id: "6d8d060c-3ae4-46fc-bb18-6e7ba3182c0f", - timestamp: 1399303478832, - readReceiptRequested: true, - videoUrl: "http://example.kik.com/video.mp4", - mention: null - } - ] - } - const res = await invoke('kik', 'parseChannelMessage',['conversation', payload, opts]) - expect(res[2].chatId).to.equal(opts.chatId) - expect(res[2].senderId).to.equal(opts.senderId) - expect(res[1].attachment.value).to.equal(payload.messages[0].videoUrl) - }) - - it('should be Ok picture', async() => { - payload = { - messages: [ - { - chatId: "b3be3bc15dbe59931666c06290abd944aaa769bb2ecaaf859bfb65678880afab", - type: "picture", - from: "laura", - participants: ["laura"], - id: "6d8d060c-3ae4-46fc-bb18-6e7ba3182c0f", - picUrl: "http://example.kik.com/apicture.jpg", - timestamp: 1399303478832, - readReceiptRequested: true, - mention: null - } - ] - } - const res = await invoke('kik', 'parseChannelMessage',['conversation', payload, opts]) - expect(res[2].chatId).to.equal(opts.chatId) - expect(res[2].senderId).to.equal(opts.senderId) - expect(res[1].attachment.value).to.equal(payload.messages[0].picUrl) - }) - - it('should be Ok link', async() => { - payload = { - messages: [ - { - chatId: "b3be3bc15dbe59931666c06290abd944aaa769bb2ecaaf859bfb65678880afab", - type: "link", - from: "laura", - participants: ["laura"], - id: "6d8d060c-3ae4-46fc-bb18-6e7ba3182c0f", - timestamp: 83294238952, - url: "http://mywebpage.com", - noForward: true, - readReceiptRequested: true, - mention: null - } - ] - } - const res = await invoke('kik', 'parseChannelMessage',['conversation', payload, opts]) - expect(res[2].chatId).to.equal(opts.chatId) - expect(res[2].senderId).to.equal(opts.senderId) - expect(res[1].attachment.value).to.equal(payload.messages[0].url) - }) - - describe('Formatting message', () => { - it('should be Ok simple text', async() => { - payload = { - attachment: { - type: 'text', - content: 'Yo les loozers', - }, - } - const res = invokeSync('kik', 'formatMessage',['conversation', payload, opts]) - expect(res[0].chatId).to.equal(opts.chatId) - expect(res[0].to).to.equal(opts.senderId) - expect(res[0].type).to.equal(payload.attachment.type) - expect(res[0].body).to.equal(payload.attachment.content) - }) - - it('should be defaul whit wrong parmetter', async() => { - payload = { - attachment: { - type: 'qqwerreww', - content: 'Yo les loozers', - }, - } - const res = invokeSync('kik', 'formatMessage',['conversation', payload, opts]) - expect(res[0].chatId).to.equal(opts.chatId) - expect(res[0].to).to.equal(opts.senderId) - expect(res[0].type).to.equal('text') - expect(res[0].body).to.equal('wrong parameter') - }) - - - it('should be Ok picture', async () => { - payload = { - attachment: { - type: 'picture', - value: 'picurl', - } - } - const res = invokeSync('kik', 'formatMessage',['conversation', payload, opts]) - expect(res[0].chatId).to.equal(opts.chatId) - expect(res[0].to).to.equal(opts.senderId) - expect(res[0].type).to.equal(payload.attachment.type) - expect(res[0].body).to.equal(payload.attachment.content) - }) - - it('should be Ok video', async() => { - payload = { - attachment: { - type: 'video', - value: 'videourl', - }, - } - const res = invokeSync('kik', 'formatMessage',['conversation', payload, opts]) - expect(res[0].chatId).to.equal(opts.chatId) - expect(res[0].to).to.equal(opts.senderId) - expect(res[0].type).to.equal(payload.attachment.type) - expect(res[0].body).to.equal(payload.attachment.content) - }) - - it('should be Ok quickreplies', async() => { - payload = { - attachment: - { - type: 'quickReplies', - content: { - title: 'i am the title', - buttons: [{title:'1', value:'1'}, {title:'2',value:'2'},{title:'2',value:'2'},{title:'2',value:'2'},], - } - }, - } - const res = invokeSync('kik', 'formatMessage',['conversation', payload, opts]) - expect(res[0].chatId).to.equal(opts.chatId) - expect(res[0].to).to.equal(opts.senderId) - expect(res[0].type).to.equal('text') - expect(res[0].body).to.equal(payload.attachment.content.title) - expect(res[0].keyboards[0].type).to.equal('suggested') - expect(res[0].keyboards[0].responses[0].type).to.equal('text') - expect(res[0].keyboards[0].responses[0].body).to.equal(payload.attachment.content.buttons[0].value) - expect(res[0].keyboards[0].responses[1].type).to.equal('text') - expect(res[0].keyboards[0].responses[1].body).to.equal(payload.attachment.content.buttons[1].value) - expect(res[0].keyboards[0].responses[2].type).to.equal('text') - expect(res[0].keyboards[0].responses[2].body).to.equal(payload.attachment.content.buttons[2].value) - expect(res[0].keyboards[0].responses[3].type).to.equal('text') - expect(res[0].keyboards[0].responses[3].body).to.equal(payload.attachment.content.buttons[3].value) - }) - - it('should be Ok card', async() => { - payload = { - attachment: { - type: 'card', - content: { - title: 'hello', - imageUrl: 'imageUrl', - buttons: [{type: 'text', title:'hello'}], - }, - }, - } - - const res = invokeSync('kik', 'formatMessage',['conversation', payload, opts]) - expect(res[0].chatId).to.equal(opts.chatId) - expect(res[0].to).to.equal(opts.senderId) - expect(res[0].type).to.equal('text') - expect(res[0].body).to.equal(payload.attachment.content.title) - - expect(res[1].type).to.equal('picture') - expect(res[1].picUrl).to.equal(payload.attachment.content.imageUrl) - - expect(res[1].keyboards[0].type).to.equal('suggested') - expect(res[1].keyboards[0].type).to.equal('suggested') - expect(res[1].keyboards[0].responses[0].type).to.equal('text') - expect(res[1].keyboards[0].responses[0].body).to.equal(payload.attachment.content.buttons[0].title) - }) - }) - }) - }) diff --git a/test/services/Slack.service.tests.js b/test/services/Slack.service.tests.js deleted file mode 100644 index 9b1df12..0000000 --- a/test/services/Slack.service.tests.js +++ /dev/null @@ -1,403 +0,0 @@ -import chai from 'chai' -import sinon from 'sinon' -import { RtmClient } from '@slack/client' -import request from 'superagent' - -import Bot from '../../src/models/Bot.model' -import Channel from '../../src/models/Channel.model' -import Conversation from '../../src/models/Conversation.model' -import SlackService from '../../src/services/Slack.service' -import Logger from '../../src/utils/Logger' - -const expect = chai.expect - -let bot -let activeChannel -let inactiveChannel - -const url = 'http://localhost:8080' -const getChannelInfo = (slug, isActivated, bot) => { - return { - type: 'slack', - token: 'slack-token', - isActivated, - slug, - bot: bot._id, - } -} - -const textSlackMessage = { - text: 'Hello you!' -} - -const imageSlackMessage = { - file: { - mimetype: 'image/jpg', - url_private: 'some_image_url', - } -} - -const videoSlackMessage = { - file: { - mimetype: 'video/mp4', - url_private: 'some_video_url', - } -} - -const invalidSlackMessage = { - file: { - mimetype: 'application/json', - } -} - -const connectorTextMessage = { - attachment: { - type: 'text', - content: 'Hello you!' - } -} - -const connectorImageMessage = { - attachment: { - type: 'picture', - content: 'some_image_url' - } -} - -const connectorVideoMessage = { - attachment: { - type: 'video', - content: 'some_video_url' - } -} - -const connectorCardMessage = { - attachment: { - type: 'card', - content: { - title: 'A nice card', - imageUrl: 'some_image_url', - buttons: [{ - title: 'First button', - type: 'button', - value: 'First!', - }] - } - } -} - -const connectorQuickRepliesMessage = { - attachment: { - type: 'quickReplies', - content: { - title: 'A nice card', - buttons: [{ - title: 'First button', - value: 'First!', - }] - } - } -} - -const invalidConnectorMessage = { - attachment: { - type: 'invalid_type', - }, -} - - -describe('Slack service', () => { - before(async () => { - bot = await new Bot({ url, }).save() - await new Channel(getChannelInfo('slack-1', true, bot)).save() - activeChannel = await new Channel(getChannelInfo('slack-2', true, bot)).save() - inactiveChannel = await new Channel(getChannelInfo('slack-3', false, bot)).save() - await new Channel({ type: 'kik', isActivated: true, slug: 'kik-1', token: 'token-kik', bot: bot._id }).save() - }) - - after(async () => { - await Bot.remove({}) - await Channel.remove({}) - }) - - describe('checkParamsValidity', async () => { - it('should throw if not token is set', async () => { - const channel = await new Channel({ type: 'slack', slug: 'slug', isActivated: true , bot}).save() - let err = null - - try { - SlackService.checkParamsValidity(channel) - } catch (ex) { - err = ex - } - expect(err).not.to.equal(null) - - channel.token = 'token' - await channel.save() - err = null - let res = null - try { - res = SlackService.checkParamsValidity(channel) - } catch (ex) { - err = ex - } - expect(err).to.equal(null) - expect(res).to.equal(true) - await channel.remove() - }) - - describe('onLaunch', async () => { - it('should call onChannelCreate for each slack channels', async () => { - const stub = sinon.stub(SlackService, 'onChannelCreate', () => { true }) - await SlackService.onLaunch() - expect(SlackService.onChannelCreate.callCount).to.equal(3) - stub.restore() - }) - - it('should log and resolve on error', async () => { - sinon.spy(Logger, 'error') - const stub = sinon.stub(SlackService, 'onChannelCreate', () => { - throw new Error('error !') - }) - - await SlackService.onLaunch() - expect(Logger.error.calledOnce) - stub.restore() - Logger.error.restore() - }) - }) - - describe('onChannelCreate', async () => { - it('should return early is channel is not active', async () => { - sinon.spy(RtmClient, 'constructor') - SlackService.onChannelCreate(inactiveChannel) - expect(RtmClient.constructor.calledOnce).to.equal(false) - RtmClient.constructor.restore() - }) - - it('should start rtm connection if channel is active', async () => { - const startStub = sinon.stub(RtmClient.prototype, 'start', () => { true }) - const onStub = sinon.stub(RtmClient.prototype, 'on', () => { true }) - SlackService.onChannelCreate(activeChannel) - expect(RtmClient.constructor.calledOnce) - expect(RtmClient.prototype.on.calledOnce) - expect(RtmClient.prototype.start.calledOnce) - startStub.restore() - onStub.restore() - }) - - it('set rtm client in allRtm map', async () => { - const startStub = sinon.stub(RtmClient.prototype, 'start', () => { true }) - const onStub = sinon.stub(RtmClient.prototype, 'on', () => { true }) - SlackService.allRtm = new Map() - SlackService.onChannelCreate(activeChannel) - - expect(RtmClient.constructor.calledOnce) - expect(RtmClient.prototype.on.calledOnce) - expect(RtmClient.prototype.start.calledOnce) - - expect(SlackService.allRtm.get(activeChannel._id.toString())).to.exist - - startStub.restore() - onStub.restore() - }) - }) - - describe('onChannelDelete', async () => { - it('should close rtm connection and remove channel from allRtm', async () => { - const startStub = sinon.stub(RtmClient.prototype, 'start', () => { true }) - const onStub = sinon.stub(RtmClient.prototype, 'on', () => { true }) - const disconnectStub = sinon.stub(RtmClient.prototype, 'disconnect', () => { true }) - SlackService.allRtm = new Map() - SlackService.onChannelCreate(activeChannel) - - expect(RtmClient.constructor.calledOnce) - expect(RtmClient.prototype.on.calledOnce) - expect(RtmClient.prototype.start.calledOnce) - expect(SlackService.allRtm.get(activeChannel._id.toString())).to.exist - - SlackService.onChannelDelete(activeChannel) - expect(RtmClient.prototype.disconnect.calledOnce) - expect(SlackService.allRtm.get(activeChannel._id.toString())).not.to.exist - - startStub.restore() - onStub.restore() - disconnectStub.restore() - }) - - it('should do nothing is rtm connection is not active', async () => { - const disconnectStub = sinon.stub(RtmClient.prototype, 'disconnect', () => { true }) - SlackService.allRtm = new Map() - - SlackService.onChannelDelete(activeChannel) - expect(RtmClient.prototype.disconnect.calledOnce).to.equal(false) - - disconnectStub.restore() - }) - }) - - describe('onChannelUpdate', async () => { - it('should call onDelete and onCreate', async () => { - const deleteStub = sinon.stub(SlackService, 'onChannelDelete', () => { true }) - const createStub = sinon.stub(SlackService, 'onChannelCreate', () => { Promise.resolve(true) }) - SlackService.onChannelUpdate(activeChannel) - expect(deleteStub.calledOnce) - deleteStub.restore() - expect(createStub.calledOnce) - createStub.restore() - }) - }) - - describe('parseChannelMessage', async () => { - it('should parse text message correctly', async () => { - const conv = {}, opts = {} // This methd doesnt use conversation nor opts - - const [c, parsedMessage, opt] = await SlackService.parseChannelMessage(conv, textSlackMessage, opts) - expect(parsedMessage.channelType).to.equal('slack') - expect(parsedMessage.attachment.type).to.equal('text') - expect(parsedMessage.attachment.content).to.equal('Hello you!') - }) - - it('should parse image message correctly', async () => { - const conv = {}, opts = {} // This methd doesnt use conversation nor opts - - const [c, parsedMessage, opt] = await SlackService.parseChannelMessage(conv, imageSlackMessage, opts) - expect(parsedMessage.channelType).to.equal('slack') - expect(parsedMessage.attachment.type).to.equal('picture') - expect(parsedMessage.attachment.content).to.equal('some_image_url') - }) - - it('should parse video message correctly', async () => { - const conv = {}, opts = {} // This methd doesnt use conversation nor opts - - const [c, parsedMessage, opt] = await SlackService.parseChannelMessage(conv, videoSlackMessage, opts) - expect(parsedMessage.channelType).to.equal('slack') - expect(parsedMessage.attachment.type).to.equal('picture') - expect(parsedMessage.attachment.content).to.equal('some_video_url') - }) - - it('should throw error on other mimetype', async () => { - const conv = {}, opts = {} // This methd doesnt use conversation nor opts - - let err = null - try { - const [c, parsedMessage, opt] = await SlackService.parseChannelMessage(conv, invalidSlackMessage, opts) - } catch (ex) { - err = ex - } - expect(err).to.exist - expect(err.message).to.equal('Sorry but we don\'t handle such type of file') - }) - }) - - describe('parseMessage', async () => { - it('should format text messages correctly', () => { - const formattedMessage = SlackService.formatMessage({}, connectorTextMessage) - expect(formattedMessage.text).to.equal(connectorTextMessage.attachment.content) - }) - - it('should format image messages correctly', () => { - const formattedMessage = SlackService.formatMessage({}, connectorImageMessage) - expect(formattedMessage.text).to.equal(connectorImageMessage.attachment.content) - }) - - it('should format video messages correctly', () => { - const formattedMessage = SlackService.formatMessage({}, connectorVideoMessage) - expect(formattedMessage.text).to.equal(connectorVideoMessage.attachment.content) - }) - - it('should format card messages correctly', () => { - const formattedMessage = SlackService.formatMessage({}, connectorCardMessage) - const attach = formattedMessage.attachments[0] - expect(attach.fallback).to.equal('Sorry but I can\'t display buttons') - expect(attach.attachment_type).to.equal('default') - - expect(attach.actions[0].name).to.equal('First button') - expect(attach.actions[0].text).to.equal('First button') - expect(attach.actions[0].value).to.equal('First!') - expect(attach.actions[0].type).to.equal('button') - }) - - it('should format quickReplies messages correctly', () => { - const formattedMessage = SlackService.formatMessage({}, connectorQuickRepliesMessage) - const attach = formattedMessage.attachments[0] - expect(attach.fallback).to.equal('Sorry but I can\'t display buttons') - expect(attach.attachment_type).to.equal('default') - - expect(attach.actions[0].name).to.equal('First button') - expect(attach.actions[0].text).to.equal('First button') - expect(attach.actions[0].value).to.equal('First!') - expect(attach.actions[0].type).to.equal('button') - }) - - it('should throw with invalid message type', () => { - let err = null - - try { - SlackService.formatMessage({}, invalidConnectorMessage) - } catch (ex) { - err = ex - } - - expect(err).to.exist - expect(err.message).to.equal('Invalid message type') - }) - }) - - describe('sendMessage', async () => { - it('should make request to slack', async () => { - const formattedMessage = SlackService.formatMessage({}, connectorCardMessage) - const channel = activeChannel - const convers = await new Conversation({ chatId: 'chatId', channel: channel._id, bot: bot }).save() - convers.channel = channel - await convers.save() - - const expectedParams = `https://slack.com/api/chat.postMessage?token=${channel.token}&channel=${convers.chatId}&as_user=true&text=${formattedMessage.text}&attachments=${JSON.stringify(formattedMessage.attachments)}` - - const requestStub = sinon.stub(request, 'post', (url) => { - expect(url).to.equal(expectedParams) - // Fake superagent end method - return { end: (cb) => cb(null) } - }) - - const res = await SlackService.sendMessage(convers, formattedMessage) - - expect(res).to.equal('Message sent') - expect(requestStub.calledWith(expectedParams)) - - requestStub.restore() - }) - - it('should log and throw on error', async () => { - const formattedMessage = SlackService.formatMessage({}, connectorCardMessage) - const channel = activeChannel - const convers = await new Conversation({ chatId: 'chatId', channel: channel._id, bot: bot }).save() - convers.channel = channel - await convers.save() - - const expectedParams = `https://slack.com/api/chat.postMessage?token=${channel.token}&channel=${convers.chatId}&as_user=true&text=${formattedMessage.text}&attachments=${JSON.stringify(formattedMessage.attachments)}` - - const loggerStub = sinon.stub(Logger, 'error') - const requestStub = sinon.stub(request, 'post', (url) => { - expect(url).to.equal(expectedParams) - // Fake superagent end method - return { end: (cb) => cb(new Error('Fake error')) } - }) - - let err = null - try { - const res = await SlackService.sendMessage(convers, formattedMessage) - } catch (ex) { - err = ex - } - - expect(err).exist - expect(requestStub.calledWith(expectedParams)) - expect(loggerStub.calledOnce) - - requestStub.restore() - loggerStub.restore() - }) - }) - }) -}) diff --git a/test/services/fetchMethod.service.js b/test/services/fetchMethod.service.js deleted file mode 100644 index 6f614d9..0000000 --- a/test/services/fetchMethod.service.js +++ /dev/null @@ -1,22 +0,0 @@ -module.exports = (method, routePath) => { - const router = global.app._router.stack.filter(middleware => middleware.name === 'router') - - if (!router || router.length < 1) { - return null - } - - const routes = router[0].handle.stack.filter(route => route.route.methods[method.toLowerCase()] && routePath.match(route.regexp)) - - if (!routes || routes.length < 1) { - return null - } - - const stack = routes[0].route.stack - - if (!stack || stack.length < 1) { - return null - } - - // We take the last function, because the previous ones are middleware - return stack[stack.length - 1].handle -} diff --git a/test/tools/index.js b/test/tools/index.js new file mode 100644 index 0000000..0ae51fe --- /dev/null +++ b/test/tools/index.js @@ -0,0 +1,14 @@ +export function fetchMethod (routesArray, method, path) { + for (let route of routesArray) { + // If methods match + // and route.path is a string and match + // or route.path is an array and match + if (route.method.toLowerCase() === method.toLowerCase() && ((typeof route.path === 'string' && route.path === path) || (typeof route.path === 'object' && route.path.indexOf(path) >= 0))) { + return route.handler + } + } + + return null +} + +export { setupChannelIntegrationTests } from './integration_setup' diff --git a/test/tools/integration_setup.js b/test/tools/integration_setup.js new file mode 100644 index 0000000..0b6df4a --- /dev/null +++ b/test/tools/integration_setup.js @@ -0,0 +1,68 @@ +import { Channel, Connector } from '../../src/models' +import config from '../../config/test' +import nock from 'nock' +import superagentPromise from 'superagent-promise' +import superagent from 'superagent' +import _ from 'lodash' + +const agent = superagentPromise(superagent, Promise) +const connectorUrl = config.skillsBuilderUrl +async function newConnector (opts = {}) { + const connector = new Connector({ + url: opts.url || connectorUrl, + isActive: opts.isActive || true, + }) + return connector.save() +} + +export function setupChannelIntegrationTests (cleanupDB = true) { + + process.env.ROUTETEST = `http://localhost:${config.server.port}` + + afterEach(async () => { + if (cleanupDB) { + await Connector.remove() + await Channel.remove() + } + }) + + async function sendMessageToWebhook ( + channel, message, additionalHeaders = {}, connectorResponse + ) { + nock(connectorUrl) + .post('') + .reply(200, connectorResponse || { + messages: JSON.stringify([{ type: 'text', content: 'my message' }]), + }) + const baseHeaders = { + Accept: '*/*', + } + const headers = _.extend(baseHeaders, additionalHeaders) + try { + return await agent.post(channel.webhook).set(headers).send(message) + } catch (e) { + nock.cleanAll() + throw e + } + + } + + return { + sendMessageToWebhook, + createChannel: async (parameters) => { + const connector = await newConnector() + return agent.post(`${process.env.ROUTETEST}/v1/connectors/${connector._id}/channels`) + .send(parameters) + }, + deleteChannel: async (channel, headers = {}) => { + return agent.del(`${process.env.ROUTETEST}/v1/connectors/${channel.connector}/channels/${channel.slug}`) + .set(headers) + .send() + }, + updateChannel: async (channel, payload, headers = {}) => { + return agent.put(`${process.env.ROUTETEST}/v1/connectors/${channel.connector}/channels/${channel.slug}`) + .set(headers) + .send(payload) + }, + } +} diff --git a/test/util/message_queue.js b/test/util/message_queue.js new file mode 100644 index 0000000..2a25acd --- /dev/null +++ b/test/util/message_queue.js @@ -0,0 +1,96 @@ +import chai from 'chai' +import defaultMessageQueue from '../../src/utils/message_queue' +import { MessageQueue } from '../../src/utils/message_queue' + +const should = chai.should() +const expect = chai.expect + +describe('Message Queue', () => { + + let queue + beforeEach(() => { + queue = new MessageQueue() + }) + + const watcher = () => true + const watcher2 = () => false + const conversationId = 'abc' + + it('should initialize all necessary fields', () => { + should.exist(queue.queue) + expect(queue.pollWatchers).to.eql({}) + }) + + describe('setWatcher', () => { + it('should add a new watcher if none exists', () => { + const watcherId = '123' + queue.setWatcher(conversationId, watcherId, watcher) + expect(queue.pollWatchers).to.have.property(conversationId) + expect(queue.pollWatchers[conversationId]).to.have.property(watcherId) + expect(queue.pollWatchers[conversationId][watcherId].handler).to.equal(watcher) + }) + + it('should add a second watcher if one already exists', () => { + const firstWatcherId = '123' + const secondWatcherId = '456' + queue.setWatcher(conversationId, firstWatcherId, watcher) + queue.setWatcher(conversationId, secondWatcherId, watcher2) + expect(queue.pollWatchers[conversationId]).to.have.property(firstWatcherId) + expect(queue.pollWatchers[conversationId]).to.have.property(secondWatcherId) + }) + + it('should replace an existing watcher if IDs match', () => { + const watcherId = '123' + const secondWatcher = () => 'something else' + queue.setWatcher(conversationId, watcherId, watcher) + queue.setWatcher(conversationId, watcherId, secondWatcher) + expect(queue.pollWatchers[conversationId][watcherId].handler).to.equal(secondWatcher) + }) + }) + + describe('removeWatcher', () => { + it('should remove an existing watcher', () => { + const watcherId = '123' + queue.setWatcher(conversationId, watcherId, watcher) + queue.removeWatcher(conversationId, watcherId) + expect(queue.pollWatchers).not.to.have.property(conversationId) + }) + + it('should remove keep an existing watcher with same conversationId', () => { + const firstWatcherId = '123' + const secondWatcherId = '456' + queue.setWatcher(conversationId, firstWatcherId, watcher) + queue.setWatcher(conversationId, secondWatcherId, watcher2) + queue.removeWatcher(conversationId, firstWatcherId, watcher2) + expect(queue.pollWatchers).to.have.property(conversationId) + expect(queue.pollWatchers[conversationId]).not.to.have.property(firstWatcherId) + expect(queue.pollWatchers[conversationId]).to.have.property(secondWatcherId) + }) + + it('should not have any side effects for unknown IDs', () => { + const watcherId = '123' + queue.setWatcher(conversationId, watcherId, watcher) + queue.removeWatcher(conversationId, 'unknown') + queue.removeWatcher('unknown', 'unknown') + queue.removeWatcher('unknown', watcherId) + expect(queue.pollWatchers).to.have.property(conversationId) + expect(queue.pollWatchers[conversationId]).to.have.property(watcherId) + expect(queue.pollWatchers[conversationId][watcherId].handler).to.equal(watcher) + }) + + }) + + describe('default export', () => { + it('should ba an instance of MessageQueue', () => { + expect(defaultMessageQueue).to.be.an.instanceof(MessageQueue) + }) + + it('should be subscribed to all necessary events', () => { + expect(defaultMessageQueue).to.be.an.instanceof(MessageQueue) + expect(defaultMessageQueue.getQueue()._events).to.have.property('error') + expect(defaultMessageQueue.getQueue()._events).to.have.property('job enqueue') + }) + + }) + +}) diff --git a/test/util/start_application.js b/test/util/start_application.js new file mode 100644 index 0000000..853b817 --- /dev/null +++ b/test/util/start_application.js @@ -0,0 +1,13 @@ +import nock from 'nock' +import { startApplication } from '../../src/app' + +const config = require('../../config/test') +process.env.ROUTETEST = `http://localhost:${config.server.port}` + +before(async () => { + // nock all external request + nock.disableNetConnect() + nock.enableNetConnect(/(localhost)/) + + await startApplication() +}) diff --git a/test/validators/Bots.validators.tests.js b/test/validators/Bots.validators.tests.js deleted file mode 100644 index fd4ed85..0000000 --- a/test/validators/Bots.validators.tests.js +++ /dev/null @@ -1,95 +0,0 @@ -import chai from 'chai' -import chaiHttp from 'chai-http' -import mongoose from 'mongoose' - -import Bot from '../../src/models/Bot.model' - -const assert = require('chai').assert -const expect = chai.expect -const should = chai.should() - -chai.use(chaiHttp) - -const baseUrl = 'http://localhost:8080' - -describe('Bot validator', () => { - describe('createBot', () => { - it('should be a 400 with an invalid url', async () => { - try { - await chai.request(baseUrl).post('/bots').send() - should.fail() - } catch (err) { - const res = err.response - - assert.equal(res.status, 400) - assert.equal(res.body.message, 'Parameter url is invalid') - } - }) - }) - - describe('getBotById', () => { - it('should be a 400 with an invalid bot_id', async () => { - try { - await chai.request(baseUrl).get('/bots/12345').send() - should.fail() - } catch (err) { - const res = err.response - const { message, results } = res.body - - assert.equal(res.status, 400) - assert.equal(results, null) - assert.equal(message, 'Parameter bot_id is invalid') - } - }) - }) - - describe('updateBotById', () => { - let bot = {} - before(async () => bot = await new Bot({ url: 'https://test.com' })) - - it('should be a 400 with an invalid bot_id', async () => { - try { - await chai.request(baseUrl).put('/bots/12345').send() - should.fail() - } catch (err) { - const res = err.response - const { message, results } = res.body - - assert.equal(res.status, 400) - assert.equal(results, null) - assert.equal(message, 'Parameter bot_id is invalid') - } - }) - - it('should be a 400 with an invalid url', async () => { - try { - const res = await chai.request(baseUrl).put(`/bots/${bot._id}`).send({ url: 'invalid' }) - should.fail() - } catch (err) { - const res = err.response - const { message, results } = res.body - - assert.equal(res.status, 400) - assert.equal(results, null) - assert.equal(message, 'Parameter url is invalid') - } - }) - }) - - describe('deleteBotById', () => { - it('should be a 400 with an invalid bot_id', async () => { - try { - await chai.request(baseUrl).del('/bots/12345').send() - should.fail() - } catch (err) { - const res = err.response - const { message, results } = res.body - - assert.equal(res.status, 400) - assert.equal(results, null) - assert.equal(message, 'Parameter bot_id is invalid') - } - }) - }) -}) - diff --git a/test/validators/Channels.validators.tests.js b/test/validators/Channels.validators.tests.js deleted file mode 100644 index c440840..0000000 --- a/test/validators/Channels.validators.tests.js +++ /dev/null @@ -1,174 +0,0 @@ -import chai from 'chai' -import chaiHttp from 'chai-http' -import mongoose from 'mongoose' - -import Bot from '../../src/models/Bot.model' -import Channel from '../../src/models/Channel.model' - -const assert = require('chai').assert -const expect = chai.expect -const should = chai.should() - -chai.use(chaiHttp) - -const url = 'https://test.com' -const baseUrl = 'http://localhost:8080' -const payload = { - type: 'slack', - isActivated: true, - slug: 'test', -} - -describe('Channel validator', () => { - describe('createChannelByBotId', () => { - let bot = {} - before(async () => bot = await new Bot({ url })) - after(async () => await Bot.remove({})) - - it ('should be a 400 with an invalid bot_id', async () => { - try { - await chai.request(baseUrl).post('/bots/1234/channels').send() - should.fail() - } catch (err) { - const res = err.response - - assert.equal(res.status, 400) - assert.equal(res.body.message, 'Parameter bot_id is invalid') - } - }) - - it('should be a 400 with a missing type', async () => { - const payload = { isActivated: true, slug: 'slug-test' } - try { - await chai.request(baseUrl).post(`/bots/${bot._id}/channels`).send(payload) - should.fail() - } catch (err) { - const res = err.response - - assert.equal(res.status, 400) - assert.equal(res.body.message, 'Parameter type is missing') - } - }) - - it ('should be a 400 with an invalid type', async () => { - const payload = { type: 'invalid', isActivated: true, slug: 'slug-test' } - try { - await chai.request(baseUrl).post(`/bots/${bot._id}/channels`).send(payload) - should.fail() - } catch (err) { - const res = err.response - - assert.equal(res.status, 400) - assert.equal(res.body.message, 'Parameter type is invalid') - } - }) - - it ('should be a 400 with a missing isActivated', async () => { - const payload = { type: 'slack', slug: 'slug-test' } - try { - await chai.request(baseUrl).post(`/bots/${bot._id}/channels`).send(payload) - should.fail() - } catch (err) { - const res = err.response - - assert.equal(res.status, 400) - assert.equal(res.body.message, 'Parameter isActivated is missing') - } - }) - - it ('should be a 400 with a missing slug', async () => { - const payload = { type: 'slack', isActivated: true } - try { - await chai.request(baseUrl).post(`/bots/${bot._id}/channels`).send(payload) - should.fail() - } catch (err) { - const res = err.response - - assert.equal(res.status, 400) - assert.equal(res.body.message, 'Parameter slug is missing') - } - }) - - }) - - describe('getChannelsByBotId', () => { - it ('should be a 400 with an invalid bot_id', async () => { - try { - await chai.request(baseUrl).get('/bots/1234/channels').send() - should.fail() - } catch (err) { - const res = err.response - - assert.equal(res.status, 400) - assert.equal(res.body.message, 'Parameter bot_id is invalid') - } - }) - }) - - describe('getChannelByBotId', () => { - let bot = {} - before(async () => bot = await new Bot({ url }).save()) - after(async () => Bot.remove({})) - - it ('should be a 400 with an invalid bot_id', async () => { - try { - await chai.request(baseUrl).get('/bots/1234/channels/1234').send() - should.fail() - } catch (err) { - const res = err.response - - assert.equal(res.status, 400) - assert.equal(res.body.message, 'Parameter bot_id is invalid') - } - }) - }) - - describe('updateChannelByBotId', () => { - let bot = {} - let channel = {} - before(async () => { - bot = await new Bot({ url }).save() - channel = await new Channel({ ...payload, bot: bot._id }).save() - }) - after(async () => Promise.all([Bot.remove({}), Channel.remove({})])) - - it ('should be a 400 with an invalid bot_id', async () => { - try { - await chai.request(baseUrl).put(`/bots/1234/channels/${channel._slug}`).send() - should.fail() - } catch (err) { - const res = err.response - - assert.equal(res.status, 400) - assert.equal(res.body.message, 'Parameter bot_id is invalid') - } - }) - - it ('should be a 409 with an invalid bot_id', async () => { - try { - await new Channel({ ...payload, slug: 'test1', bot: bot._id }).save() - await chai.request(baseUrl).put(`/bots/${bot._id}/channels/${channel.slug}`).send({ slug: 'test1' }) - should.fail() - } catch (err) { - const res = err.response - - assert.equal(res.status, 409) - assert.equal(res.body.message, 'Channel slug already exists') - } - }) - }) - - describe('deleteChannelBotById', () => { - it ('should be a 400 with an invalid bot_id', async () => { - try { - await chai.request(baseUrl).del(`/bots/1234/channels/test`).send() - should.fail() - } catch (err) { - const res = err.response - - assert.equal(res.status, 400) - assert.equal(res.body.message, 'Parameter bot_id is invalid') - } - }) - }) -}) diff --git a/test/validators/Participants.validators.tests.js b/test/validators/Participants.validators.tests.js deleted file mode 100644 index 869e0db..0000000 --- a/test/validators/Participants.validators.tests.js +++ /dev/null @@ -1,68 +0,0 @@ -import chai from 'chai' -import chaiHttp from 'chai-http' -import mongoose from 'mongoose' - -import Bot from '../../src/models/Bot.model' - -const assert = require('chai').assert -const expect = chai.expect -const should = chai.should() - -chai.use(chaiHttp) - -const url = 'https://bonjour.com' -const baseUrl = 'http://localhost:8080' - -describe('Participant validator', () => { - describe('getParticipantsByBotId', () => { - it('should be a 400 with an invalid bot_id', async () => { - try { - await chai.request(baseUrl).get('/bots/12345/participants').send() - should.fail() - } catch (err) { - const res = err.response - const { message, results } = res.body - - assert.equal(res.status, 400) - assert.equal(results, null) - assert.equal(message, 'Parameter bot_id is invalid') - } - }) - }) - - describe('getParticipantByBotId', () => { - let bot = {} - before(async () => bot = await new Bot({ url }).save()) - after(async () => Bot.remove({})) - - it('should be a 400 with an invalid bot_id', async () => { - try { - await chai.request(baseUrl).get('/bots/1234/participants/test').send() - should.fail() - } catch (err) { - const res = err.response - const { message, results } = res.body - - assert.equal(res.status, 400) - assert.equal(results, null) - assert.equal(message, 'Parameter bot_id is invalid') - } - }) - - it('should be a 400 with an invalid participant_id', async () => { - try { - await chai.request(baseUrl).get(`/bots/${bot._id}/participants/test`).send() - should.fail() - } catch (err) { - const res = err.response - const { message, results } = res.body - - assert.equal(res.status, 400) - assert.equal(results, null) - assert.equal(message, 'Parameter participant_id is invalid') - } - }) - }) -}) - - diff --git a/test/validators/Webhooks.validators.tests.js b/test/validators/Webhooks.validators.tests.js deleted file mode 100644 index 8b5019a..0000000 --- a/test/validators/Webhooks.validators.tests.js +++ /dev/null @@ -1,47 +0,0 @@ -import chai from 'chai' -import chaiHttp from 'chai-http' -import mongoose from 'mongoose' - -const assert = require('chai').assert -const expect = chai.expect -const should = chai.should() - -chai.use(chaiHttp) - -const baseUrl = 'http://localhost:8080' - -describe('Webhook validator', () => { - describe('forwardMessage', () => { - it('should be a 400 with an invalid channel_id', async () => { - try { - await chai.request(baseUrl).post('/webhook/12345').send() - should.fail() - } catch (err) { - const res = err.response - const { message, results } = res.body - - assert.equal(res.status, 400) - assert.equal(results, null) - assert.equal(message, 'Parameter channel_id is invalid') - } - }) - }) - - describe('subscribeWebhook', () => { - it('should be a 400 with an invalid channel_id', async () => { - try { - await chai.request(baseUrl).get('/webhook/12345').send() - should.fail() - } catch (err) { - const res = err.response - const { message, results } = res.body - - assert.equal(res.status, 400) - assert.equal(results, null) - assert.equal(message, 'Parameter channel_id is invalid') - } - }) - }) -}) - - diff --git a/yarn.lock b/yarn.lock deleted file mode 100644 index ef30ac9..0000000 --- a/yarn.lock +++ /dev/null @@ -1,3705 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@slack/client@^3.6.0": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@slack/client/-/client-3.6.1.tgz#4af22459b8019b8a8d7620dd20a9d4c152003f0b" - dependencies: - async "^1.5.0" - bluebird "^3.3.3" - eventemitter3 "^1.1.1" - https-proxy-agent "^1.0.0" - inherits "^2.0.1" - lodash "^4.13.1" - request "^2.64.0" - retry "^0.9.0" - url-join "0.0.1" - winston "^2.1.1" - ws "^1.0.1" - -abbrev@1, abbrev@1.0.x: - version "1.0.9" - resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135" - -accepts@~1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.3.tgz#c3ca7434938648c3e0d9c1e328dd68b622c284ca" - dependencies: - mime-types "~2.1.11" - negotiator "0.6.1" - -acorn-jsx@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b" - dependencies: - acorn "^3.0.4" - -acorn@^3.0.4: - version "3.3.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" - -acorn@^4.0.1: - version "4.0.3" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.3.tgz#1a3e850b428e73ba6b09d1cc527f5aaad4d03ef1" - -agent-base@2: - version "2.0.1" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-2.0.1.tgz#bd8f9e86a8eb221fffa07bd14befd55df142815e" - dependencies: - extend "~3.0.0" - semver "~5.0.1" - -ajv-keywords@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.1.1.tgz#02550bc605a3e576041565628af972e06c549d50" - -ajv@^4.7.0: - version "4.9.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.9.0.tgz#5a358085747b134eb567d6d15e015f1d7802f45c" - dependencies: - co "^4.6.0" - json-stable-stringify "^1.0.1" - -align-text@^0.1.1, align-text@^0.1.3: - version "0.1.4" - resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117" - dependencies: - kind-of "^3.0.2" - longest "^1.0.1" - repeat-string "^1.5.2" - -amdefine@>=0.0.4: - version "1.0.1" - resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" - -ansi-escapes@^1.1.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e" - -ansi-regex@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.0.0.tgz#c5061b6e0ef8a81775e50f5d66151bf6bf371107" - -ansi-styles@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" - -ansi-styles@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.0.0.tgz#cb102df1c56f5123eab8b67cd7b98027a0279178" - -anymatch@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.0.tgz#a3e52fa39168c825ff57b0248126ce5a8ff95507" - dependencies: - arrify "^1.0.0" - micromatch "^2.1.5" - -apidoc-core@~0.7.1: - version "0.7.1" - resolved "https://registry.yarnpkg.com/apidoc-core/-/apidoc-core-0.7.1.tgz#e686dad66281e040e16635dd3f7cbab1c4bac724" - dependencies: - glob "^7.0.3" - iconv-lite "^0.4.13" - lodash "~4.11.1" - semver "~5.1.0" - wrench "~1.5.9" - -apidoc@^0.16.1: - version "0.16.1" - resolved "https://registry.yarnpkg.com/apidoc/-/apidoc-0.16.1.tgz#26a1cde4f2c4910936ad57adf41dbc83c246e5e9" - dependencies: - apidoc-core "~0.7.1" - fs-extra "~0.28.0" - lodash "~4.11.1" - markdown-it "^6.0.1" - nomnom "~1.8.1" - winston "~2.2.0" - -aproba@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.0.4.tgz#2713680775e7614c8ba186c065d4e2e52d1072c0" - -are-we-there-yet@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.2.tgz#80e470e95a084794fe1899262c5667c6e88de1b3" - dependencies: - delegates "^1.0.0" - readable-stream "^2.0.0 || ^1.1.13" - -argparse@^1.0.7: - version "1.0.9" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86" - dependencies: - sprintf-js "~1.0.2" - -arr-diff@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf" - dependencies: - arr-flatten "^1.0.1" - -arr-flatten@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.0.1.tgz#e5ffe54d45e19f32f216e91eb99c8ce892bb604b" - -array-flatten@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" - -array-union@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" - dependencies: - array-uniq "^1.0.1" - -array-uniq@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" - -array-unique@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" - -arrify@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" - -asn1@~0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" - -assert-plus@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" - -assert-plus@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" - -assertion-error@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.0.2.tgz#13ca515d86206da0bac66e834dd397d87581094c" - -async-each@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" - -async@1.x, async@^1.4.0, async@^1.5.0, async@^1.5.2: - version "1.5.2" - resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" - -async@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/async/-/async-2.1.2.tgz#612a4ab45ef42a70cde806bad86ee6db047e8385" - dependencies: - lodash "^4.14.0" - -async@~0.2.6: - version "0.2.10" - resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1" - -async@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/async/-/async-1.0.0.tgz#f8fc04ca3a13784ade9e1641af98578cfbd647a9" - -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - -aws-sign2@~0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" - -aws4@^1.2.1: - version "1.5.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.5.0.tgz#0a29ffb79c31c9e712eeb087e8e7a64b4a56d755" - -babel-cli@^6.16.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babel-cli/-/babel-cli-6.18.0.tgz#92117f341add9dead90f6fa7d0a97c0cc08ec186" - dependencies: - babel-core "^6.18.0" - babel-polyfill "^6.16.0" - babel-register "^6.18.0" - babel-runtime "^6.9.0" - commander "^2.8.1" - convert-source-map "^1.1.0" - fs-readdir-recursive "^1.0.0" - glob "^5.0.5" - lodash "^4.2.0" - output-file-sync "^1.1.0" - path-is-absolute "^1.0.0" - slash "^1.0.0" - source-map "^0.5.0" - v8flags "^2.0.10" - optionalDependencies: - chokidar "^1.0.0" - -babel-code-frame@^6.16.0: - version "6.16.0" - resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.16.0.tgz#f90e60da0862909d3ce098733b5d3987c97cb8de" - dependencies: - chalk "^1.1.0" - esutils "^2.0.2" - js-tokens "^2.0.0" - -babel-core@^6.18.0: - version "6.18.2" - resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.18.2.tgz#d8bb14dd6986fa4f3566a26ceda3964fa0e04e5b" - dependencies: - babel-code-frame "^6.16.0" - babel-generator "^6.18.0" - babel-helpers "^6.16.0" - babel-messages "^6.8.0" - babel-register "^6.18.0" - babel-runtime "^6.9.1" - babel-template "^6.16.0" - babel-traverse "^6.18.0" - babel-types "^6.18.0" - babylon "^6.11.0" - convert-source-map "^1.1.0" - debug "^2.1.1" - json5 "^0.5.0" - lodash "^4.2.0" - minimatch "^3.0.2" - path-is-absolute "^1.0.0" - private "^0.1.6" - slash "^1.0.0" - source-map "^0.5.0" - -babel-eslint@^7.0.0: - version "7.1.1" - resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-7.1.1.tgz#8a6a884f085aa7060af69cfc77341c2f99370fb2" - dependencies: - babel-code-frame "^6.16.0" - babel-traverse "^6.15.0" - babel-types "^6.15.0" - babylon "^6.13.0" - lodash.pickby "^4.6.0" - -babel-generator@^6.18.0: - version "6.19.0" - resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.19.0.tgz#9b2f244204777a3d6810ec127c673c87b349fac5" - dependencies: - babel-messages "^6.8.0" - babel-runtime "^6.9.0" - babel-types "^6.19.0" - detect-indent "^4.0.0" - jsesc "^1.3.0" - lodash "^4.2.0" - source-map "^0.5.0" - -babel-helper-bindify-decorators@^6.18.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babel-helper-bindify-decorators/-/babel-helper-bindify-decorators-6.18.0.tgz#fc00c573676a6e702fffa00019580892ec8780a5" - dependencies: - babel-runtime "^6.0.0" - babel-traverse "^6.18.0" - babel-types "^6.18.0" - -babel-helper-builder-binary-assignment-operator-visitor@^6.8.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.18.0.tgz#8ae814989f7a53682152e3401a04fabd0bb333a6" - dependencies: - babel-helper-explode-assignable-expression "^6.18.0" - babel-runtime "^6.0.0" - babel-types "^6.18.0" - -babel-helper-call-delegate@^6.18.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babel-helper-call-delegate/-/babel-helper-call-delegate-6.18.0.tgz#05b14aafa430884b034097ef29e9f067ea4133bd" - dependencies: - babel-helper-hoist-variables "^6.18.0" - babel-runtime "^6.0.0" - babel-traverse "^6.18.0" - babel-types "^6.18.0" - -babel-helper-define-map@^6.18.0, babel-helper-define-map@^6.8.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babel-helper-define-map/-/babel-helper-define-map-6.18.0.tgz#8d6c85dc7fbb4c19be3de40474d18e97c3676ec2" - dependencies: - babel-helper-function-name "^6.18.0" - babel-runtime "^6.9.0" - babel-types "^6.18.0" - lodash "^4.2.0" - -babel-helper-explode-assignable-expression@^6.18.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.18.0.tgz#14b8e8c2d03ad735d4b20f1840b24cd1f65239fe" - dependencies: - babel-runtime "^6.0.0" - babel-traverse "^6.18.0" - babel-types "^6.18.0" - -babel-helper-explode-class@^6.8.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babel-helper-explode-class/-/babel-helper-explode-class-6.18.0.tgz#c44f76f4fa23b9c5d607cbac5d4115e7a76f62cb" - dependencies: - babel-helper-bindify-decorators "^6.18.0" - babel-runtime "^6.0.0" - babel-traverse "^6.18.0" - babel-types "^6.18.0" - -babel-helper-function-name@^6.18.0, babel-helper-function-name@^6.5.0, babel-helper-function-name@^6.8.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.18.0.tgz#68ec71aeba1f3e28b2a6f0730190b754a9bf30e6" - dependencies: - babel-helper-get-function-arity "^6.18.0" - babel-runtime "^6.0.0" - babel-template "^6.8.0" - babel-traverse "^6.18.0" - babel-types "^6.18.0" - -babel-helper-get-function-arity@^6.18.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.18.0.tgz#a5b19695fd3f9cdfc328398b47dafcd7094f9f24" - dependencies: - babel-runtime "^6.0.0" - babel-types "^6.18.0" - -babel-helper-hoist-variables@^6.18.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.18.0.tgz#a835b5ab8b46d6de9babefae4d98ea41e866b82a" - dependencies: - babel-runtime "^6.0.0" - babel-types "^6.18.0" - -babel-helper-optimise-call-expression@^6.18.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.18.0.tgz#9261d0299ee1a4f08a6dd28b7b7c777348fd8f0f" - dependencies: - babel-runtime "^6.0.0" - babel-types "^6.18.0" - -babel-helper-regex@^6.8.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babel-helper-regex/-/babel-helper-regex-6.18.0.tgz#ae0ebfd77de86cb2f1af258e2cc20b5fe893ecc6" - dependencies: - babel-runtime "^6.9.0" - babel-types "^6.18.0" - lodash "^4.2.0" - -babel-helper-remap-async-to-generator@^6.16.0, babel-helper-remap-async-to-generator@^6.16.2: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.18.0.tgz#336cdf3cab650bb191b02fc16a3708e7be7f9ce5" - dependencies: - babel-helper-function-name "^6.18.0" - babel-runtime "^6.0.0" - babel-template "^6.16.0" - babel-traverse "^6.18.0" - babel-types "^6.18.0" - -babel-helper-replace-supers@^6.18.0, babel-helper-replace-supers@^6.8.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babel-helper-replace-supers/-/babel-helper-replace-supers-6.18.0.tgz#28ec69877be4144dbd64f4cc3a337e89f29a924e" - dependencies: - babel-helper-optimise-call-expression "^6.18.0" - babel-messages "^6.8.0" - babel-runtime "^6.0.0" - babel-template "^6.16.0" - babel-traverse "^6.18.0" - babel-types "^6.18.0" - -babel-helpers@^6.16.0: - version "6.16.0" - resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.16.0.tgz#1095ec10d99279460553e67eb3eee9973d3867e3" - dependencies: - babel-runtime "^6.0.0" - babel-template "^6.16.0" - -babel-messages@^6.8.0: - version "6.8.0" - resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.8.0.tgz#bf504736ca967e6d65ef0adb5a2a5f947c8e0eb9" - dependencies: - babel-runtime "^6.0.0" - -babel-plugin-check-es2015-constants@^6.3.13: - version "6.8.0" - resolved "https://registry.yarnpkg.com/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.8.0.tgz#dbf024c32ed37bfda8dee1e76da02386a8d26fe7" - dependencies: - babel-runtime "^6.0.0" - -babel-plugin-coverage@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/babel-plugin-coverage/-/babel-plugin-coverage-1.0.0.tgz#ef008e238b6926cb837bf9692a4e645a6fd13a8c" - dependencies: - babel-helper-function-name "^6.5.0" - babel-template "^6.8.0" - -babel-plugin-syntax-async-functions@^6.8.0: - version "6.13.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz#cad9cad1191b5ad634bf30ae0872391e0647be95" - -babel-plugin-syntax-async-generators@^6.5.0: - version "6.13.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-generators/-/babel-plugin-syntax-async-generators-6.13.0.tgz#6bc963ebb16eccbae6b92b596eb7f35c342a8b9a" - -babel-plugin-syntax-class-constructor-call@^6.18.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-class-constructor-call/-/babel-plugin-syntax-class-constructor-call-6.18.0.tgz#9cb9d39fe43c8600bec8146456ddcbd4e1a76416" - -babel-plugin-syntax-class-properties@^6.8.0: - version "6.13.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz#d7eb23b79a317f8543962c505b827c7d6cac27de" - -babel-plugin-syntax-decorators@^6.13.0: - version "6.13.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-decorators/-/babel-plugin-syntax-decorators-6.13.0.tgz#312563b4dbde3cc806cee3e416cceeaddd11ac0b" - -babel-plugin-syntax-do-expressions@^6.8.0: - version "6.13.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-do-expressions/-/babel-plugin-syntax-do-expressions-6.13.0.tgz#5747756139aa26d390d09410b03744ba07e4796d" - -babel-plugin-syntax-dynamic-import@^6.18.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz#8d6a26229c83745a9982a441051572caa179b1da" - -babel-plugin-syntax-exponentiation-operator@^6.8.0: - version "6.13.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de" - -babel-plugin-syntax-export-extensions@^6.8.0: - version "6.13.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-export-extensions/-/babel-plugin-syntax-export-extensions-6.13.0.tgz#70a1484f0f9089a4e84ad44bac353c95b9b12721" - -babel-plugin-syntax-function-bind@^6.8.0: - version "6.13.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-function-bind/-/babel-plugin-syntax-function-bind-6.13.0.tgz#48c495f177bdf31a981e732f55adc0bdd2601f46" - -babel-plugin-syntax-object-rest-spread@^6.8.0: - version "6.13.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5" - -babel-plugin-syntax-trailing-function-commas@^6.3.13: - version "6.13.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.13.0.tgz#2b84b7d53dd744f94ff1fad7669406274b23f541" - -babel-plugin-transform-async-generator-functions@^6.17.0: - version "6.17.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-generator-functions/-/babel-plugin-transform-async-generator-functions-6.17.0.tgz#d0b5a2b2f0940f2b245fa20a00519ed7bc6cae54" - dependencies: - babel-helper-remap-async-to-generator "^6.16.2" - babel-plugin-syntax-async-generators "^6.5.0" - babel-runtime "^6.0.0" - -babel-plugin-transform-async-to-generator@^6.16.0: - version "6.16.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.16.0.tgz#19ec36cb1486b59f9f468adfa42ce13908ca2999" - dependencies: - babel-helper-remap-async-to-generator "^6.16.0" - babel-plugin-syntax-async-functions "^6.8.0" - babel-runtime "^6.0.0" - -babel-plugin-transform-class-constructor-call@^6.3.13: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-class-constructor-call/-/babel-plugin-transform-class-constructor-call-6.18.0.tgz#80855e38a1ab47b8c6c647f8ea1bcd2c00ca3aae" - dependencies: - babel-plugin-syntax-class-constructor-call "^6.18.0" - babel-runtime "^6.0.0" - babel-template "^6.8.0" - -babel-plugin-transform-class-properties@^6.18.0: - version "6.19.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.19.0.tgz#1274b349abaadc835164e2004f4a2444a2788d5f" - dependencies: - babel-helper-function-name "^6.18.0" - babel-plugin-syntax-class-properties "^6.8.0" - babel-runtime "^6.9.1" - babel-template "^6.15.0" - -babel-plugin-transform-decorators@^6.13.0: - version "6.13.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-decorators/-/babel-plugin-transform-decorators-6.13.0.tgz#82d65c1470ae83e2d13eebecb0a1c2476d62da9d" - dependencies: - babel-helper-define-map "^6.8.0" - babel-helper-explode-class "^6.8.0" - babel-plugin-syntax-decorators "^6.13.0" - babel-runtime "^6.0.0" - babel-template "^6.8.0" - babel-types "^6.13.0" - -babel-plugin-transform-do-expressions@^6.3.13: - version "6.8.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-do-expressions/-/babel-plugin-transform-do-expressions-6.8.0.tgz#fda692af339835cc255bb7544efb8f7c1306c273" - dependencies: - babel-plugin-syntax-do-expressions "^6.8.0" - babel-runtime "^6.0.0" - -babel-plugin-transform-es2015-arrow-functions@^6.3.13: - version "6.8.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.8.0.tgz#5b63afc3181bdc9a8c4d481b5a4f3f7d7fef3d9d" - dependencies: - babel-runtime "^6.0.0" - -babel-plugin-transform-es2015-block-scoped-functions@^6.3.13: - version "6.8.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.8.0.tgz#ed95d629c4b5a71ae29682b998f70d9833eb366d" - dependencies: - babel-runtime "^6.0.0" - -babel-plugin-transform-es2015-block-scoping@^6.18.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.18.0.tgz#3bfdcfec318d46df22525cdea88f1978813653af" - dependencies: - babel-runtime "^6.9.0" - babel-template "^6.15.0" - babel-traverse "^6.18.0" - babel-types "^6.18.0" - lodash "^4.2.0" - -babel-plugin-transform-es2015-classes@^6.18.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.18.0.tgz#ffe7a17321bf83e494dcda0ae3fc72df48ffd1d9" - dependencies: - babel-helper-define-map "^6.18.0" - babel-helper-function-name "^6.18.0" - babel-helper-optimise-call-expression "^6.18.0" - babel-helper-replace-supers "^6.18.0" - babel-messages "^6.8.0" - babel-runtime "^6.9.0" - babel-template "^6.14.0" - babel-traverse "^6.18.0" - babel-types "^6.18.0" - -babel-plugin-transform-es2015-computed-properties@^6.3.13: - version "6.8.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.8.0.tgz#f51010fd61b3bd7b6b60a5fdfd307bb7a5279870" - dependencies: - babel-helper-define-map "^6.8.0" - babel-runtime "^6.0.0" - babel-template "^6.8.0" - -babel-plugin-transform-es2015-destructuring@^6.18.0: - version "6.19.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.19.0.tgz#ff1d911c4b3f4cab621bd66702a869acd1900533" - dependencies: - babel-runtime "^6.9.0" - -babel-plugin-transform-es2015-duplicate-keys@^6.6.0: - version "6.8.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.8.0.tgz#fd8f7f7171fc108cc1c70c3164b9f15a81c25f7d" - dependencies: - babel-runtime "^6.0.0" - babel-types "^6.8.0" - -babel-plugin-transform-es2015-for-of@^6.18.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.18.0.tgz#4c517504db64bf8cfc119a6b8f177211f2028a70" - dependencies: - babel-runtime "^6.0.0" - -babel-plugin-transform-es2015-function-name@^6.9.0: - version "6.9.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.9.0.tgz#8c135b17dbd064e5bba56ec511baaee2fca82719" - dependencies: - babel-helper-function-name "^6.8.0" - babel-runtime "^6.9.0" - babel-types "^6.9.0" - -babel-plugin-transform-es2015-literals@^6.3.13: - version "6.8.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.8.0.tgz#50aa2e5c7958fc2ab25d74ec117e0cc98f046468" - dependencies: - babel-runtime "^6.0.0" - -babel-plugin-transform-es2015-modules-amd@^6.18.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.18.0.tgz#49a054cbb762bdf9ae2d8a807076cfade6141e40" - dependencies: - babel-plugin-transform-es2015-modules-commonjs "^6.18.0" - babel-runtime "^6.0.0" - babel-template "^6.8.0" - -babel-plugin-transform-es2015-modules-commonjs@^6.18.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.18.0.tgz#c15ae5bb11b32a0abdcc98a5837baa4ee8d67bcc" - dependencies: - babel-plugin-transform-strict-mode "^6.18.0" - babel-runtime "^6.0.0" - babel-template "^6.16.0" - babel-types "^6.18.0" - -babel-plugin-transform-es2015-modules-systemjs@^6.18.0: - version "6.19.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.19.0.tgz#50438136eba74527efa00a5b0fefaf1dc4071da6" - dependencies: - babel-helper-hoist-variables "^6.18.0" - babel-runtime "^6.11.6" - babel-template "^6.14.0" - -babel-plugin-transform-es2015-modules-umd@^6.18.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.18.0.tgz#23351770ece5c1f8e83ed67cb1d7992884491e50" - dependencies: - babel-plugin-transform-es2015-modules-amd "^6.18.0" - babel-runtime "^6.0.0" - babel-template "^6.8.0" - -babel-plugin-transform-es2015-object-super@^6.3.13: - version "6.8.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.8.0.tgz#1b858740a5a4400887c23dcff6f4d56eea4a24c5" - dependencies: - babel-helper-replace-supers "^6.8.0" - babel-runtime "^6.0.0" - -babel-plugin-transform-es2015-parameters@^6.18.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.18.0.tgz#9b2cfe238c549f1635ba27fc1daa858be70608b1" - dependencies: - babel-helper-call-delegate "^6.18.0" - babel-helper-get-function-arity "^6.18.0" - babel-runtime "^6.9.0" - babel-template "^6.16.0" - babel-traverse "^6.18.0" - babel-types "^6.18.0" - -babel-plugin-transform-es2015-shorthand-properties@^6.18.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.18.0.tgz#e2ede3b7df47bf980151926534d1dd0cbea58f43" - dependencies: - babel-runtime "^6.0.0" - babel-types "^6.18.0" - -babel-plugin-transform-es2015-spread@^6.3.13: - version "6.8.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.8.0.tgz#0217f737e3b821fa5a669f187c6ed59205f05e9c" - dependencies: - babel-runtime "^6.0.0" - -babel-plugin-transform-es2015-sticky-regex@^6.3.13: - version "6.8.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.8.0.tgz#e73d300a440a35d5c64f5c2a344dc236e3df47be" - dependencies: - babel-helper-regex "^6.8.0" - babel-runtime "^6.0.0" - babel-types "^6.8.0" - -babel-plugin-transform-es2015-template-literals@^6.6.0: - version "6.8.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.8.0.tgz#86eb876d0a2c635da4ec048b4f7de9dfc897e66b" - dependencies: - babel-runtime "^6.0.0" - -babel-plugin-transform-es2015-typeof-symbol@^6.18.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.18.0.tgz#0b14c48629c90ff47a0650077f6aa699bee35798" - dependencies: - babel-runtime "^6.0.0" - -babel-plugin-transform-es2015-unicode-regex@^6.3.13: - version "6.11.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.11.0.tgz#6298ceabaad88d50a3f4f392d8de997260f6ef2c" - dependencies: - babel-helper-regex "^6.8.0" - babel-runtime "^6.0.0" - regexpu-core "^2.0.0" - -babel-plugin-transform-exponentiation-operator@^6.3.13: - version "6.8.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.8.0.tgz#db25742e9339eade676ca9acec46f955599a68a4" - dependencies: - babel-helper-builder-binary-assignment-operator-visitor "^6.8.0" - babel-plugin-syntax-exponentiation-operator "^6.8.0" - babel-runtime "^6.0.0" - -babel-plugin-transform-export-extensions@^6.3.13: - version "6.8.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-export-extensions/-/babel-plugin-transform-export-extensions-6.8.0.tgz#fa80ff655b636549431bfd38f6b817bd82e47f5b" - dependencies: - babel-plugin-syntax-export-extensions "^6.8.0" - babel-runtime "^6.0.0" - -babel-plugin-transform-function-bind@^6.3.13: - version "6.8.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-function-bind/-/babel-plugin-transform-function-bind-6.8.0.tgz#e7f334ce69f50d28fe850a822eaaab9fa4f4d821" - dependencies: - babel-plugin-syntax-function-bind "^6.8.0" - babel-runtime "^6.0.0" - -babel-plugin-transform-object-rest-spread@^6.16.0: - version "6.19.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.19.0.tgz#f6ac428ee3cb4c6aa00943ed1422ce813603b34c" - dependencies: - babel-plugin-syntax-object-rest-spread "^6.8.0" - babel-runtime "^6.0.0" - -babel-plugin-transform-regenerator@^6.16.0: - version "6.16.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.16.1.tgz#a75de6b048a14154aae14b0122756c5bed392f59" - dependencies: - babel-runtime "^6.9.0" - babel-types "^6.16.0" - private "~0.1.5" - -babel-plugin-transform-runtime@^6.15.0: - version "6.15.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-runtime/-/babel-plugin-transform-runtime-6.15.0.tgz#3d75b4d949ad81af157570273846fb59aeb0d57c" - dependencies: - babel-runtime "^6.9.0" - -babel-plugin-transform-strict-mode@^6.18.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.18.0.tgz#df7cf2991fe046f44163dcd110d5ca43bc652b9d" - dependencies: - babel-runtime "^6.0.0" - babel-types "^6.18.0" - -babel-polyfill@^6.16.0: - version "6.16.0" - resolved "https://registry.yarnpkg.com/babel-polyfill/-/babel-polyfill-6.16.0.tgz#2d45021df87e26a374b6d4d1a9c65964d17f2422" - dependencies: - babel-runtime "^6.9.1" - core-js "^2.4.0" - regenerator-runtime "^0.9.5" - -babel-preset-es2015@^6.16.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babel-preset-es2015/-/babel-preset-es2015-6.18.0.tgz#b8c70df84ec948c43dcf2bf770e988eb7da88312" - dependencies: - babel-plugin-check-es2015-constants "^6.3.13" - babel-plugin-transform-es2015-arrow-functions "^6.3.13" - babel-plugin-transform-es2015-block-scoped-functions "^6.3.13" - babel-plugin-transform-es2015-block-scoping "^6.18.0" - babel-plugin-transform-es2015-classes "^6.18.0" - babel-plugin-transform-es2015-computed-properties "^6.3.13" - babel-plugin-transform-es2015-destructuring "^6.18.0" - babel-plugin-transform-es2015-duplicate-keys "^6.6.0" - babel-plugin-transform-es2015-for-of "^6.18.0" - babel-plugin-transform-es2015-function-name "^6.9.0" - babel-plugin-transform-es2015-literals "^6.3.13" - babel-plugin-transform-es2015-modules-amd "^6.18.0" - babel-plugin-transform-es2015-modules-commonjs "^6.18.0" - babel-plugin-transform-es2015-modules-systemjs "^6.18.0" - babel-plugin-transform-es2015-modules-umd "^6.18.0" - babel-plugin-transform-es2015-object-super "^6.3.13" - babel-plugin-transform-es2015-parameters "^6.18.0" - babel-plugin-transform-es2015-shorthand-properties "^6.18.0" - babel-plugin-transform-es2015-spread "^6.3.13" - babel-plugin-transform-es2015-sticky-regex "^6.3.13" - babel-plugin-transform-es2015-template-literals "^6.6.0" - babel-plugin-transform-es2015-typeof-symbol "^6.18.0" - babel-plugin-transform-es2015-unicode-regex "^6.3.13" - babel-plugin-transform-regenerator "^6.16.0" - -babel-preset-stage-0@^6.16.0: - version "6.16.0" - resolved "https://registry.yarnpkg.com/babel-preset-stage-0/-/babel-preset-stage-0-6.16.0.tgz#f5a263c420532fd57491f1a7315b3036e428f823" - dependencies: - babel-plugin-transform-do-expressions "^6.3.13" - babel-plugin-transform-function-bind "^6.3.13" - babel-preset-stage-1 "^6.16.0" - -babel-preset-stage-1@^6.16.0: - version "6.16.0" - resolved "https://registry.yarnpkg.com/babel-preset-stage-1/-/babel-preset-stage-1-6.16.0.tgz#9d31fbbdae7b17c549fd3ac93e3cf6902695e479" - dependencies: - babel-plugin-transform-class-constructor-call "^6.3.13" - babel-plugin-transform-export-extensions "^6.3.13" - babel-preset-stage-2 "^6.16.0" - -babel-preset-stage-2@^6.16.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babel-preset-stage-2/-/babel-preset-stage-2-6.18.0.tgz#9eb7bf9a8e91c68260d5ba7500493caaada4b5b5" - dependencies: - babel-plugin-syntax-dynamic-import "^6.18.0" - babel-plugin-transform-class-properties "^6.18.0" - babel-plugin-transform-decorators "^6.13.0" - babel-preset-stage-3 "^6.17.0" - -babel-preset-stage-3@^6.17.0: - version "6.17.0" - resolved "https://registry.yarnpkg.com/babel-preset-stage-3/-/babel-preset-stage-3-6.17.0.tgz#b6638e46db6e91e3f889013d8ce143917c685e39" - dependencies: - babel-plugin-syntax-trailing-function-commas "^6.3.13" - babel-plugin-transform-async-generator-functions "^6.17.0" - babel-plugin-transform-async-to-generator "^6.16.0" - babel-plugin-transform-exponentiation-operator "^6.3.13" - babel-plugin-transform-object-rest-spread "^6.16.0" - -babel-register@^6.18.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.18.0.tgz#892e2e03865078dd90ad2c715111ec4449b32a68" - dependencies: - babel-core "^6.18.0" - babel-runtime "^6.11.6" - core-js "^2.4.0" - home-or-tmp "^2.0.0" - lodash "^4.2.0" - mkdirp "^0.5.1" - source-map-support "^0.4.2" - -babel-runtime@^6.0.0, babel-runtime@^6.11.6, babel-runtime@^6.9.0, babel-runtime@^6.9.1: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.18.0.tgz#0f4177ffd98492ef13b9f823e9994a02584c9078" - dependencies: - core-js "^2.4.0" - regenerator-runtime "^0.9.5" - -babel-template@^6.14.0, babel-template@^6.15.0, babel-template@^6.16.0, babel-template@^6.8.0: - version "6.16.0" - resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.16.0.tgz#e149dd1a9f03a35f817ddbc4d0481988e7ebc8ca" - dependencies: - babel-runtime "^6.9.0" - babel-traverse "^6.16.0" - babel-types "^6.16.0" - babylon "^6.11.0" - lodash "^4.2.0" - -babel-traverse@^6.15.0, babel-traverse@^6.16.0, babel-traverse@^6.18.0: - version "6.19.0" - resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.19.0.tgz#68363fb821e26247d52a519a84b2ceab8df4f55a" - dependencies: - babel-code-frame "^6.16.0" - babel-messages "^6.8.0" - babel-runtime "^6.9.0" - babel-types "^6.19.0" - babylon "^6.11.0" - debug "^2.2.0" - globals "^9.0.0" - invariant "^2.2.0" - lodash "^4.2.0" - -babel-types@^6.13.0, babel-types@^6.15.0, babel-types@^6.16.0, babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.8.0, babel-types@^6.9.0: - version "6.19.0" - resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.19.0.tgz#8db2972dbed01f1192a8b602ba1e1e4c516240b9" - dependencies: - babel-runtime "^6.9.1" - esutils "^2.0.2" - lodash "^4.2.0" - to-fast-properties "^1.0.1" - -babylon@^6.11.0, babylon@^6.13.0: - version "6.14.1" - resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.14.1.tgz#956275fab72753ad9b3435d7afe58f8bf0a29815" - -balanced-match@^0.4.1: - version "0.4.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" - -bcrypt-pbkdf@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.0.tgz#3ca76b85241c7170bf7d9703e7b9aa74630040d4" - dependencies: - tweetnacl "^0.14.3" - -binary-extensions@^1.0.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.7.0.tgz#6c1610db163abfb34edfe42fa423343a1e01185d" - -block-stream@*: - version "0.0.9" - resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" - dependencies: - inherits "~2.0.0" - -bluebird@2.10.2: - version "2.10.2" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.10.2.tgz#024a5517295308857f14f91f1106fc3b555f446b" - -bluebird@^3.3.3: - version "3.4.6" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.6.tgz#01da8d821d87813d158967e743d5fe6c62cf8c0f" - -blueimp-md5@^2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/blueimp-md5/-/blueimp-md5-2.5.0.tgz#9766c8976b1141289a9b2210f313dea201f123cb" - -body-parser@^1.15.2: - version "1.15.2" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.15.2.tgz#d7578cf4f1d11d5f6ea804cef35dc7a7ff6dae67" - dependencies: - bytes "2.4.0" - content-type "~1.0.2" - debug "~2.2.0" - depd "~1.1.0" - http-errors "~1.5.0" - iconv-lite "0.4.13" - on-finished "~2.3.0" - qs "6.2.0" - raw-body "~2.1.7" - type-is "~1.6.13" - -boom@2.x.x: - version "2.10.1" - resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f" - dependencies: - hoek "2.x.x" - -brace-expansion@^1.0.0: - version "1.1.6" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.6.tgz#7197d7eaa9b87e648390ea61fc66c84427420df9" - dependencies: - balanced-match "^0.4.1" - concat-map "0.0.1" - -braces@^1.8.2: - version "1.8.5" - resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7" - dependencies: - expand-range "^1.8.1" - preserve "^0.2.0" - repeat-element "^1.1.2" - -browser-stdout@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.0.tgz#f351d32969d32fa5d7a5567154263d928ae3bd1f" - -bson@~0.5.4, bson@~0.5.6: - version "0.5.7" - resolved "https://registry.yarnpkg.com/bson/-/bson-0.5.7.tgz#0d11fe0936c1fee029e11f7063f5d0ab2422ea3e" - -buffer-shims@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/buffer-shims/-/buffer-shims-1.0.0.tgz#9978ce317388c649ad8793028c3477ef044a8b51" - -bytes@2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.4.0.tgz#7d97196f9d5baf7f6935e25985549edd2a6c2339" - -caller-id@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/caller-id/-/caller-id-0.1.0.tgz#59bdac0893d12c3871408279231f97458364f07b" - dependencies: - stack-trace "~0.0.7" - -caller-path@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f" - dependencies: - callsites "^0.2.0" - -callr@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/callr/-/callr-1.0.0.tgz#70c0b7c61198259523eb4e30f2c8afa6a369b888" - -callsites@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca" - -camelcase@^1.0.2: - version "1.2.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39" - -caseless@~0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.11.0.tgz#715b96ea9841593cc33067923f5ec60ebda4f7d7" - -center-align@^0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad" - dependencies: - align-text "^0.1.3" - lazy-cache "^1.0.3" - -chai-http@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/chai-http/-/chai-http-3.0.0.tgz#5460d8036e1f1a12b0b5b5cbd529e6dc1d31eb4b" - dependencies: - cookiejar "2.0.x" - is-ip "1.0.0" - methods "^1.1.2" - qs "^6.2.0" - superagent "^2.0.0" - -chai-spies@^0.7.1: - version "0.7.1" - resolved "https://registry.yarnpkg.com/chai-spies/-/chai-spies-0.7.1.tgz#343d99f51244212e8b17e64b93996ff7b2c2a9b1" - -"chai@>=1.9.2 <4.0.0", chai@^3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/chai/-/chai-3.5.0.tgz#4d02637b067fe958bdbfdd3a40ec56fef7373247" - dependencies: - assertion-error "^1.0.1" - deep-eql "^0.1.3" - type-detect "^1.0.0" - -chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" - dependencies: - ansi-styles "^2.2.1" - escape-string-regexp "^1.0.2" - has-ansi "^2.0.0" - strip-ansi "^3.0.0" - supports-color "^2.0.0" - -chalk@~0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.4.0.tgz#5199a3ddcd0c1efe23bc08c1b027b06176e0c64f" - dependencies: - ansi-styles "~1.0.0" - has-color "~0.1.0" - strip-ansi "~0.1.0" - -chokidar@^1.0.0, chokidar@^1.4.3: - version "1.6.1" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.6.1.tgz#2f4447ab5e96e50fb3d789fd90d4c72e0e4c70c2" - dependencies: - anymatch "^1.3.0" - async-each "^1.0.0" - glob-parent "^2.0.0" - inherits "^2.0.1" - is-binary-path "^1.0.0" - is-glob "^2.0.0" - path-is-absolute "^1.0.0" - readdirp "^2.0.0" - optionalDependencies: - fsevents "^1.0.0" - -circular-json@^0.3.0: - version "0.3.1" - resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.1.tgz#be8b36aefccde8b3ca7aa2d6afc07a37242c0d2d" - -cli-cursor@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987" - dependencies: - restore-cursor "^1.0.1" - -cli-width@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.1.0.tgz#b234ca209b29ef66fc518d9b98d5847b00edf00a" - -cliui@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1" - dependencies: - center-align "^0.1.1" - right-align "^0.1.1" - wordwrap "0.0.2" - -co@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" - -code-point-at@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" - -colors@1.0.x: - version "1.0.3" - resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" - -combined-stream@^1.0.5, combined-stream@~1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" - dependencies: - delayed-stream "~1.0.0" - -commander@2.9.0, commander@^2.8.1, commander@^2.9.0: - version "2.9.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" - dependencies: - graceful-readlink ">= 1.0.0" - -component-emitter@^1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - -concat-stream@^1.4.6: - version "1.5.2" - resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.5.2.tgz#708978624d856af41a5a741defdd261da752c266" - dependencies: - inherits "~2.0.1" - readable-stream "~2.0.0" - typedarray "~0.0.5" - -configstore@^1.0.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/configstore/-/configstore-1.4.0.tgz#c35781d0501d268c25c54b8b17f6240e8a4fb021" - dependencies: - graceful-fs "^4.1.2" - mkdirp "^0.5.0" - object-assign "^4.0.1" - os-tmpdir "^1.0.0" - osenv "^0.1.0" - uuid "^2.0.1" - write-file-atomic "^1.1.2" - xdg-basedir "^2.0.0" - -console-control-strings@^1.0.0, console-control-strings@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" - -content-disposition@0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.1.tgz#87476c6a67c8daa87e32e87616df883ba7fb071b" - -content-type@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.2.tgz#b7d113aee7a8dd27bd21133c4dc2529df1721eed" - -convert-source-map@^1.1.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.3.0.tgz#e9f3e9c6e2728efc2676696a70eb382f73106a67" - -cookie-signature@1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" - -cookie@0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" - -cookiejar@2.0.x: - version "2.0.6" - resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.0.6.tgz#0abf356ad00d1c5a219d88d44518046dd026acfe" - -cookiejar@^2.0.6: - version "2.1.0" - resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.0.tgz#86549689539b6d0e269b6637a304be508194d898" - -core-js@^2.4.0: - version "2.4.1" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e" - -core-util-is@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - -cors@^2.8.1: - version "2.8.1" - resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.1.tgz#6181aa56abb45a2825be3304703747ae4e9d2383" - dependencies: - vary "^1" - -cross-env@3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-3.1.3.tgz#58cd8231808f50089708b091f7dd37275a8e8154" - dependencies: - cross-spawn "^3.0.1" - -cross-spawn@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982" - dependencies: - lru-cache "^4.0.1" - which "^1.2.9" - -cryptiles@2.x.x: - version "2.0.5" - resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" - dependencies: - boom "2.x.x" - -crypto@^0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/crypto/-/crypto-0.0.3.tgz#470a81b86be4c5ee17acc8207a1f5315ae20dbb0" - -cycle@1.0.x: - version "1.0.3" - resolved "https://registry.yarnpkg.com/cycle/-/cycle-1.0.3.tgz#21e80b2be8580f98b468f379430662b046c34ad2" - -d@^0.1.1, d@~0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/d/-/d-0.1.1.tgz#da184c535d18d8ee7ba2aa229b914009fae11309" - dependencies: - es5-ext "~0.10.2" - -dashdash@^1.12.0: - version "1.14.1" - resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" - dependencies: - assert-plus "^1.0.0" - -debug@2, debug@2.2.0, debug@^2.1.1, debug@^2.2.0, debug@~2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" - dependencies: - ms "0.7.1" - -decamelize@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" - -deep-eql@^0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-0.1.3.tgz#ef558acab8de25206cd713906d74e56930eb69f2" - dependencies: - type-detect "0.1.1" - -deep-equal@^1.0.0, deep-equal@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" - -deep-extend@~0.4.0: - version "0.4.1" - resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.1.tgz#efe4113d08085f4e6f9687759810f807469e2253" - -deep-is@~0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" - -del@^2.0.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/del/-/del-2.2.2.tgz#c12c981d067846c84bcaf862cff930d907ffd1a8" - dependencies: - globby "^5.0.0" - is-path-cwd "^1.0.0" - is-path-in-cwd "^1.0.0" - object-assign "^4.0.1" - pify "^2.0.0" - pinkie-promise "^2.0.0" - rimraf "^2.2.8" - -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - -delegates@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" - -depd@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.0.tgz#e1bd82c6aab6ced965b97b88b17ed3e528ca18c3" - -destroy@~1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" - -detect-indent@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" - dependencies: - repeating "^2.0.0" - -diff@1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-1.4.0.tgz#7f28d2eb9ee7b15a97efd89ce63dcfdaa3ccbabf" - -doctrine@^1.2.2: - version "1.5.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" - dependencies: - esutils "^2.0.2" - isarray "^1.0.0" - -duplexer@~0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" - -duplexify@^3.2.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.5.0.tgz#1aa773002e1578457e9d9d4a50b0ccaaebcbd604" - dependencies: - end-of-stream "1.0.0" - inherits "^2.0.1" - readable-stream "^2.0.0" - stream-shift "^1.0.0" - -ecc-jsbn@~0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" - dependencies: - jsbn "~0.1.0" - -ee-first@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" - -encodeurl@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20" - -end-of-stream@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.0.0.tgz#d4596e702734a93e40e9af864319eabd99ff2f0e" - dependencies: - once "~1.3.0" - -entities@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0" - -es5-ext@^0.10.7, es5-ext@^0.10.8, es5-ext@~0.10.11, es5-ext@~0.10.2, es5-ext@~0.10.7: - version "0.10.12" - resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.12.tgz#aa84641d4db76b62abba5e45fd805ecbab140047" - dependencies: - es6-iterator "2" - es6-symbol "~3.1" - -es6-iterator@2: - version "2.0.0" - resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.0.tgz#bd968567d61635e33c0b80727613c9cb4b096bac" - dependencies: - d "^0.1.1" - es5-ext "^0.10.7" - es6-symbol "3" - -es6-map@^0.1.3: - version "0.1.4" - resolved "https://registry.yarnpkg.com/es6-map/-/es6-map-0.1.4.tgz#a34b147be224773a4d7da8072794cefa3632b897" - dependencies: - d "~0.1.1" - es5-ext "~0.10.11" - es6-iterator "2" - es6-set "~0.1.3" - es6-symbol "~3.1.0" - event-emitter "~0.3.4" - -es6-promise@3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.2.1.tgz#ec56233868032909207170c39448e24449dd1fc4" - -es6-promise@^3.0.2: - version "3.3.1" - resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613" - -es6-set@~0.1.3: - version "0.1.4" - resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.4.tgz#9516b6761c2964b92ff479456233a247dc707ce8" - dependencies: - d "~0.1.1" - es5-ext "~0.10.11" - es6-iterator "2" - es6-symbol "3" - event-emitter "~0.3.4" - -es6-symbol@3, es6-symbol@~3.1, es6-symbol@~3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.0.tgz#94481c655e7a7cad82eba832d97d5433496d7ffa" - dependencies: - d "~0.1.1" - es5-ext "~0.10.11" - -es6-weak-map@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.1.tgz#0d2bbd8827eb5fb4ba8f97fbfea50d43db21ea81" - dependencies: - d "^0.1.1" - es5-ext "^0.10.8" - es6-iterator "2" - es6-symbol "3" - -escape-html@~1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" - -escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - -escodegen@1.8.x: - version "1.8.1" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.8.1.tgz#5a5b53af4693110bebb0867aa3430dd3b70a1018" - dependencies: - esprima "^2.7.1" - estraverse "^1.9.1" - esutils "^2.0.2" - optionator "^0.8.1" - optionalDependencies: - source-map "~0.2.0" - -escope@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/escope/-/escope-3.6.0.tgz#e01975e812781a163a6dadfdd80398dc64c889c3" - dependencies: - es6-map "^0.1.3" - es6-weak-map "^2.0.1" - esrecurse "^4.1.0" - estraverse "^4.1.1" - -eslint-config-zavatta@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/eslint-config-zavatta/-/eslint-config-zavatta-4.2.0.tgz#8c98a92ad9f176d887f9166ea8f5ce1cd67043ea" - -eslint@^3.7.1: - version "3.10.2" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-3.10.2.tgz#c9a10e8bf6e9d65651204778c503341f1eac3ce7" - dependencies: - babel-code-frame "^6.16.0" - chalk "^1.1.3" - concat-stream "^1.4.6" - debug "^2.1.1" - doctrine "^1.2.2" - escope "^3.6.0" - espree "^3.3.1" - estraverse "^4.2.0" - esutils "^2.0.2" - file-entry-cache "^2.0.0" - glob "^7.0.3" - globals "^9.2.0" - ignore "^3.2.0" - imurmurhash "^0.1.4" - inquirer "^0.12.0" - is-my-json-valid "^2.10.0" - is-resolvable "^1.0.0" - js-yaml "^3.5.1" - json-stable-stringify "^1.0.0" - levn "^0.3.0" - lodash "^4.0.0" - mkdirp "^0.5.0" - natural-compare "^1.4.0" - optionator "^0.8.2" - path-is-inside "^1.0.1" - pluralize "^1.2.1" - progress "^1.1.8" - require-uncached "^1.0.2" - shelljs "^0.7.5" - strip-bom "^3.0.0" - strip-json-comments "~1.0.1" - table "^3.7.8" - text-table "~0.2.0" - user-home "^2.0.0" - -espree@^3.3.1: - version "3.3.2" - resolved "https://registry.yarnpkg.com/espree/-/espree-3.3.2.tgz#dbf3fadeb4ecb4d4778303e50103b3d36c88b89c" - dependencies: - acorn "^4.0.1" - acorn-jsx "^3.0.0" - -esprima@2.7.x, esprima@^2.6.0, esprima@^2.7.1: - version "2.7.3" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" - -esrecurse@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.1.0.tgz#4713b6536adf7f2ac4f327d559e7756bff648220" - dependencies: - estraverse "~4.1.0" - object-assign "^4.0.1" - -estraverse@^1.9.1: - version "1.9.3" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-1.9.3.tgz#af67f2dc922582415950926091a4005d29c9bb44" - -estraverse@^4.1.1, estraverse@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" - -estraverse@~4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.1.1.tgz#f6caca728933a850ef90661d0e17982ba47111a2" - -esutils@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" - -etag@~1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/etag/-/etag-1.7.0.tgz#03d30b5f67dd6e632d2945d30d6652731a34d5d8" - -event-emitter@~0.3.4: - version "0.3.4" - resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.4.tgz#8d63ddfb4cfe1fae3b32ca265c4c720222080bb5" - dependencies: - d "~0.1.1" - es5-ext "~0.10.7" - -event-stream@~3.3.0: - version "3.3.4" - resolved "https://registry.yarnpkg.com/event-stream/-/event-stream-3.3.4.tgz#4ab4c9a0f5a54db9338b4c34d86bfce8f4b35571" - dependencies: - duplexer "~0.1.1" - from "~0" - map-stream "~0.1.0" - pause-stream "0.0.11" - split "0.3" - stream-combiner "~0.0.4" - through "~2.3.1" - -eventemitter3@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508" - -exit-hook@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8" - -expand-brackets@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b" - dependencies: - is-posix-bracket "^0.1.0" - -expand-range@^1.8.1: - version "1.8.2" - resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337" - dependencies: - fill-range "^2.1.0" - -express@^4.14.0: - version "4.14.0" - resolved "https://registry.yarnpkg.com/express/-/express-4.14.0.tgz#c1ee3f42cdc891fb3dc650a8922d51ec847d0d66" - dependencies: - accepts "~1.3.3" - array-flatten "1.1.1" - content-disposition "0.5.1" - content-type "~1.0.2" - cookie "0.3.1" - cookie-signature "1.0.6" - debug "~2.2.0" - depd "~1.1.0" - encodeurl "~1.0.1" - escape-html "~1.0.3" - etag "~1.7.0" - finalhandler "0.5.0" - fresh "0.3.0" - merge-descriptors "1.0.1" - methods "~1.1.2" - on-finished "~2.3.0" - parseurl "~1.3.1" - path-to-regexp "0.1.7" - proxy-addr "~1.1.2" - qs "6.2.0" - range-parser "~1.2.0" - send "0.14.1" - serve-static "~1.11.1" - type-is "~1.6.13" - utils-merge "1.0.0" - vary "~1.1.0" - -extend-shallow@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" - dependencies: - is-extendable "^0.1.0" - -extend@3, extend@^3.0.0, extend@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.0.tgz#5a474353b9f3353ddd8176dfd37b91c83a46f1d4" - -extglob@^0.3.1: - version "0.3.2" - resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" - dependencies: - is-extglob "^1.0.0" - -extsprintf@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.0.2.tgz#e1080e0658e300b06294990cc70e1502235fd550" - -eyes@0.1.x: - version "0.1.8" - resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0" - -fast-levenshtein@~2.0.4: - version "2.0.5" - resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.5.tgz#bd33145744519ab1c36c3ee9f31f08e9079b67f2" - -figures@^1.3.5: - version "1.7.0" - resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" - dependencies: - escape-string-regexp "^1.0.5" - object-assign "^4.1.0" - -file-entry-cache@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361" - dependencies: - flat-cache "^1.2.1" - object-assign "^4.0.1" - -filename-regex@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.0.tgz#996e3e80479b98b9897f15a8a58b3d084e926775" - -fill-range@^2.1.0: - version "2.2.3" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.3.tgz#50b77dfd7e469bc7492470963699fe7a8485a723" - dependencies: - is-number "^2.1.0" - isobject "^2.0.0" - randomatic "^1.1.3" - repeat-element "^1.1.2" - repeat-string "^1.5.2" - -filter-keys@^1.0.2: - version "1.1.0" - resolved "https://registry.yarnpkg.com/filter-keys/-/filter-keys-1.1.0.tgz#e3851541c924695646f8c1fc4dcac91193b2e77b" - dependencies: - micromatch "^2.2.0" - -filter-object@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/filter-object/-/filter-object-2.1.0.tgz#af9c0ad0bb40a006946b84b4db33c3ae5e93df86" - dependencies: - extend-shallow "^2.0.1" - filter-keys "^1.0.2" - filter-values "^0.4.0" - kind-of "^2.0.1" - object.pick "^1.1.1" - -filter-values@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/filter-values/-/filter-values-0.4.0.tgz#f1b618dad908d0dd9906d27ca81620300ebf7a09" - dependencies: - for-own "^0.1.3" - is-match "^0.4.0" - -finalhandler@0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-0.5.0.tgz#e9508abece9b6dba871a6942a1d7911b91911ac7" - dependencies: - debug "~2.2.0" - escape-html "~1.0.3" - on-finished "~2.3.0" - statuses "~1.3.0" - unpipe "~1.0.0" - -flat-cache@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.2.1.tgz#6c837d6225a7de5659323740b36d5361f71691ff" - dependencies: - circular-json "^0.3.0" - del "^2.0.2" - graceful-fs "^4.1.2" - write "^0.2.1" - -for-in@^0.1.5: - version "0.1.6" - resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.6.tgz#c9f96e89bfad18a545af5ec3ed352a1d9e5b4dc8" - -for-own@^0.1.3, for-own@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.4.tgz#0149b41a39088c7515f51ebe1c1386d45f935072" - dependencies: - for-in "^0.1.5" - -forever-agent@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" - -form-data@1.0.0-rc4: - version "1.0.0-rc4" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-1.0.0-rc4.tgz#05ac6bc22227b43e4461f488161554699d4f8b5e" - dependencies: - async "^1.5.2" - combined-stream "^1.0.5" - mime-types "^2.1.10" - -form-data@~2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.2.tgz#89c3534008b97eada4cbb157d58f6f5df025eae4" - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.5" - mime-types "^2.1.12" - -formatio@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/formatio/-/formatio-1.1.1.tgz#5ed3ccd636551097383465d996199100e86161e9" - dependencies: - samsam "~1.1" - -formidable@^1.0.17: - version "1.0.17" - resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.0.17.tgz#ef5491490f9433b705faa77249c99029ae348559" - -forwarded@~0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.0.tgz#19ef9874c4ae1c297bcf078fde63a09b66a84363" - -fresh@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.3.0.tgz#651f838e22424e7566de161d8358caa199f83d4f" - -from@~0: - version "0.1.3" - resolved "https://registry.yarnpkg.com/from/-/from-0.1.3.tgz#ef63ac2062ac32acf7862e0d40b44b896f22f3bc" - -fs-extra@~0.28.0: - version "0.28.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.28.0.tgz#9a1c0708ea8c5169297ab06fd8cb914f5647b272" - dependencies: - graceful-fs "^4.1.2" - jsonfile "^2.1.0" - klaw "^1.0.0" - path-is-absolute "^1.0.0" - rimraf "^2.2.8" - -fs-readdir-recursive@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.0.0.tgz#8cd1745c8b4f8a29c8caec392476921ba195f560" - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - -fsevents@^1.0.0: - version "1.0.15" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.0.15.tgz#fa63f590f3c2ad91275e4972a6cea545fb0aae44" - dependencies: - nan "^2.3.0" - node-pre-gyp "^0.6.29" - -fstream-ignore@~1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105" - dependencies: - fstream "^1.0.0" - inherits "2" - minimatch "^3.0.0" - -fstream@^1.0.0, fstream@^1.0.2, fstream@~1.0.10: - version "1.0.10" - resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.10.tgz#604e8a92fe26ffd9f6fae30399d4984e1ab22822" - dependencies: - graceful-fs "^4.1.2" - inherits "~2.0.0" - mkdirp ">=0.5 0" - rimraf "2" - -gauge@~2.7.1: - version "2.7.1" - resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.1.tgz#388473894fe8be5e13ffcdb8b93e4ed0616428c7" - dependencies: - aproba "^1.0.3" - console-control-strings "^1.0.0" - has-color "^0.1.7" - has-unicode "^2.0.0" - object-assign "^4.1.0" - signal-exit "^3.0.0" - string-width "^1.0.1" - strip-ansi "^3.0.1" - wide-align "^1.1.0" - -generate-function@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.0.0.tgz#6858fe7c0969b7d4e9093337647ac79f60dfbe74" - -generate-object-property@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/generate-object-property/-/generate-object-property-1.2.0.tgz#9c0e1c40308ce804f4783618b937fa88f99d50d0" - dependencies: - is-property "^1.0.0" - -getpass@^0.1.1: - version "0.1.6" - resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.6.tgz#283ffd9fc1256840875311c1b60e8c40187110e6" - dependencies: - assert-plus "^1.0.0" - -glob-base@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" - dependencies: - glob-parent "^2.0.0" - is-glob "^2.0.0" - -glob-parent@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28" - dependencies: - is-glob "^2.0.0" - -glob@7.0.5: - version "7.0.5" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.0.5.tgz#b4202a69099bbb4d292a7c1b95b6682b67ebdc95" - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.2" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@^5.0.15, glob@^5.0.5: - version "5.0.15" - resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" - dependencies: - inflight "^1.0.4" - inherits "2" - minimatch "2 || 3" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@^7.0.0, glob@^7.0.3, glob@^7.0.5: - version "7.1.1" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.2" - once "^1.3.0" - path-is-absolute "^1.0.0" - -globals@^9.0.0, globals@^9.2.0: - version "9.14.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-9.14.0.tgz#8859936af0038741263053b39d0e76ca241e4034" - -globby@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d" - dependencies: - array-union "^1.0.1" - arrify "^1.0.0" - glob "^7.0.3" - object-assign "^4.0.1" - pify "^2.0.0" - pinkie-promise "^2.0.0" - -got@^3.2.0: - version "3.3.1" - resolved "https://registry.yarnpkg.com/got/-/got-3.3.1.tgz#e5d0ed4af55fc3eef4d56007769d98192bcb2eca" - dependencies: - duplexify "^3.2.0" - infinity-agent "^2.0.0" - is-redirect "^1.0.0" - is-stream "^1.0.0" - lowercase-keys "^1.0.0" - nested-error-stacks "^1.0.0" - object-assign "^3.0.0" - prepend-http "^1.0.0" - read-all-stream "^3.0.0" - timed-out "^2.0.0" - -graceful-fs@^4.1.2, graceful-fs@^4.1.4, graceful-fs@^4.1.6, graceful-fs@^4.1.9: - version "4.1.11" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" - -"graceful-readlink@>= 1.0.0": - version "1.0.1" - resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" - -growl@1.9.2: - version "1.9.2" - resolved "https://registry.yarnpkg.com/growl/-/growl-1.9.2.tgz#0ea7743715db8d8de2c5ede1775e1b45ac85c02f" - -handlebars@^4.0.1: - version "4.0.6" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.6.tgz#2ce4484850537f9c97a8026d5399b935c4ed4ed7" - dependencies: - async "^1.4.0" - optimist "^0.6.1" - source-map "^0.4.4" - optionalDependencies: - uglify-js "^2.6" - -har-validator@~2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-2.0.6.tgz#cdcbc08188265ad119b6a5a7c8ab70eecfb5d27d" - dependencies: - chalk "^1.1.1" - commander "^2.9.0" - is-my-json-valid "^2.12.4" - pinkie-promise "^2.0.0" - -has-ansi@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" - dependencies: - ansi-regex "^2.0.0" - -has-color@^0.1.7, has-color@~0.1.0: - version "0.1.7" - resolved "https://registry.yarnpkg.com/has-color/-/has-color-0.1.7.tgz#67144a5260c34fc3cca677d041daf52fe7b78b2f" - -has-flag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" - -has-unicode@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" - -hawk@~3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" - dependencies: - boom "2.x.x" - cryptiles "2.x.x" - hoek "2.x.x" - sntp "1.x.x" - -hoek@2.x.x: - version "2.16.3" - resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" - -home-or-tmp@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" - dependencies: - os-homedir "^1.0.0" - os-tmpdir "^1.0.1" - -hooks-fixed@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/hooks-fixed/-/hooks-fixed-1.2.0.tgz#0d2772d4d7d685ff9244724a9f0b5b2559aac96b" - -http-errors@~1.5.0: - version "1.5.1" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.5.1.tgz#788c0d2c1de2c81b9e6e8c01843b6b97eb920750" - dependencies: - inherits "2.0.3" - setprototypeof "1.0.2" - statuses ">= 1.3.1 < 2" - -http-signature@~1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" - dependencies: - assert-plus "^0.2.0" - jsprim "^1.2.2" - sshpk "^1.7.0" - -https-proxy-agent@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-1.0.0.tgz#35f7da6c48ce4ddbfa264891ac593ee5ff8671e6" - dependencies: - agent-base "2" - debug "2" - extend "3" - -iconv-lite@0.4.13, iconv-lite@^0.4.13: - version "0.4.13" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.13.tgz#1f88aba4ab0b1508e8312acc39345f36e992e2f2" - -ignore-by-default@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" - -ignore@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.2.0.tgz#8d88f03c3002a0ac52114db25d2c673b0bf1e435" - -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" - -infinity-agent@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/infinity-agent/-/infinity-agent-2.0.3.tgz#45e0e2ff7a9eb030b27d62b74b3744b7a7ac4216" - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@~2.0.0, inherits@~2.0.1: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - -inherits@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" - -ini@~1.3.0: - version "1.3.4" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e" - -inquirer@^0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-0.12.0.tgz#1ef2bfd63504df0bc75785fff8c2c41df12f077e" - dependencies: - ansi-escapes "^1.1.0" - ansi-regex "^2.0.0" - chalk "^1.0.0" - cli-cursor "^1.0.1" - cli-width "^2.0.0" - figures "^1.3.5" - lodash "^4.3.0" - readline2 "^1.0.1" - run-async "^0.1.0" - rx-lite "^3.1.2" - string-width "^1.0.1" - strip-ansi "^3.0.0" - through "^2.3.6" - -interpret@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.1.tgz#d579fb7f693b858004947af39fa0db49f795602c" - -invariant@^2.2.0: - version "2.2.2" - resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360" - dependencies: - loose-envify "^1.0.0" - -ip-regex@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-1.0.3.tgz#dc589076f659f419c222039a33316f1c7387effd" - -ipaddr.js@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.1.1.tgz#c791d95f52b29c1247d5df80ada39b8a73647230" - -is-binary-path@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" - dependencies: - binary-extensions "^1.0.0" - -is-buffer@^1.0.2: - version "1.1.4" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.4.tgz#cfc86ccd5dc5a52fa80489111c6920c457e2d98b" - -is-dotfile@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.2.tgz#2c132383f39199f8edc268ca01b9b007d205cc4d" - -is-equal-shallow@^0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534" - dependencies: - is-primitive "^2.0.0" - -is-extendable@^0.1.0, is-extendable@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" - -is-extglob@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" - -is-finite@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" - dependencies: - number-is-nan "^1.0.0" - -is-fullwidth-code-point@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" - dependencies: - number-is-nan "^1.0.0" - -is-fullwidth-code-point@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" - -is-glob@^2.0.0, is-glob@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863" - dependencies: - is-extglob "^1.0.0" - -is-ip@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-ip/-/is-ip-1.0.0.tgz#2bb6959f797ccd6f9fdc812758bcbc87c4c59074" - dependencies: - ip-regex "^1.0.0" - -is-match@^0.4.0: - version "0.4.1" - resolved "https://registry.yarnpkg.com/is-match/-/is-match-0.4.1.tgz#fb5f6c6709a1543b7c7efa7d9530e5b776f61f83" - dependencies: - deep-equal "^1.0.1" - is-extendable "^0.1.1" - is-glob "^2.0.1" - micromatch "^2.3.7" - -is-my-json-valid@^2.10.0, is-my-json-valid@^2.12.4: - version "2.15.0" - resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.15.0.tgz#936edda3ca3c211fd98f3b2d3e08da43f7b2915b" - dependencies: - generate-function "^2.0.0" - generate-object-property "^1.1.0" - jsonpointer "^4.0.0" - xtend "^4.0.0" - -is-npm@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4" - -is-number@^2.0.2, is-number@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" - dependencies: - kind-of "^3.0.2" - -is-path-cwd@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d" - -is-path-in-cwd@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz#6477582b8214d602346094567003be8a9eac04dc" - dependencies: - is-path-inside "^1.0.0" - -is-path-inside@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.0.tgz#fc06e5a1683fbda13de667aff717bbc10a48f37f" - dependencies: - path-is-inside "^1.0.1" - -is-posix-bracket@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4" - -is-primitive@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575" - -is-property@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" - -is-redirect@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24" - -is-resolvable@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.0.0.tgz#8df57c61ea2e3c501408d100fb013cf8d6e0cc62" - dependencies: - tryit "^1.0.1" - -is-stream@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" - -is-typedarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - -is_js@^0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/is_js/-/is_js-0.9.0.tgz#0ab94540502ba7afa24c856aa985561669e9c52d" - -isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - -isexe@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-1.1.2.tgz#36f3e22e60750920f5e7241a476a8c6a42275ad0" - -isobject@^2.0.0, isobject@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" - dependencies: - isarray "1.0.0" - -isstream@0.1.x, isstream@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" - -istanbul@^0.4.5: - version "0.4.5" - resolved "https://registry.yarnpkg.com/istanbul/-/istanbul-0.4.5.tgz#65c7d73d4c4da84d4f3ac310b918fb0b8033733b" - dependencies: - abbrev "1.0.x" - async "1.x" - escodegen "1.8.x" - esprima "2.7.x" - glob "^5.0.15" - handlebars "^4.0.1" - js-yaml "3.x" - mkdirp "0.5.x" - nopt "3.x" - once "1.x" - resolve "1.1.x" - supports-color "^3.1.0" - which "^1.1.1" - wordwrap "^1.0.0" - -jodid25519@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/jodid25519/-/jodid25519-1.0.2.tgz#06d4912255093419477d425633606e0e90782967" - dependencies: - jsbn "~0.1.0" - -js-tokens@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-2.0.0.tgz#79903f5563ee778cc1162e6dcf1a0027c97f9cb5" - -js-yaml@3.x, js-yaml@^3.5.1: - version "3.7.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.7.0.tgz#5c967ddd837a9bfdca5f2de84253abe8a1c03b80" - dependencies: - argparse "^1.0.7" - esprima "^2.6.0" - -jsbn@~0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.0.tgz#650987da0dd74f4ebf5a11377a2aa2d273e97dfd" - -jsesc@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" - -jsesc@~0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" - -json-schema@0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" - -json-stable-stringify@^1.0.0, json-stable-stringify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" - dependencies: - jsonify "~0.0.0" - -json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" - -json3@3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1" - -json5@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.0.tgz#9b20715b026cbe3778fd769edccd822d8332a5b2" - -jsonfile@^2.1.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8" - optionalDependencies: - graceful-fs "^4.1.6" - -jsonify@~0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" - -jsonpointer@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.0.tgz#6661e161d2fc445f19f98430231343722e1fcbd5" - -jsprim@^1.2.2: - version "1.3.1" - resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.3.1.tgz#2a7256f70412a29ee3670aaca625994c4dcff252" - dependencies: - extsprintf "1.0.2" - json-schema "0.2.3" - verror "1.3.6" - -kareem@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/kareem/-/kareem-1.1.3.tgz#0877610d8879c38da62d1dbafde4e17f2692f041" - -kind-of@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-2.0.1.tgz#018ec7a4ce7e3a86cb9141be519d24c8faa981b5" - dependencies: - is-buffer "^1.0.2" - -kind-of@^3.0.2: - version "3.0.4" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.0.4.tgz#7b8ecf18a4e17f8269d73b501c9f232c96887a74" - dependencies: - is-buffer "^1.0.2" - -klaw@^1.0.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439" - optionalDependencies: - graceful-fs "^4.1.9" - -latest-version@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-1.0.1.tgz#72cfc46e3e8d1be651e1ebb54ea9f6ea96f374bb" - dependencies: - package-json "^1.0.0" - -lazy-cache@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" - -levn@^0.3.0, levn@~0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" - dependencies: - prelude-ls "~1.1.2" - type-check "~0.3.2" - -linkify-it@~1.2.2: - version "1.2.4" - resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-1.2.4.tgz#0773526c317c8fd13bd534ee1d180ff88abf881a" - dependencies: - uc.micro "^1.0.1" - -lodash._baseassign@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz#8c38a099500f215ad09e59f1722fd0c52bfe0a4e" - dependencies: - lodash._basecopy "^3.0.0" - lodash.keys "^3.0.0" - -lodash._basecopy@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36" - -lodash._basecreate@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz#1bc661614daa7fc311b7d03bf16806a0213cf821" - -lodash._bindcallback@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e" - -lodash._createassigner@^3.0.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/lodash._createassigner/-/lodash._createassigner-3.1.1.tgz#838a5bae2fdaca63ac22dee8e19fa4e6d6970b11" - dependencies: - lodash._bindcallback "^3.0.0" - lodash._isiterateecall "^3.0.0" - lodash.restparam "^3.0.0" - -lodash._getnative@^3.0.0: - version "3.9.1" - resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" - -lodash._isiterateecall@^3.0.0: - version "3.0.9" - resolved "https://registry.yarnpkg.com/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz#5203ad7ba425fae842460e696db9cf3e6aac057c" - -lodash.assign@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-3.2.0.tgz#3ce9f0234b4b2223e296b8fa0ac1fee8ebca64fa" - dependencies: - lodash._baseassign "^3.0.0" - lodash._createassigner "^3.0.0" - lodash.keys "^3.0.0" - -lodash.create@3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/lodash.create/-/lodash.create-3.1.1.tgz#d7f2849f0dbda7e04682bb8cd72ab022461debe7" - dependencies: - lodash._baseassign "^3.0.0" - lodash._basecreate "^3.0.0" - lodash._isiterateecall "^3.0.0" - -lodash.defaults@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-3.1.2.tgz#c7308b18dbf8bc9372d701a73493c61192bd2e2c" - dependencies: - lodash.assign "^3.0.0" - lodash.restparam "^3.0.0" - -lodash.isarguments@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" - -lodash.isarray@^3.0.0: - version "3.0.4" - resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" - -lodash.keys@^3.0.0: - version "3.1.2" - resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a" - dependencies: - lodash._getnative "^3.0.0" - lodash.isarguments "^3.0.0" - lodash.isarray "^3.0.0" - -lodash.pickby@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/lodash.pickby/-/lodash.pickby-4.6.0.tgz#7dea21d8c18d7703a27c704c15d3b84a67e33aff" - -lodash.restparam@^3.0.0: - version "3.6.1" - resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" - -lodash@^4.0.0, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.16.4, lodash@^4.2.0, lodash@^4.3.0: - version "4.17.2" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.2.tgz#34a3055babe04ce42467b607d700072c7ff6bf42" - -lodash@~4.11.1: - version "4.11.2" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.11.2.tgz#d6b4338b110a58e21dae5cebcfdbbfd2bc4cdb3b" - -lodash@~4.9.0: - version "4.9.0" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.9.0.tgz#4c20d742f03ce85dc700e0dd7ab9bcab85e6fc14" - -lolex@1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/lolex/-/lolex-1.3.2.tgz#7c3da62ffcb30f0f5a80a2566ca24e45d8a01f31" - -longest@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" - -loose-envify@^1.0.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.0.tgz#6b26248c42f6d4fa4b0d8542f78edfcde35642a8" - dependencies: - js-tokens "^2.0.0" - -lowercase-keys@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306" - -lru-cache@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.0.2.tgz#1d17679c069cda5d040991a09dbc2c0db377e55e" - dependencies: - pseudomap "^1.0.1" - yallist "^2.0.0" - -map-stream@~0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194" - -markdown-it@^6.0.1: - version "6.1.1" - resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-6.1.1.tgz#ced037f4473ee9f5153ac414f77dc83c91ba927c" - dependencies: - argparse "^1.0.7" - entities "~1.1.1" - linkify-it "~1.2.2" - mdurl "~1.0.1" - uc.micro "^1.0.1" - -mdurl@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" - -media-typer@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" - -merge-descriptors@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" - -methods@^1.1.1, methods@^1.1.2, methods@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" - -micromatch@^2.1.5, micromatch@^2.2.0, micromatch@^2.3.7: - version "2.3.11" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" - dependencies: - arr-diff "^2.0.0" - array-unique "^0.2.1" - braces "^1.8.2" - expand-brackets "^0.1.4" - extglob "^0.3.1" - filename-regex "^2.0.0" - is-extglob "^1.0.0" - is-glob "^2.0.1" - kind-of "^3.0.2" - normalize-path "^2.0.1" - object.omit "^2.0.0" - parse-glob "^3.0.4" - regex-cache "^0.4.2" - -mime-db@~1.25.0: - version "1.25.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.25.0.tgz#c18dbd7c73a5dbf6f44a024dc0d165a1e7b1c392" - -mime-types@^2.1.10, mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.13, mime-types@~2.1.7: - version "2.1.13" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.13.tgz#e07aaa9c6c6b9a7ca3012c69003ad25a39e92a88" - dependencies: - mime-db "~1.25.0" - -mime@1.3.4, mime@^1.3.4: - version "1.3.4" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" - -"minimatch@2 || 3", minimatch@3.0.2, minimatch@^3.0.0, minimatch@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.2.tgz#0f398a7300ea441e9c348c83d98ab8c9dbf9c40a" - dependencies: - brace-expansion "^1.0.0" - -minimist@0.0.8, minimist@~0.0.1: - version "0.0.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" - -minimist@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" - -mkdirp@0.5.1, mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" - dependencies: - minimist "0.0.8" - -mocha@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-3.1.2.tgz#51f93b432bf7e1b175ffc22883ccd0be32dba6b5" - dependencies: - browser-stdout "1.3.0" - commander "2.9.0" - debug "2.2.0" - diff "1.4.0" - escape-string-regexp "1.0.5" - glob "7.0.5" - growl "1.9.2" - json3 "3.3.2" - lodash.create "3.1.1" - mkdirp "0.5.1" - supports-color "3.1.2" - -mock-require@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/mock-require/-/mock-require-1.3.0.tgz#826144952e504762f8e6924aa8f639465d1d7a24" - dependencies: - caller-id "^0.1.0" - -mongodb-core@2.0.13: - version "2.0.13" - resolved "https://registry.yarnpkg.com/mongodb-core/-/mongodb-core-2.0.13.tgz#f9394b588dce0e579482e53d74dbc7d7a9d4519c" - dependencies: - bson "~0.5.6" - require_optional "~1.0.0" - -mongodb@2.2.11: - version "2.2.11" - resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-2.2.11.tgz#a828b036fe6a437a35e723af5f81781c4976306c" - dependencies: - es6-promise "3.2.1" - mongodb-core "2.0.13" - readable-stream "2.1.5" - -mongoose@^4.6.3: - version "4.7.0" - resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-4.7.0.tgz#adb7c6b73dfb76f204a70ba6cd364f887dcc2012" - dependencies: - async "2.1.2" - bson "~0.5.4" - hooks-fixed "1.2.0" - kareem "1.1.3" - mongodb "2.2.11" - mpath "0.2.1" - mpromise "0.5.5" - mquery "2.0.0" - ms "0.7.1" - muri "1.1.1" - regexp-clone "0.0.1" - sliced "1.0.1" - -mpath@0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/mpath/-/mpath-0.2.1.tgz#3a4e829359801de96309c27a6b2e102e89f9e96e" - -mpromise@0.5.5: - version "0.5.5" - resolved "https://registry.yarnpkg.com/mpromise/-/mpromise-0.5.5.tgz#f5b24259d763acc2257b0a0c8c6d866fd51732e6" - -mquery@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/mquery/-/mquery-2.0.0.tgz#b5abc850b90dffc3e10ae49b4b6e7a479752df22" - dependencies: - bluebird "2.10.2" - debug "2.2.0" - regexp-clone "0.0.1" - sliced "0.0.5" - -ms@0.7.1: - version "0.7.1" - resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" - -muri@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/muri/-/muri-1.1.1.tgz#64bd904eaf8ff89600c994441fad3c5195905ac2" - -mute-stream@0.0.5: - version "0.0.5" - resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0" - -nan@^2.3.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.4.0.tgz#fb3c59d45fe4effe215f0b890f8adf6eb32d2232" - -natural-compare@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" - -negotiator@0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" - -nested-error-stacks@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/nested-error-stacks/-/nested-error-stacks-1.0.2.tgz#19f619591519f096769a5ba9a86e6eeec823c3cf" - dependencies: - inherits "~2.0.1" - -nock@^8.1.0: - version "8.2.1" - resolved "https://registry.yarnpkg.com/nock/-/nock-8.2.1.tgz#64cc65e1bdd3893f58cba7e1abfdc38f40f0364a" - dependencies: - chai ">=1.9.2 <4.0.0" - debug "^2.2.0" - deep-equal "^1.0.0" - json-stringify-safe "^5.0.1" - lodash "~4.9.0" - mkdirp "^0.5.0" - propagate "0.4.0" - qs "^6.0.2" - -node-pre-gyp@^0.6.29: - version "0.6.31" - resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.31.tgz#d8a00ddaa301a940615dbcc8caad4024d58f6017" - dependencies: - mkdirp "~0.5.1" - nopt "~3.0.6" - npmlog "^4.0.0" - rc "~1.1.6" - request "^2.75.0" - rimraf "~2.5.4" - semver "~5.3.0" - tar "~2.2.1" - tar-pack "~3.3.0" - -nodemon@^1.11.0: - version "1.11.0" - resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.11.0.tgz#226c562bd2a7b13d3d7518b49ad4828a3623d06c" - dependencies: - chokidar "^1.4.3" - debug "^2.2.0" - es6-promise "^3.0.2" - ignore-by-default "^1.0.0" - lodash.defaults "^3.1.2" - minimatch "^3.0.0" - ps-tree "^1.0.1" - touch "1.0.0" - undefsafe "0.0.3" - update-notifier "0.5.0" - -nomnom@~1.8.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/nomnom/-/nomnom-1.8.1.tgz#2151f722472ba79e50a76fc125bb8c8f2e4dc2a7" - dependencies: - chalk "~0.4.0" - underscore "~1.6.0" - -nopt@3.x, nopt@~3.0.6: - version "3.0.6" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" - dependencies: - abbrev "1" - -nopt@~1.0.10: - version "1.0.10" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" - dependencies: - abbrev "1" - -normalize-path@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.0.1.tgz#47886ac1662760d4261b7d979d241709d3ce3f7a" - -npmlog@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.0.1.tgz#d14f503b4cd79710375553004ba96e6662fbc0b8" - dependencies: - are-we-there-yet "~1.1.2" - console-control-strings "~1.1.0" - gauge "~2.7.1" - set-blocking "~2.0.0" - -number-is-nan@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" - -oauth-sign@~0.8.1: - version "0.8.2" - resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" - -object-assign@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-3.0.0.tgz#9bedd5ca0897949bca47e7ff408062d549f587f2" - -object-assign@^4.0.1, object-assign@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.0.tgz#7a3b3d0e98063d43f4c03f2e8ae6cd51a86883a0" - -object.omit@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" - dependencies: - for-own "^0.1.4" - is-extendable "^0.1.1" - -object.pick@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.2.0.tgz#b5392bee9782da6d9fb7d6afaf539779f1234c2b" - dependencies: - isobject "^2.1.0" - -on-finished@~2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" - dependencies: - ee-first "1.1.1" - -once@1.x, once@^1.3.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - dependencies: - wrappy "1" - -once@~1.3.0, once@~1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/once/-/once-1.3.3.tgz#b2e261557ce4c314ec8304f3fa82663e4297ca20" - dependencies: - wrappy "1" - -onetime@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" - -optimist@^0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" - dependencies: - minimist "~0.0.1" - wordwrap "~0.0.2" - -optionator@^0.8.1, optionator@^0.8.2: - version "0.8.2" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" - dependencies: - deep-is "~0.1.3" - fast-levenshtein "~2.0.4" - levn "~0.3.0" - prelude-ls "~1.1.2" - type-check "~0.3.2" - wordwrap "~1.0.0" - -options@>=0.0.5: - version "0.0.6" - resolved "https://registry.yarnpkg.com/options/-/options-0.0.6.tgz#ec22d312806bb53e731773e7cdaefcf1c643128f" - -os-homedir@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" - -os-tmpdir@^1.0.0, os-tmpdir@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" - -osenv@^0.1.0: - version "0.1.3" - resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.3.tgz#83cf05c6d6458fc4d5ac6362ea325d92f2754217" - dependencies: - os-homedir "^1.0.0" - os-tmpdir "^1.0.0" - -output-file-sync@^1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/output-file-sync/-/output-file-sync-1.1.2.tgz#d0a33eefe61a205facb90092e826598d5245ce76" - dependencies: - graceful-fs "^4.1.4" - mkdirp "^0.5.1" - object-assign "^4.1.0" - -package-json@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/package-json/-/package-json-1.2.0.tgz#c8ecac094227cdf76a316874ed05e27cc939a0e0" - dependencies: - got "^3.2.0" - registry-url "^3.0.0" - -parse-glob@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" - dependencies: - glob-base "^0.3.0" - is-dotfile "^1.0.0" - is-extglob "^1.0.0" - is-glob "^2.0.0" - -parseurl@~1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.1.tgz#c8ab8c9223ba34888aa64a297b28853bec18da56" - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - -path-is-inside@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" - -path-to-regexp@0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" - -pause-stream@0.0.11: - version "0.0.11" - resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445" - dependencies: - through "~2.3" - -pify@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" - -pinkie-promise@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" - dependencies: - pinkie "^2.0.0" - -pinkie@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" - -pkginfo@0.3.x: - version "0.3.1" - resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.3.1.tgz#5b29f6a81f70717142e09e765bbeab97b4f81e21" - -pluralize@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45" - -prelude-ls@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" - -prepend-http@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" - -preserve@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" - -private@^0.1.6, private@~0.1.5: - version "0.1.6" - resolved "https://registry.yarnpkg.com/private/-/private-0.1.6.tgz#55c6a976d0f9bafb9924851350fe47b9b5fbb7c1" - -process-nextick-args@~1.0.6: - version "1.0.7" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" - -progress@^1.1.8: - version "1.1.8" - resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" - -propagate@0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/propagate/-/propagate-0.4.0.tgz#f3fcca0a6fe06736a7ba572966069617c130b481" - -proxy-addr@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.2.tgz#b4cc5f22610d9535824c123aef9d3cf73c40ba37" - dependencies: - forwarded "~0.1.0" - ipaddr.js "1.1.1" - -ps-tree@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/ps-tree/-/ps-tree-1.1.0.tgz#b421b24140d6203f1ed3c76996b4427b08e8c014" - dependencies: - event-stream "~3.3.0" - -pseudomap@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" - -punycode@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - -qs@6.2.0, qs@^6.0.2, qs@^6.1.0, qs@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.2.0.tgz#3b7848c03c2dece69a9522b0fae8c4126d745f3b" - -qs@~6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.0.tgz#f403b264f23bc01228c74131b407f18d5ea5d442" - -randomatic@^1.1.3: - version "1.1.5" - resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.5.tgz#5e9ef5f2d573c67bd2b8124ae90b5156e457840b" - dependencies: - is-number "^2.0.2" - kind-of "^3.0.2" - -range-parser@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" - -raw-body@~2.1.7: - version "2.1.7" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.1.7.tgz#adfeace2e4fb3098058014d08c072dcc59758774" - dependencies: - bytes "2.4.0" - iconv-lite "0.4.13" - unpipe "1.0.0" - -rc@^1.0.1, rc@~1.1.6: - version "1.1.6" - resolved "https://registry.yarnpkg.com/rc/-/rc-1.1.6.tgz#43651b76b6ae53b5c802f1151fa3fc3b059969c9" - dependencies: - deep-extend "~0.4.0" - ini "~1.3.0" - minimist "^1.2.0" - strip-json-comments "~1.0.4" - -read-all-stream@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/read-all-stream/-/read-all-stream-3.1.0.tgz#35c3e177f2078ef789ee4bfafa4373074eaef4fa" - dependencies: - pinkie-promise "^2.0.0" - readable-stream "^2.0.0" - -readable-stream@2.1.5, readable-stream@~2.1.4: - version "2.1.5" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.1.5.tgz#66fa8b720e1438b364681f2ad1a63c618448c9d0" - dependencies: - buffer-shims "^1.0.0" - core-util-is "~1.0.0" - inherits "~2.0.1" - isarray "~1.0.0" - process-nextick-args "~1.0.6" - string_decoder "~0.10.x" - util-deprecate "~1.0.1" - -readable-stream@^2.0.0, "readable-stream@^2.0.0 || ^1.1.13", readable-stream@^2.0.2, readable-stream@^2.0.5: - version "2.2.2" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.2.tgz#a9e6fec3c7dda85f8bb1b3ba7028604556fc825e" - dependencies: - buffer-shims "^1.0.0" - core-util-is "~1.0.0" - inherits "~2.0.1" - isarray "~1.0.0" - process-nextick-args "~1.0.6" - string_decoder "~0.10.x" - util-deprecate "~1.0.1" - -readable-stream@~2.0.0: - version "2.0.6" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e" - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.1" - isarray "~1.0.0" - process-nextick-args "~1.0.6" - string_decoder "~0.10.x" - util-deprecate "~1.0.1" - -readdirp@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78" - dependencies: - graceful-fs "^4.1.2" - minimatch "^3.0.2" - readable-stream "^2.0.2" - set-immediate-shim "^1.0.1" - -readline2@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/readline2/-/readline2-1.0.1.tgz#41059608ffc154757b715d9989d199ffbf372e35" - dependencies: - code-point-at "^1.0.0" - is-fullwidth-code-point "^1.0.0" - mute-stream "0.0.5" - -rechoir@^0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" - dependencies: - resolve "^1.1.6" - -recursive-readdir@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.1.0.tgz#78b7bfd79582d3d7596b8ff1bd29fbd50229f6aa" - dependencies: - minimatch "3.0.2" - -regenerate@^1.2.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.2.tgz#d1941c67bad437e1be76433add5b385f95b19260" - -regenerator-runtime@^0.9.5: - version "0.9.6" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.9.6.tgz#d33eb95d0d2001a4be39659707c51b0cb71ce029" - -regex-cache@^0.4.2: - version "0.4.3" - resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.3.tgz#9b1a6c35d4d0dfcef5711ae651e8e9d3d7114145" - dependencies: - is-equal-shallow "^0.1.3" - is-primitive "^2.0.0" - -regexp-clone@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/regexp-clone/-/regexp-clone-0.0.1.tgz#a7c2e09891fdbf38fbb10d376fb73003e68ac589" - -regexpu-core@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-2.0.0.tgz#49d038837b8dcf8bfa5b9a42139938e6ea2ae240" - dependencies: - regenerate "^1.2.1" - regjsgen "^0.2.0" - regjsparser "^0.1.4" - -registry-url@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-3.1.0.tgz#3d4ef870f73dde1d77f0cf9a381432444e174942" - dependencies: - rc "^1.0.1" - -regjsgen@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.2.0.tgz#6c016adeac554f75823fe37ac05b92d5a4edb1f7" - -regjsparser@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.1.5.tgz#7ee8f84dc6fa792d3fd0ae228d24bd949ead205c" - dependencies: - jsesc "~0.5.0" - -repeat-element@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a" - -repeat-string@^1.5.2: - version "1.6.1" - resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" - -repeating@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/repeating/-/repeating-1.1.3.tgz#3d4114218877537494f97f77f9785fab810fa4ac" - dependencies: - is-finite "^1.0.0" - -repeating@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" - dependencies: - is-finite "^1.0.0" - -request@^2.64.0, request@^2.75.0: - version "2.79.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.79.0.tgz#4dfe5bf6be8b8cdc37fcf93e04b65577722710de" - dependencies: - aws-sign2 "~0.6.0" - aws4 "^1.2.1" - caseless "~0.11.0" - combined-stream "~1.0.5" - extend "~3.0.0" - forever-agent "~0.6.1" - form-data "~2.1.1" - har-validator "~2.0.6" - hawk "~3.1.3" - http-signature "~1.1.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.7" - oauth-sign "~0.8.1" - qs "~6.3.0" - stringstream "~0.0.4" - tough-cookie "~2.3.0" - tunnel-agent "~0.4.1" - uuid "^3.0.0" - -require-uncached@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3" - dependencies: - caller-path "^0.1.0" - resolve-from "^1.0.0" - -require_optional@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/require_optional/-/require_optional-1.0.0.tgz#52a86137a849728eb60a55533617f8f914f59abf" - dependencies: - resolve-from "^2.0.0" - semver "^5.1.0" - -resolve-from@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226" - -resolve-from@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-2.0.0.tgz#9480ab20e94ffa1d9e80a804c7ea147611966b57" - -resolve@1.1.x, resolve@^1.1.6: - version "1.1.7" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" - -restore-cursor@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541" - dependencies: - exit-hook "^1.0.0" - onetime "^1.0.0" - -retry@^0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/retry/-/retry-0.9.0.tgz#6f697e50a0e4ddc8c8f7fb547a9b60dead43678d" - -right-align@^0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" - dependencies: - align-text "^0.1.1" - -rimraf@2, rimraf@^2.2.8, rimraf@~2.5.1, rimraf@~2.5.4: - version "2.5.4" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.5.4.tgz#96800093cbf1a0c86bd95b4625467535c29dfa04" - dependencies: - glob "^7.0.5" - -run-async@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389" - dependencies: - once "^1.3.0" - -rx-lite@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102" - -samsam@1.1.2, samsam@~1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.1.2.tgz#bec11fdc83a9fda063401210e40176c3024d1567" - -semver-diff@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36" - dependencies: - semver "^5.0.3" - -semver@^5.0.3, semver@^5.1.0, semver@~5.1.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.1.1.tgz#a3292a373e6f3e0798da0b20641b9a9c5bc47e19" - -semver@~5.0.1: - version "5.0.3" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.0.3.tgz#77466de589cd5d3c95f138aa78bc569a3cb5d27a" - -semver@~5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" - -send@0.14.1: - version "0.14.1" - resolved "https://registry.yarnpkg.com/send/-/send-0.14.1.tgz#a954984325392f51532a7760760e459598c89f7a" - dependencies: - debug "~2.2.0" - depd "~1.1.0" - destroy "~1.0.4" - encodeurl "~1.0.1" - escape-html "~1.0.3" - etag "~1.7.0" - fresh "0.3.0" - http-errors "~1.5.0" - mime "1.3.4" - ms "0.7.1" - on-finished "~2.3.0" - range-parser "~1.2.0" - statuses "~1.3.0" - -serve-static@~1.11.1: - version "1.11.1" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.11.1.tgz#d6cce7693505f733c759de57befc1af76c0f0805" - dependencies: - encodeurl "~1.0.1" - escape-html "~1.0.3" - parseurl "~1.3.1" - send "0.14.1" - -set-blocking@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" - -set-immediate-shim@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" - -setprototypeof@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.2.tgz#81a552141ec104b88e89ce383103ad5c66564d08" - -shelljs@^0.7.5: - version "0.7.5" - resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.5.tgz#2eef7a50a21e1ccf37da00df767ec69e30ad0675" - dependencies: - glob "^7.0.0" - interpret "^1.0.0" - rechoir "^0.6.2" - -signal-exit@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.1.tgz#5a4c884992b63a7acd9badb7894c3ee9cfccad81" - -sinon@^1.17.6: - version "1.17.6" - resolved "https://registry.yarnpkg.com/sinon/-/sinon-1.17.6.tgz#a43116db59577c8296356afee13fafc2332e58e1" - dependencies: - formatio "1.1.1" - lolex "1.3.2" - samsam "1.1.2" - util ">=0.10.3 <1" - -slash@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" - -slice-ansi@0.0.4: - version "0.0.4" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35" - -sliced@0.0.5: - version "0.0.5" - resolved "https://registry.yarnpkg.com/sliced/-/sliced-0.0.5.tgz#5edc044ca4eb6f7816d50ba2fc63e25d8fe4707f" - -sliced@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/sliced/-/sliced-1.0.1.tgz#0b3a662b5d04c3177b1926bea82b03f837a2ef41" - -slide@^1.1.5: - version "1.1.6" - resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707" - -sntp@1.x.x: - version "1.0.9" - resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" - dependencies: - hoek "2.x.x" - -source-map-support@^0.4.2: - version "0.4.6" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.6.tgz#32552aa64b458392a85eab3b0b5ee61527167aeb" - dependencies: - source-map "^0.5.3" - -source-map@^0.4.4: - version "0.4.4" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b" - dependencies: - amdefine ">=0.0.4" - -source-map@^0.5.0, source-map@^0.5.3, source-map@~0.5.1: - version "0.5.6" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" - -source-map@~0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.2.0.tgz#dab73fbcfc2ba819b4de03bd6f6eaa48164b3f9d" - dependencies: - amdefine ">=0.0.4" - -split@0.3: - version "0.3.3" - resolved "https://registry.yarnpkg.com/split/-/split-0.3.3.tgz#cd0eea5e63a211dfff7eb0f091c4133e2d0dd28f" - dependencies: - through "2" - -sprintf-js@~1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - -sshpk@^1.7.0: - version "1.10.1" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.10.1.tgz#30e1a5d329244974a1af61511339d595af6638b0" - dependencies: - asn1 "~0.2.3" - assert-plus "^1.0.0" - dashdash "^1.12.0" - getpass "^0.1.1" - optionalDependencies: - bcrypt-pbkdf "^1.0.0" - ecc-jsbn "~0.1.1" - jodid25519 "^1.0.0" - jsbn "~0.1.0" - tweetnacl "~0.14.0" - -stack-trace@0.0.x, stack-trace@~0.0.7: - version "0.0.9" - resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.9.tgz#a8f6eaeca90674c333e7c43953f275b451510695" - -"statuses@>= 1.3.1 < 2", statuses@~1.3.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" - -stream-combiner@~0.0.4: - version "0.0.4" - resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.0.4.tgz#4d5e433c185261dde623ca3f44c586bcf5c4ad14" - dependencies: - duplexer "~0.1.1" - -stream-shift@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" - -string-length@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/string-length/-/string-length-1.0.1.tgz#56970fb1c38558e9e70b728bf3de269ac45adfac" - dependencies: - strip-ansi "^3.0.0" - -string-width@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" - dependencies: - code-point-at "^1.0.0" - is-fullwidth-code-point "^1.0.0" - strip-ansi "^3.0.0" - -string-width@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.0.0.tgz#635c5436cc72a6e0c387ceca278d4e2eec52687e" - dependencies: - is-fullwidth-code-point "^2.0.0" - strip-ansi "^3.0.0" - -string_decoder@~0.10.x: - version "0.10.31" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" - -stringstream@~0.0.4: - version "0.0.5" - resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" - -strip-ansi@^3.0.0, strip-ansi@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" - dependencies: - ansi-regex "^2.0.0" - -strip-ansi@~0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-0.1.1.tgz#39e8a98d044d150660abe4a6808acf70bb7bc991" - -strip-bom@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" - -strip-json-comments@~1.0.1, strip-json-comments@~1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-1.0.4.tgz#1e15fbcac97d3ee99bf2d73b4c656b082bbafb91" - -superagent-promise@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/superagent-promise/-/superagent-promise-1.1.0.tgz#baf22d8bbdd439a9b07dd10f8c08f54fe2503533" - -superagent@^2.0.0, superagent@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/superagent/-/superagent-2.3.0.tgz#703529a0714e57e123959ddefbce193b2e50d115" - dependencies: - component-emitter "^1.2.0" - cookiejar "^2.0.6" - debug "^2.2.0" - extend "^3.0.0" - form-data "1.0.0-rc4" - formidable "^1.0.17" - methods "^1.1.1" - mime "^1.3.4" - qs "^6.1.0" - readable-stream "^2.0.5" - -supports-color@3.1.2, supports-color@^3.1.0: - version "3.1.2" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.1.2.tgz#72a262894d9d408b956ca05ff37b2ed8a6e2a2d5" - dependencies: - has-flag "^1.0.0" - -supports-color@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" - -table@^3.7.8: - version "3.8.3" - resolved "https://registry.yarnpkg.com/table/-/table-3.8.3.tgz#2bbc542f0fda9861a755d3947fefd8b3f513855f" - dependencies: - ajv "^4.7.0" - ajv-keywords "^1.0.0" - chalk "^1.1.1" - lodash "^4.0.0" - slice-ansi "0.0.4" - string-width "^2.0.0" - -tar-pack@~3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.3.0.tgz#30931816418f55afc4d21775afdd6720cee45dae" - dependencies: - debug "~2.2.0" - fstream "~1.0.10" - fstream-ignore "~1.0.5" - once "~1.3.3" - readable-stream "~2.1.4" - rimraf "~2.5.1" - tar "~2.2.1" - uid-number "~0.0.6" - -tar@~2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1" - dependencies: - block-stream "*" - fstream "^1.0.2" - inherits "2" - -text-table@~0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" - -through@2, through@^2.3.6, through@~2.3, through@~2.3.1: - version "2.3.8" - resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" - -timed-out@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-2.0.0.tgz#f38b0ae81d3747d628001f41dafc652ace671c0a" - -to-fast-properties@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.2.tgz#f3f5c0c3ba7299a7ef99427e44633257ade43320" - -touch@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/touch/-/touch-1.0.0.tgz#449cbe2dbae5a8c8038e30d71fa0ff464947c4de" - dependencies: - nopt "~1.0.10" - -tough-cookie@~2.3.0: - version "2.3.2" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.2.tgz#f081f76e4c85720e6c37a5faced737150d84072a" - dependencies: - punycode "^1.4.1" - -tryit@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/tryit/-/tryit-1.0.3.tgz#393be730a9446fd1ead6da59a014308f36c289cb" - -tsscmp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/tsscmp/-/tsscmp-1.0.5.tgz#7dc4a33af71581ab4337da91d85ca5427ebd9a97" - -tunnel-agent@~0.4.1: - version "0.4.3" - resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.4.3.tgz#6373db76909fe570e08d73583365ed828a74eeeb" - -tweetnacl@^0.14.3, tweetnacl@~0.14.0: - version "0.14.3" - resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.3.tgz#3da382f670f25ded78d7b3d1792119bca0b7132d" - -type-check@~0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" - dependencies: - prelude-ls "~1.1.2" - -type-detect@0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-0.1.1.tgz#0ba5ec2a885640e470ea4e8505971900dac58822" - -type-detect@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-1.0.0.tgz#762217cc06db258ec48908a1298e8b95121e8ea2" - -type-is@~1.6.13: - version "1.6.14" - resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.14.tgz#e219639c17ded1ca0789092dd54a03826b817cb2" - dependencies: - media-typer "0.3.0" - mime-types "~2.1.13" - -typedarray@~0.0.5: - version "0.0.6" - resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" - -uc.micro@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.3.tgz#7ed50d5e0f9a9fb0a573379259f2a77458d50192" - -uglify-js@^2.6: - version "2.7.4" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.7.4.tgz#a295a0de12b6a650c031c40deb0dc40b14568bd2" - dependencies: - async "~0.2.6" - source-map "~0.5.1" - uglify-to-browserify "~1.0.0" - yargs "~3.10.0" - -uglify-to-browserify@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" - -uid-number@~0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" - -ultron@1.0.x: - version "1.0.2" - resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.0.2.tgz#ace116ab557cd197386a4e88f4685378c8b2e4fa" - -undefsafe@0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-0.0.3.tgz#ecca3a03e56b9af17385baac812ac83b994a962f" - -underscore@~1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.6.0.tgz#8b38b10cacdef63337b8b24e4ff86d45aea529a8" - -unpipe@1.0.0, unpipe@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" - -update-notifier@0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-0.5.0.tgz#07b5dc2066b3627ab3b4f530130f7eddda07a4cc" - dependencies: - chalk "^1.0.0" - configstore "^1.0.0" - is-npm "^1.0.0" - latest-version "^1.0.0" - repeating "^1.1.2" - semver-diff "^2.0.0" - string-length "^1.0.0" - -url-join@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/url-join/-/url-join-0.0.1.tgz#1db48ad422d3402469a87f7d97bdebfe4fb1e3c8" - -user-home@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/user-home/-/user-home-1.1.1.tgz#2b5be23a32b63a7c9deb8d0f28d485724a3df190" - -user-home@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/user-home/-/user-home-2.0.0.tgz#9c70bfd8169bc1dcbf48604e0f04b8b49cde9e9f" - dependencies: - os-homedir "^1.0.0" - -util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - -"util@>=0.10.3 <1": - version "0.10.3" - resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" - dependencies: - inherits "2.0.1" - -utils-merge@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.0.tgz#0294fb922bb9375153541c4f7096231f287c8af8" - -uuid@^2.0.1: - version "2.0.3" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a" - -uuid@^3.0.0, uuid@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.0.1.tgz#6544bba2dfda8c1cf17e629a3a305e2bb1fee6c1" - -v8flags@^2.0.10: - version "2.0.11" - resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-2.0.11.tgz#bca8f30f0d6d60612cc2c00641e6962d42ae6881" - dependencies: - user-home "^1.1.1" - -vary@^1, vary@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.0.tgz#e1e5affbbd16ae768dd2674394b9ad3022653140" - -verror@1.3.6: - version "1.3.6" - resolved "https://registry.yarnpkg.com/verror/-/verror-1.3.6.tgz#cff5df12946d297d2baaefaa2689e25be01c005c" - dependencies: - extsprintf "1.0.2" - -which@^1.1.1, which@^1.2.9: - version "1.2.12" - resolved "https://registry.yarnpkg.com/which/-/which-1.2.12.tgz#de67b5e450269f194909ef23ece4ebe416fa1192" - dependencies: - isexe "^1.1.1" - -wide-align@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.0.tgz#40edde802a71fea1f070da3e62dcda2e7add96ad" - dependencies: - string-width "^1.0.1" - -window-size@0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" - -winston@^2.1.1: - version "2.3.0" - resolved "https://registry.yarnpkg.com/winston/-/winston-2.3.0.tgz#207faaab6fccf3fe493743dd2b03dbafc7ceb78c" - dependencies: - async "~1.0.0" - colors "1.0.x" - cycle "1.0.x" - eyes "0.1.x" - isstream "0.1.x" - stack-trace "0.0.x" - -winston@~2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/winston/-/winston-2.2.0.tgz#2c853dd87ab552a8e8485d72cbbf9a2286f029b7" - dependencies: - async "~1.0.0" - colors "1.0.x" - cycle "1.0.x" - eyes "0.1.x" - isstream "0.1.x" - pkginfo "0.3.x" - stack-trace "0.0.x" - -wordwrap@0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" - -wordwrap@^1.0.0, wordwrap@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" - -wordwrap@~0.0.2: - version "0.0.3" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - -wrench@~1.5.9: - version "1.5.9" - resolved "https://registry.yarnpkg.com/wrench/-/wrench-1.5.9.tgz#411691c63a9b2531b1700267279bdeca23b2142a" - -write-file-atomic@^1.1.2: - version "1.2.0" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-1.2.0.tgz#14c66d4e4cb3ca0565c28cf3b7a6f3e4d5938fab" - dependencies: - graceful-fs "^4.1.2" - imurmurhash "^0.1.4" - slide "^1.1.5" - -write@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757" - dependencies: - mkdirp "^0.5.1" - -ws@^1.0.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-1.1.1.tgz#082ddb6c641e85d4bb451f03d52f06eabdb1f018" - dependencies: - options ">=0.0.5" - ultron "1.0.x" - -xdg-basedir@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-2.0.0.tgz#edbc903cc385fc04523d966a335504b5504d1bd2" - dependencies: - os-homedir "^1.0.0" - -xtend@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" - -yallist@^2.0.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" - -yargs@~3.10.0: - version "3.10.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1" - dependencies: - camelcase "^1.0.2" - cliui "^2.1.0" - decamelize "^1.0.0" - window-size "0.1.0"